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