Cleanup use of Guava library
[yangtools.git] / yang / yang-model-util / src / main / java / org / opendaylight / yangtools / yang / model / repo / util / FilesystemSchemaSourceCache.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.model.repo.util;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.base.MoreObjects.ToStringHelper;
14 import com.google.common.base.Strings;
15 import com.google.common.collect.Lists;
16 import com.google.common.util.concurrent.CheckedFuture;
17 import com.google.common.util.concurrent.Futures;
18 import java.io.File;
19 import java.io.FilenameFilter;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.nio.file.FileVisitResult;
23 import java.nio.file.Files;
24 import java.nio.file.Path;
25 import java.nio.file.SimpleFileVisitor;
26 import java.nio.file.StandardCopyOption;
27 import java.nio.file.attribute.BasicFileAttributes;
28 import java.text.DateFormat;
29 import java.text.ParseException;
30 import java.util.Collections;
31 import java.util.Date;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Optional;
35 import java.util.TreeMap;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38 import org.opendaylight.yangtools.yang.common.SimpleDateFormatUtil;
39 import org.opendaylight.yangtools.yang.model.repo.api.MissingSchemaSourceException;
40 import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
41 import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceException;
42 import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
43 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
44 import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
45 import org.opendaylight.yangtools.yang.model.repo.spi.PotentialSchemaSource.Costs;
46 import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceRegistry;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 /**
51  * Cache implementation that stores schemas in form of files under provided folder.
52  */
53 public final class FilesystemSchemaSourceCache<T extends SchemaSourceRepresentation>
54         extends AbstractSchemaSourceCache<T> {
55
56     private static final Logger LOG = LoggerFactory.getLogger(FilesystemSchemaSourceCache.class);
57
58     // Init storage adapters
59     private static final Map<Class<? extends SchemaSourceRepresentation>,
60             StorageAdapter<? extends SchemaSourceRepresentation>> STORAGE_ADAPTERS = Collections.singletonMap(
61                     YangTextSchemaSource.class, new YangTextSchemaStorageAdapter());
62
63     private static final Pattern CACHED_FILE_PATTERN =
64             Pattern.compile("(?<moduleName>[^@]+)" + "(@(?<revision>" + SourceIdentifier.REVISION_PATTERN + "))?");
65
66     private final Class<T> representation;
67     private final File storageDirectory;
68
69     public FilesystemSchemaSourceCache(
70             final SchemaSourceRegistry consumer, final Class<T> representation, final File storageDirectory) {
71         super(consumer, representation, Costs.LOCAL_IO);
72         this.representation = representation;
73         this.storageDirectory = requireNonNull(storageDirectory);
74
75         checkSupportedRepresentation(representation);
76
77         if (!storageDirectory.exists()) {
78             checkArgument(storageDirectory.mkdirs(), "Unable to create cache directory at %s", storageDirectory);
79         }
80         checkArgument(storageDirectory.exists());
81         checkArgument(storageDirectory.isDirectory());
82         checkArgument(storageDirectory.canWrite());
83         checkArgument(storageDirectory.canRead());
84
85         init();
86     }
87
88     private static void checkSupportedRepresentation(final Class<? extends SchemaSourceRepresentation> representation) {
89         for (final Class<? extends SchemaSourceRepresentation> supportedRepresentation : STORAGE_ADAPTERS.keySet()) {
90             if (supportedRepresentation.isAssignableFrom(representation)) {
91                 return;
92             }
93         }
94
95         throw new IllegalArgumentException(String.format(
96                    "This cache does not support representation: %s, supported representations are: %s",
97                    representation, STORAGE_ADAPTERS.keySet()));
98     }
99
100     /**
101      * Restore cache state.
102      */
103     private void init() {
104
105         final CachedModulesFileVisitor fileVisitor = new CachedModulesFileVisitor();
106         try {
107             Files.walkFileTree(storageDirectory.toPath(), fileVisitor);
108         } catch (final IOException e) {
109             LOG.warn("Unable to restore cache from {}. Starting with empty cache", storageDirectory);
110             return;
111         }
112
113         for (final SourceIdentifier cachedSchema : fileVisitor.getCachedSchemas()) {
114             register(cachedSchema);
115         }
116     }
117
118     @Override
119     public synchronized CheckedFuture<? extends T, SchemaSourceException> getSource(
120             final SourceIdentifier sourceIdentifier) {
121         final File file = sourceIdToFile(sourceIdentifier, storageDirectory);
122         if (file.exists() && file.canRead()) {
123             LOG.trace("Source {} found in cache as {}", sourceIdentifier, file);
124             final SchemaSourceRepresentation restored = STORAGE_ADAPTERS.get(representation).restore(sourceIdentifier,
125                     file);
126             return Futures.immediateCheckedFuture(representation.cast(restored));
127         }
128
129         LOG.debug("Source {} not found in cache as {}", sourceIdentifier, file);
130         return Futures.immediateFailedCheckedFuture(new MissingSchemaSourceException("Source not found",
131                     sourceIdentifier));
132     }
133
134     @Override
135     protected synchronized void offer(final T source) {
136         LOG.trace("Source {} offered to cache", source.getIdentifier());
137         final File file = sourceIdToFile(source);
138         if (file.exists()) {
139             LOG.debug("Source {} already in cache as {}", source.getIdentifier(), file);
140             return;
141         }
142
143         storeSource(file, source);
144         register(source.getIdentifier());
145         LOG.trace("Source {} stored in cache as {}", source.getIdentifier(), file);
146     }
147
148     private File sourceIdToFile(final T source) {
149         return sourceIdToFile(source.getIdentifier(), storageDirectory);
150     }
151
152     static File sourceIdToFile(final SourceIdentifier identifier, final File storageDirectory) {
153         final String rev = identifier.getRevision();
154         final File file;
155         if (Strings.isNullOrEmpty(rev)) {
156             file = findFileWithNewestRev(identifier, storageDirectory);
157         } else {
158             file = new File(storageDirectory, identifier.toYangFilename());
159         }
160         return file;
161     }
162
163     private static File findFileWithNewestRev(final SourceIdentifier identifier, final File storageDirectory) {
164         File[] files = storageDirectory.listFiles(new FilenameFilter() {
165             final Pattern pat = Pattern.compile(Pattern.quote(identifier.getName())
166                     + "(\\.yang|@\\d\\d\\d\\d-\\d\\d-\\d\\d.yang)");
167
168             @Override
169             public boolean accept(final File dir, final String name) {
170                 return pat.matcher(name).matches();
171             }
172         });
173
174         if (files.length == 0) {
175             return new File(storageDirectory, identifier.toYangFilename());
176         }
177         if (files.length == 1) {
178             return files[0];
179         }
180
181         File file = null;
182         TreeMap<Date, File> map = new TreeMap<>();
183         for (File sorted : files) {
184             String fileName = sorted.getName();
185             Matcher match = SourceIdentifier.REVISION_PATTERN.matcher(fileName);
186             if (match.find()) {
187                 String revStr = match.group();
188                 /*
189                  * FIXME: Consider using string for comparison.
190                  * String is comparable, pattern check tested format
191                  * so comparing as ASCII string should be sufficient
192                  */
193                 DateFormat df = SimpleDateFormatUtil.getRevisionFormat();
194                 try {
195                     Date date = df.parse(revStr);
196                     map.put(date, sorted);
197                 } catch (final ParseException e) {
198                     LOG.info("Unable to parse date from yang file name {}", fileName);
199                     map.put(new Date(0L), sorted);
200                 }
201
202             } else {
203                 map.put(new Date(0L), sorted);
204             }
205         }
206         file = map.lastEntry().getValue();
207
208         return file;
209     }
210
211     private void storeSource(final File file, final T schemaRepresentation) {
212         STORAGE_ADAPTERS.get(representation).store(file, schemaRepresentation);
213     }
214
215     private abstract static class StorageAdapter<T extends SchemaSourceRepresentation> {
216
217         private final Class<T> supportedType;
218
219         protected StorageAdapter(final Class<T> supportedType) {
220             this.supportedType = supportedType;
221         }
222
223         void store(final File file, final SchemaSourceRepresentation schemaSourceRepresentation) {
224             checkArgument(supportedType.isAssignableFrom(schemaSourceRepresentation.getClass()),
225                     "Cannot store schema source %s, this adapter only supports %s", schemaSourceRepresentation,
226                     supportedType);
227
228             storeAsType(file, supportedType.cast(schemaSourceRepresentation));
229         }
230
231         protected abstract void storeAsType(File file, T cast);
232
233         public T restore(final SourceIdentifier sourceIdentifier, final File cachedSource) {
234             checkArgument(cachedSource.isFile());
235             checkArgument(cachedSource.exists());
236             checkArgument(cachedSource.canRead());
237             return restoreAsType(sourceIdentifier, cachedSource);
238         }
239
240         protected abstract T restoreAsType(SourceIdentifier sourceIdentifier, File cachedSource);
241     }
242
243     private static final class YangTextSchemaStorageAdapter extends StorageAdapter<YangTextSchemaSource> {
244
245         protected YangTextSchemaStorageAdapter() {
246             super(YangTextSchemaSource.class);
247         }
248
249         @Override
250         protected void storeAsType(final File file, final YangTextSchemaSource cast) {
251             try (InputStream castStream = cast.openStream()) {
252                 Files.copy(castStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
253             } catch (final IOException e) {
254                 throw new IllegalStateException("Cannot store schema source " + cast.getIdentifier() + " to " + file,
255                         e);
256             }
257         }
258
259         @Override
260         public YangTextSchemaSource restoreAsType(final SourceIdentifier sourceIdentifier, final File cachedSource) {
261             return new YangTextSchemaSource(sourceIdentifier) {
262
263                 @Override
264                 protected ToStringHelper addToStringAttributes(final ToStringHelper toStringHelper) {
265                     return toStringHelper;
266                 }
267
268                 @Override
269                 public InputStream openStream() throws IOException {
270                     return Files.newInputStream(cachedSource.toPath());
271                 }
272             };
273         }
274     }
275
276     private static final class CachedModulesFileVisitor extends SimpleFileVisitor<Path> {
277         private final List<SourceIdentifier> cachedSchemas = Lists.newArrayList();
278
279         @Override
280         public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
281             final FileVisitResult fileVisitResult = super.visitFile(file, attrs);
282             String fileName = file.toFile().getName();
283             fileName = com.google.common.io.Files.getNameWithoutExtension(fileName);
284
285             final Optional<SourceIdentifier> si = getSourceIdentifier(fileName);
286             if (si.isPresent()) {
287                 LOG.trace("Restoring cached file {} as {}", file, si.get());
288                 cachedSchemas.add(si.get());
289             } else {
290                 LOG.debug("Skipping cached file {}, cannot restore source identifier from filename: {},"
291                         + " does not match {}", file, fileName, CACHED_FILE_PATTERN);
292             }
293             return fileVisitResult;
294         }
295
296         private static Optional<SourceIdentifier> getSourceIdentifier(final String fileName) {
297             final Matcher matcher = CACHED_FILE_PATTERN.matcher(fileName);
298             if (matcher.matches()) {
299                 final String moduleName = matcher.group("moduleName");
300                 final String revision = matcher.group("revision");
301                 return Optional.of(RevisionSourceIdentifier.create(moduleName, Optional.ofNullable(revision)));
302             }
303             return Optional.empty();
304         }
305
306         @Override
307         public FileVisitResult visitFileFailed(final Path file, final IOException exc) throws IOException {
308             LOG.warn("Unable to restore cached file {}. Ignoring", file, exc);
309             return FileVisitResult.CONTINUE;
310         }
311
312         public List<SourceIdentifier> getCachedSchemas() {
313             return cachedSchemas;
314         }
315     }
316 }