/*
* Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
package org.opendaylight.yangtools.yang.model.repo.util;
import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.CheckedFuture;
import com.google.common.util.concurrent.Futures;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.opendaylight.yangtools.yang.common.SimpleDateFormatUtil;
import org.opendaylight.yangtools.yang.model.repo.api.MissingSchemaSourceException;
import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceException;
import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
import org.opendaylight.yangtools.yang.model.repo.spi.PotentialSchemaSource.Costs;
import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Cache implementation that stores schemas in form of files under provided folder
*/
public final class FilesystemSchemaSourceCache<T extends SchemaSourceRepresentation> extends AbstractSchemaSourceCache<T> {
private static final Logger LOG = LoggerFactory.getLogger(FilesystemSchemaSourceCache.class);
// Init storage adapters
private static final Map<Class<? extends SchemaSourceRepresentation>, StorageAdapter<? extends SchemaSourceRepresentation>> STORAGE_ADAPTERS =
Collections.singletonMap(
YangTextSchemaSource.class, new YangTextSchemaStorageAdapter());
private static final Pattern CACHED_FILE_PATTERN =
Pattern.compile(
"(?<moduleName>[^@]+)" +
"(@(?<revision>" + SourceIdentifier.REVISION_PATTERN + "))?");
private final Class<T> representation;
private final File storageDirectory;
public FilesystemSchemaSourceCache(
final SchemaSourceRegistry consumer, final Class<T> representation, final File storageDirectory) {
super(consumer, representation, Costs.LOCAL_IO);
this.representation = representation;
this.storageDirectory = Preconditions.checkNotNull(storageDirectory);
checkSupportedRepresentation(representation);
if (!storageDirectory.exists()) {
Preconditions.checkArgument(storageDirectory.mkdirs(), "Unable to create cache directory at %s", storageDirectory);
}
Preconditions.checkArgument(storageDirectory.exists());
Preconditions.checkArgument(storageDirectory.isDirectory());
Preconditions.checkArgument(storageDirectory.canWrite());
Preconditions.checkArgument(storageDirectory.canRead());
init();
}
private static void checkSupportedRepresentation(final Class<? extends SchemaSourceRepresentation> representation) {
for (final Class<? extends SchemaSourceRepresentation> supportedRepresentation : STORAGE_ADAPTERS.keySet()) {
if (supportedRepresentation.isAssignableFrom(representation)) {
return;
}
}
throw new IllegalArgumentException(String.format(
"This cache does not support representation: %s, supported representations are: %s", representation, STORAGE_ADAPTERS.keySet()));
}
/**
* Restore cache state
*/
private void init() {
final CachedModulesFileVisitor fileVisitor = new CachedModulesFileVisitor();
try {
Files.walkFileTree(storageDirectory.toPath(), fileVisitor);
} catch (final IOException e) {
LOG.warn("Unable to restore cache from {}. Starting with empty cache", storageDirectory);
return;
}
for (final SourceIdentifier cachedSchema : fileVisitor.getCachedSchemas()) {
register(cachedSchema);
}
}
@Override
public synchronized CheckedFuture<? extends T, SchemaSourceException> getSource(final SourceIdentifier sourceIdentifier) {
final File file = sourceIdToFile(sourceIdentifier, storageDirectory);
if (file.exists() && file.canRead()) {
LOG.trace("Source {} found in cache as {}", sourceIdentifier, file);
final SchemaSourceRepresentation restored = STORAGE_ADAPTERS.get(representation).restore(sourceIdentifier, file);
return Futures.immediateCheckedFuture(representation.cast(restored));
}
LOG.debug("Source {} not found in cache as {}", sourceIdentifier, file);
return Futures.immediateFailedCheckedFuture(new MissingSchemaSourceException("Source not found", sourceIdentifier));
}
@Override
protected synchronized void offer(final T source) {
LOG.trace("Source {} offered to cache", source.getIdentifier());
final File file = sourceIdToFile(source);
if (file.exists()) {
LOG.debug("Source {} already in cache as {}", source.getIdentifier(), file);
return;
}
storeSource(file, source);
register(source.getIdentifier());
LOG.trace("Source {} stored in cache as {}", source.getIdentifier(), file);
}
private File sourceIdToFile(final T source) {
return sourceIdToFile(source.getIdentifier(), storageDirectory);
}
static File sourceIdToFile(final SourceIdentifier identifier, final File storageDirectory) {
final String rev = identifier.getRevision();
final File file;
if (Strings.isNullOrEmpty(rev)) {
file = findFileWithNewestRev(identifier, storageDirectory);
} else {
file = new File(storageDirectory, identifier.toYangFilename());
}
return file;
}
private static File findFileWithNewestRev(final SourceIdentifier identifier, final File storageDirectory) {
File[] files = storageDirectory.listFiles(new FilenameFilter() {
final Pattern p = Pattern.compile(Pattern.quote(identifier.getName()) + "(\\.yang|@\\d\\d\\d\\d-\\d\\d-\\d\\d.yang)");
@Override
public boolean accept(final File dir, final String name) {
return p.matcher(name).matches();
}
});
if (files.length == 0) {
return new File(storageDirectory, identifier.toYangFilename());
}
if (files.length == 1) {
return files[0];
}
File file = null;
TreeMap<Date, File> map = new TreeMap<>();
for (File sorted : files) {
String fileName = sorted.getName();
Matcher m = SourceIdentifier.REVISION_PATTERN.matcher(fileName);
if (m.find()) {
String revStr = m.group();
/*
* FIXME: Consider using string for comparison.
* String is comparable, pattern check tested format
* so comparing as ASCII string should be sufficient
*/
DateFormat df = SimpleDateFormatUtil.getRevisionFormat();
try {
Date d = df.parse(revStr);
map.put(d, sorted);
} catch (final ParseException e) {
LOG.info("Unable to parse date from yang file name {}", fileName);
map.put(new Date(0L), sorted);
}
} else {
map.put(new Date(0L), sorted);
}
}
file = map.lastEntry().getValue();
return file;
}
private void storeSource(final File file, final T schemaRepresentation) {
STORAGE_ADAPTERS.get(representation).store(file, schemaRepresentation);
}
private static abstract class StorageAdapter<T extends SchemaSourceRepresentation> {
private final Class<T> supportedType;
protected StorageAdapter(final Class<T> supportedType) {
this.supportedType = supportedType;
}
void store(final File file, final SchemaSourceRepresentation schemaSourceRepresentation) {
Preconditions.checkArgument(supportedType.isAssignableFrom(schemaSourceRepresentation.getClass()),
"Cannot store schema source %s, this adapter only supports %s", schemaSourceRepresentation, supportedType);
storeAsType(file, supportedType.cast(schemaSourceRepresentation));
}
protected abstract void storeAsType(final File file, final T cast);
public T restore(final SourceIdentifier sourceIdentifier, final File cachedSource) {
Preconditions.checkArgument(cachedSource.isFile());
Preconditions.checkArgument(cachedSource.exists());
Preconditions.checkArgument(cachedSource.canRead());
return restoreAsType(sourceIdentifier, cachedSource);
}
protected abstract T restoreAsType(final SourceIdentifier sourceIdentifier, final File cachedSource);
}
private static final class YangTextSchemaStorageAdapter extends StorageAdapter<YangTextSchemaSource> {
protected YangTextSchemaStorageAdapter() {
super(YangTextSchemaSource.class);
}
@Override
protected void storeAsType(final File file, final YangTextSchemaSource cast) {
try (final InputStream castStream = cast.openStream()) {
Files.copy(castStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (final IOException e) {
throw new IllegalStateException("Cannot store schema source " + cast.getIdentifier() + " to " + file, e);
}
}
@Override
public YangTextSchemaSource restoreAsType(final SourceIdentifier sourceIdentifier, final File cachedSource) {
return new YangTextSchemaSource(sourceIdentifier) {
@Override
protected MoreObjects.ToStringHelper addToStringAttributes(final MoreObjects.ToStringHelper toStringHelper) {
return toStringHelper;
}
@Override
public InputStream openStream() throws IOException {
return Files.newInputStream(cachedSource.toPath());
}
};
}
}
private static final class CachedModulesFileVisitor extends SimpleFileVisitor<Path> {
private final List<SourceIdentifier> cachedSchemas = Lists.newArrayList();
@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
final FileVisitResult fileVisitResult = super.visitFile(file, attrs);
String fileName = file.toFile().getName();
fileName = com.google.common.io.Files.getNameWithoutExtension(fileName);
final Optional<SourceIdentifier> si = getSourceIdentifier(fileName);
if (si.isPresent()) {
LOG.trace("Restoring cached file {} as {}", file, si.get());
cachedSchemas.add(si.get());
} else {
LOG.debug("Skipping cached file {}, cannot restore source identifier from filename: {}, does not match {}", file, fileName, CACHED_FILE_PATTERN);
}
return fileVisitResult;
}
private static Optional<SourceIdentifier> getSourceIdentifier(final String fileName) {
final Matcher matcher = CACHED_FILE_PATTERN.matcher(fileName);
if (matcher.matches()) {
final String moduleName = matcher.group("moduleName");
final String revision = matcher.group("revision");
return Optional.of(RevisionSourceIdentifier.create(moduleName, Optional.fromNullable(revision)));
}
return Optional.absent();
}
@Override
public FileVisitResult visitFileFailed(final Path file, final IOException exc) throws IOException {
LOG.warn("Unable to restore cached file {}. Ignoring", file, exc);
return FileVisitResult.CONTINUE;
}
public List<SourceIdentifier> getCachedSchemas() {
return cachedSchemas;
}
}
}