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