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