2 * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.yangtools.yang.model.repo.fs;
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;
15 import com.google.common.util.concurrent.FluentFuture;
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;
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.repo.api.MissingSchemaSourceException;
39 import org.opendaylight.yangtools.yang.model.repo.spi.AbstractSchemaSourceCache;
40 import org.opendaylight.yangtools.yang.model.repo.spi.PotentialSchemaSource.Costs;
41 import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceRegistry;
42 import org.opendaylight.yangtools.yang.model.spi.source.YangTextSource;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * Cache implementation that stores schemas in form of files under provided folder.
49 public final class FilesystemSchemaSourceCache<T extends SourceRepresentation> extends AbstractSchemaSourceCache<T> {
50 private static final Logger LOG = LoggerFactory.getLogger(FilesystemSchemaSourceCache.class);
52 // Init storage adapters
53 private static final Map<Class<? extends SourceRepresentation>, StorageAdapter<? extends SourceRepresentation>>
54 STORAGE_ADAPTERS = Collections.singletonMap(YangTextSource.class, new YangTextStorageAdapter());
56 private static final Pattern CACHED_FILE_PATTERN =
57 Pattern.compile("(?<moduleName>[^@]+)" + "(@(?<revision>" + Revision.STRING_FORMAT_PATTERN + "))?");
59 private final Class<T> representation;
60 private final File storageDirectory;
62 public FilesystemSchemaSourceCache(final SchemaSourceRegistry consumer, final Class<T> representation,
63 final File storageDirectory) {
64 super(consumer, representation, Costs.LOCAL_IO);
65 this.representation = representation;
66 this.storageDirectory = requireNonNull(storageDirectory);
68 checkSupportedRepresentation(representation);
70 checkArgument(storageDirectory.mkdirs() || storageDirectory.isDirectory(),
71 "Unable to create cache directory at %s", storageDirectory);
72 checkArgument(storageDirectory.canWrite());
73 checkArgument(storageDirectory.canRead());
78 private static void checkSupportedRepresentation(final Class<? extends SourceRepresentation> representation) {
79 for (final var supportedRepresentation : STORAGE_ADAPTERS.keySet()) {
80 if (supportedRepresentation.isAssignableFrom(representation)) {
85 throw new IllegalArgumentException(String.format(
86 "This cache does not support representation: %s, supported representations are: %s",
87 representation, STORAGE_ADAPTERS.keySet()));
91 * Restore cache state.
95 final CachedModulesFileVisitor fileVisitor = new CachedModulesFileVisitor();
97 Files.walkFileTree(storageDirectory.toPath(), fileVisitor);
98 } catch (final IOException e) {
99 LOG.warn("Unable to restore cache from {}. Starting with an empty cache", storageDirectory, e);
103 fileVisitor.getCachedSchemas().stream().forEach(this::register);
107 public synchronized FluentFuture<? extends T> getSource(final SourceIdentifier sourceIdentifier) {
108 final File file = sourceIdToFile(sourceIdentifier, storageDirectory);
109 if (file.exists() && file.canRead()) {
110 LOG.trace("Source {} found in cache as {}", sourceIdentifier, file);
111 final var restored = STORAGE_ADAPTERS.get(representation).restore(sourceIdentifier, file);
112 return immediateFluentFuture(representation.cast(restored));
115 LOG.debug("Source {} not found in cache as {}", sourceIdentifier, file);
116 return immediateFailedFluentFuture(new MissingSchemaSourceException(sourceIdentifier, "Source not found"));
120 protected synchronized void offer(final T source) {
121 LOG.trace("Source {} offered to cache", source.sourceId());
122 final File file = sourceIdToFile(source);
124 LOG.debug("Source {} already in cache as {}", source.sourceId(), file);
128 storeSource(file, source);
129 register(source.sourceId());
130 LOG.trace("Source {} stored in cache as {}", source.sourceId(), file);
133 private File sourceIdToFile(final T source) {
134 return sourceIdToFile(source.sourceId(), storageDirectory);
137 static File sourceIdToFile(final SourceIdentifier identifier, final File storageDirectory) {
138 final Revision rev = identifier.revision();
141 // FIXME: this does not look right
142 file = findFileWithNewestRev(identifier, storageDirectory);
144 file = new File(storageDirectory, identifier.toYangFilename());
149 private static File findFileWithNewestRev(final SourceIdentifier identifier, final File storageDirectory) {
150 File[] files = storageDirectory.listFiles(new FilenameFilter() {
151 final Pattern pat = Pattern.compile(Pattern.quote(identifier.name().getLocalName())
152 + "(\\.yang|@\\d\\d\\d\\d-\\d\\d-\\d\\d.yang)");
155 public boolean accept(final File dir, final String name) {
156 return pat.matcher(name).matches();
160 if (files.length == 0) {
161 return new File(storageDirectory, identifier.toYangFilename());
163 if (files.length == 1) {
168 TreeMap<Optional<Revision>, File> map = new TreeMap<>(Revision::compare);
169 for (File sorted : files) {
170 String fileName = sorted.getName();
171 Matcher match = Revision.STRING_FORMAT_PATTERN.matcher(fileName);
173 String revStr = match.group();
176 rev = Revision.of(revStr);
177 } catch (final DateTimeParseException e) {
178 LOG.info("Unable to parse date from yang file name {}, falling back to not-present", fileName, e);
182 map.put(Optional.ofNullable(rev), sorted);
185 map.put(Optional.empty(), sorted);
188 file = map.lastEntry().getValue();
193 private void storeSource(final File file, final T schemaRepresentation) {
194 STORAGE_ADAPTERS.get(representation).store(file, schemaRepresentation);
197 private abstract static class StorageAdapter<T extends SourceRepresentation> {
198 private final Class<T> supportedType;
200 protected StorageAdapter(final Class<T> supportedType) {
201 this.supportedType = supportedType;
204 void store(final File file, final SourceRepresentation schemaSourceRepresentation) {
205 checkArgument(supportedType.isAssignableFrom(schemaSourceRepresentation.getClass()),
206 "Cannot store schema source %s, this adapter only supports %s", schemaSourceRepresentation,
209 storeAsType(file, supportedType.cast(schemaSourceRepresentation));
212 // FIXME: use java.nio.filePath
213 protected abstract void storeAsType(File file, T cast);
215 T restore(final SourceIdentifier sourceIdentifier, final File cachedSource) {
216 checkArgument(cachedSource.isFile());
217 checkArgument(cachedSource.exists());
218 checkArgument(cachedSource.canRead());
219 return restoreAsType(sourceIdentifier, cachedSource);
222 abstract T restoreAsType(SourceIdentifier sourceIdentifier, File cachedSource);
225 private static final class YangTextStorageAdapter extends StorageAdapter<YangTextSource> {
226 protected YangTextStorageAdapter() {
227 super(YangTextSource.class);
231 protected void storeAsType(final File file, final YangTextSource cast) {
232 try (var castStream = cast.asByteSource(StandardCharsets.UTF_8).openStream()) {
233 Files.copy(castStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
234 } catch (final IOException e) {
235 throw new IllegalStateException("Cannot store schema source " + cast.sourceId() + " to " + file, e);
240 YangTextSource restoreAsType(final SourceIdentifier sourceIdentifier, final File cachedSource) {
241 return YangTextSource.forPath(cachedSource.toPath(), sourceIdentifier);
245 private static final class CachedModulesFileVisitor extends SimpleFileVisitor<Path> {
246 private final List<SourceIdentifier> cachedSchemas = new ArrayList<>();
249 public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
250 final FileVisitResult fileVisitResult = super.visitFile(file, attrs);
251 String fileName = file.toFile().getName();
252 fileName = com.google.common.io.Files.getNameWithoutExtension(fileName);
254 final Optional<SourceIdentifier> si = getSourceIdentifier(fileName);
255 if (si.isPresent()) {
256 LOG.trace("Restoring cached file {} as {}", file, si.orElseThrow());
257 cachedSchemas.add(si.orElseThrow());
259 LOG.debug("Skipping cached file {}, cannot restore source identifier from filename: {},"
260 + " does not match {}", file, fileName, CACHED_FILE_PATTERN);
262 return fileVisitResult;
265 private static Optional<SourceIdentifier> getSourceIdentifier(final String fileName) {
266 final Matcher matcher = CACHED_FILE_PATTERN.matcher(fileName);
267 if (matcher.matches()) {
268 final String moduleName = matcher.group("moduleName");
269 final String revision = matcher.group("revision");
270 return Optional.of(new SourceIdentifier(moduleName, revision));
272 return Optional.empty();
276 public FileVisitResult visitFileFailed(final Path file, final IOException exc) throws IOException {
277 LOG.warn("Unable to restore cached file {}. Ignoring", file, exc);
278 return FileVisitResult.CONTINUE;
281 public List<SourceIdentifier> getCachedSchemas() {
282 return cachedSchemas;