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