Do not subclass YangTextSchemaSource in yang-repo-fs
[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 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFailedFluentFuture;
13 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
14
15 import com.google.common.util.concurrent.FluentFuture;
16 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
17 import java.io.File;
18 import java.io.FilenameFilter;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.nio.file.FileVisitResult;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24 import java.nio.file.SimpleFileVisitor;
25 import java.nio.file.StandardCopyOption;
26 import java.nio.file.attribute.BasicFileAttributes;
27 import java.time.format.DateTimeParseException;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Optional;
33 import java.util.TreeMap;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 import org.opendaylight.yangtools.yang.common.Revision;
37 import org.opendaylight.yangtools.yang.model.repo.api.MissingSchemaSourceException;
38 import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
39 import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
40 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
41 import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
42 import org.opendaylight.yangtools.yang.model.repo.spi.PotentialSchemaSource.Costs;
43 import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceRegistry;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * Cache implementation that stores schemas in form of files under provided folder.
49  */
50 public final class FilesystemSchemaSourceCache<T extends SchemaSourceRepresentation>
51         extends AbstractSchemaSourceCache<T> {
52
53     private static final Logger LOG = LoggerFactory.getLogger(FilesystemSchemaSourceCache.class);
54
55     // Init storage adapters
56     private static final Map<Class<? extends SchemaSourceRepresentation>,
57             StorageAdapter<? extends SchemaSourceRepresentation>> STORAGE_ADAPTERS = Collections.singletonMap(
58                     YangTextSchemaSource.class, new YangTextSchemaStorageAdapter());
59
60     private static final Pattern CACHED_FILE_PATTERN =
61             Pattern.compile("(?<moduleName>[^@]+)" + "(@(?<revision>" + Revision.STRING_FORMAT_PATTERN + "))?");
62
63     private final Class<T> representation;
64     private final File storageDirectory;
65
66     public FilesystemSchemaSourceCache(
67             final SchemaSourceRegistry consumer, final Class<T> representation, final File storageDirectory) {
68         super(consumer, representation, Costs.LOCAL_IO);
69         this.representation = representation;
70         this.storageDirectory = requireNonNull(storageDirectory);
71
72         checkSupportedRepresentation(representation);
73
74         checkArgument(storageDirectory.mkdirs() || storageDirectory.isDirectory(),
75                 "Unable to create cache directory at %s", storageDirectory);
76         checkArgument(storageDirectory.canWrite());
77         checkArgument(storageDirectory.canRead());
78
79         init();
80     }
81
82     private static void checkSupportedRepresentation(final Class<? extends SchemaSourceRepresentation> representation) {
83         for (final Class<? extends SchemaSourceRepresentation> supportedRepresentation : STORAGE_ADAPTERS.keySet()) {
84             if (supportedRepresentation.isAssignableFrom(representation)) {
85                 return;
86             }
87         }
88
89         throw new IllegalArgumentException(String.format(
90                    "This cache does not support representation: %s, supported representations are: %s",
91                    representation, STORAGE_ADAPTERS.keySet()));
92     }
93
94     /**
95      * Restore cache state.
96      */
97     private void init() {
98
99         final CachedModulesFileVisitor fileVisitor = new CachedModulesFileVisitor();
100         try {
101             Files.walkFileTree(storageDirectory.toPath(), fileVisitor);
102         } catch (final IOException e) {
103             LOG.warn("Unable to restore cache from {}. Starting with an empty cache", storageDirectory, e);
104             return;
105         }
106
107         fileVisitor.getCachedSchemas().stream().forEach(this::register);
108     }
109
110     @Override
111     public synchronized FluentFuture<? extends T> getSource(final SourceIdentifier sourceIdentifier) {
112         final File file = sourceIdToFile(sourceIdentifier, storageDirectory);
113         if (file.exists() && file.canRead()) {
114             LOG.trace("Source {} found in cache as {}", sourceIdentifier, file);
115             final SchemaSourceRepresentation restored = STORAGE_ADAPTERS.get(representation).restore(sourceIdentifier,
116                     file);
117             return immediateFluentFuture(representation.cast(restored));
118         }
119
120         LOG.debug("Source {} not found in cache as {}", sourceIdentifier, file);
121         return immediateFailedFluentFuture(new MissingSchemaSourceException("Source not found", sourceIdentifier));
122     }
123
124     @Override
125     protected synchronized void offer(final T source) {
126         LOG.trace("Source {} offered to cache", source.getIdentifier());
127         final File file = sourceIdToFile(source);
128         if (file.exists()) {
129             LOG.debug("Source {} already in cache as {}", source.getIdentifier(), file);
130             return;
131         }
132
133         storeSource(file, source);
134         register(source.getIdentifier());
135         LOG.trace("Source {} stored in cache as {}", source.getIdentifier(), file);
136     }
137
138     private File sourceIdToFile(final T source) {
139         return sourceIdToFile(source.getIdentifier(), storageDirectory);
140     }
141
142     static File sourceIdToFile(final SourceIdentifier identifier, final File storageDirectory) {
143         final Optional<Revision> rev = identifier.getRevision();
144         final File file;
145         if (rev.isEmpty()) {
146             // FIXME: this does not look right
147             file = findFileWithNewestRev(identifier, storageDirectory);
148         } else {
149             file = new File(storageDirectory, identifier.toYangFilename());
150         }
151         return file;
152     }
153
154     @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
155             justification = "listFiles is analyzed to return null")
156     private static File findFileWithNewestRev(final SourceIdentifier identifier, final File storageDirectory) {
157         File[] files = storageDirectory.listFiles(new FilenameFilter() {
158             final Pattern pat = Pattern.compile(Pattern.quote(identifier.getName())
159                     + "(\\.yang|@\\d\\d\\d\\d-\\d\\d-\\d\\d.yang)");
160
161             @Override
162             public boolean accept(final File dir, final String name) {
163                 return pat.matcher(name).matches();
164             }
165         });
166
167         if (files.length == 0) {
168             return new File(storageDirectory, identifier.toYangFilename());
169         }
170         if (files.length == 1) {
171             return files[0];
172         }
173
174         File file = null;
175         TreeMap<Optional<Revision>, File> map = new TreeMap<>(Revision::compare);
176         for (File sorted : files) {
177             String fileName = sorted.getName();
178             Matcher match = Revision.STRING_FORMAT_PATTERN.matcher(fileName);
179             if (match.find()) {
180                 String revStr = match.group();
181                 Revision rev;
182                 try {
183                     rev = Revision.of(revStr);
184                 } catch (final DateTimeParseException e) {
185                     LOG.info("Unable to parse date from yang file name {}, falling back to not-present", fileName, e);
186                     rev = null;
187                 }
188
189                 map.put(Optional.ofNullable(rev), sorted);
190
191             } else {
192                 map.put(Optional.empty(), sorted);
193             }
194         }
195         file = map.lastEntry().getValue();
196
197         return file;
198     }
199
200     private void storeSource(final File file, final T schemaRepresentation) {
201         STORAGE_ADAPTERS.get(representation).store(file, schemaRepresentation);
202     }
203
204     private abstract static class StorageAdapter<T extends SchemaSourceRepresentation> {
205
206         private final Class<T> supportedType;
207
208         protected StorageAdapter(final Class<T> supportedType) {
209             this.supportedType = supportedType;
210         }
211
212         void store(final File file, final SchemaSourceRepresentation schemaSourceRepresentation) {
213             checkArgument(supportedType.isAssignableFrom(schemaSourceRepresentation.getClass()),
214                     "Cannot store schema source %s, this adapter only supports %s", schemaSourceRepresentation,
215                     supportedType);
216
217             storeAsType(file, supportedType.cast(schemaSourceRepresentation));
218         }
219
220         protected abstract void storeAsType(File file, T cast);
221
222         public T restore(final SourceIdentifier sourceIdentifier, final File cachedSource) {
223             checkArgument(cachedSource.isFile());
224             checkArgument(cachedSource.exists());
225             checkArgument(cachedSource.canRead());
226             return restoreAsType(sourceIdentifier, cachedSource);
227         }
228
229         protected abstract T restoreAsType(SourceIdentifier sourceIdentifier, File cachedSource);
230     }
231
232     private static final class YangTextSchemaStorageAdapter extends StorageAdapter<YangTextSchemaSource> {
233
234         protected YangTextSchemaStorageAdapter() {
235             super(YangTextSchemaSource.class);
236         }
237
238         @Override
239         @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE",
240             justification = "https://github.com/spotbugs/spotbugs/issues/600")
241         protected void storeAsType(final File file, final YangTextSchemaSource cast) {
242             try (InputStream castStream = cast.openStream()) {
243                 Files.copy(castStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
244             } catch (final IOException e) {
245                 throw new IllegalStateException("Cannot store schema source " + cast.getIdentifier() + " to " + file,
246                         e);
247             }
248         }
249
250         @Override
251         public YangTextSchemaSource restoreAsType(final SourceIdentifier sourceIdentifier, final File cachedSource) {
252             return YangTextSchemaSource.forFile(cachedSource, sourceIdentifier);
253         }
254     }
255
256     private static final class CachedModulesFileVisitor extends SimpleFileVisitor<Path> {
257         private final List<SourceIdentifier> cachedSchemas = new ArrayList<>();
258
259         @Override
260         public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
261             final FileVisitResult fileVisitResult = super.visitFile(file, attrs);
262             String fileName = file.toFile().getName();
263             fileName = com.google.common.io.Files.getNameWithoutExtension(fileName);
264
265             final Optional<SourceIdentifier> si = getSourceIdentifier(fileName);
266             if (si.isPresent()) {
267                 LOG.trace("Restoring cached file {} as {}", file, si.get());
268                 cachedSchemas.add(si.get());
269             } else {
270                 LOG.debug("Skipping cached file {}, cannot restore source identifier from filename: {},"
271                         + " does not match {}", file, fileName, CACHED_FILE_PATTERN);
272             }
273             return fileVisitResult;
274         }
275
276         private static Optional<SourceIdentifier> getSourceIdentifier(final String fileName) {
277             final Matcher matcher = CACHED_FILE_PATTERN.matcher(fileName);
278             if (matcher.matches()) {
279                 final String moduleName = matcher.group("moduleName");
280                 final String revision = matcher.group("revision");
281                 return Optional.of(RevisionSourceIdentifier.create(moduleName, Revision.ofNullable(revision)));
282             }
283             return Optional.empty();
284         }
285
286         @Override
287         public FileVisitResult visitFileFailed(final Path file, final IOException exc) throws IOException {
288             LOG.warn("Unable to restore cached file {}. Ignoring", file, exc);
289             return FileVisitResult.CONTINUE;
290         }
291
292         public List<SourceIdentifier> getCachedSchemas() {
293             return cachedSchemas;
294         }
295     }
296 }