/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* 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
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.vfs.watcher;
import com.google.inject.Inject;
import org.eclipse.che.commons.schedule.ScheduleRate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
import static java.nio.file.Files.exists;
import static java.nio.file.Files.walkFileTree;
import static java.util.stream.Collectors.toSet;
/**
* Walks a file system tree, register addition, update and removal of file system items.
* On events runs corresponding consumers that can be registered in DI configuration modules.
*/
@Singleton
public class FileTreeWalker {
private static final Logger LOG = LoggerFactory.getLogger(FileTreeWalker.class);
private final File root;
private final Set<Consumer<Path>> directoryUpdateConsumers;
private final Set<Consumer<Path>> directoryCreateConsumers;
private final Set<Consumer<Path>> directoryDeleteConsumers;
private final Set<PathMatcher> directoryExcludes;
private final Set<Consumer<Path>> fileUpdateConsumers;
private final Set<Consumer<Path>> fileCreateConsumers;
private final Set<Consumer<Path>> fileDeleteConsumers;
private final Set<PathMatcher> fileExcludes;
private final Map<Path, Long> files = new HashMap<>();
private final Map<Path, Long> directories = new HashMap<>();
@Inject
public FileTreeWalker(@Named("che.user.workspaces.storage") File root,
@Named("che.fs.directory.update") Set<Consumer<Path>> directoryUpdateConsumers,
@Named("che.fs.directory.create") Set<Consumer<Path>> directoryCreateConsumers,
@Named("che.fs.directory.delete") Set<Consumer<Path>> directoryDeleteConsumers,
@Named("che.fs.directory.excludes") Set<PathMatcher> directoryExcludes,
@Named("che.fs.file.update") Set<Consumer<Path>> fileUpdateConsumers,
@Named("che.fs.file.create") Set<Consumer<Path>> fileCreateConsumers,
@Named("che.fs.file.delete") Set<Consumer<Path>> fileDeleteConsumers,
@Named("che.fs.file.excludes") Set<PathMatcher> fileExcludes) {
this.root = root;
this.directoryUpdateConsumers = directoryUpdateConsumers;
this.directoryCreateConsumers = directoryCreateConsumers;
this.directoryDeleteConsumers = directoryDeleteConsumers;
this.fileUpdateConsumers = fileUpdateConsumers;
this.fileCreateConsumers = fileCreateConsumers;
this.fileDeleteConsumers = fileDeleteConsumers;
this.directoryExcludes = directoryExcludes;
this.fileExcludes = fileExcludes;
}
@ScheduleRate(period = 10)
void walk() {
try {
LOG.debug("Tree walk started");
Set<Path> deletedFiles = files.keySet().stream().filter(it -> !exists(it)).collect(toSet());
fileDeleteConsumers.forEach(deletedFiles::forEach);
files.keySet().removeAll(deletedFiles);
Set<Path> deletedDirectories = directories.keySet().stream().filter(it -> !exists(it)).collect(toSet());
directoryDeleteConsumers.forEach(deletedDirectories::forEach);
directories.keySet().removeAll(deletedDirectories);
walkFileTree(root.toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
for (PathMatcher matcher : directoryExcludes) {
if (matcher.matches(dir)) {
return SKIP_SUBTREE;
}
}
updateFsTreeAndAcceptConsumables(directories, directoryUpdateConsumers, directoryCreateConsumers, dir, attrs);
return CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
for (PathMatcher matcher : fileExcludes) {
if (matcher.matches(file)) {
return CONTINUE;
}
}
updateFsTreeAndAcceptConsumables(files, fileUpdateConsumers, fileCreateConsumers, file, attrs);
return CONTINUE;
}
});
LOG.debug("Tree walk finished");
} catch (NoSuchFileException e) {
LOG.debug("Trying to process a file, however seems like it is already not present: {}", e.getMessage());
} catch (Exception e) {
LOG.error("Error while walking file tree", e);
}
}
private void updateFsTreeAndAcceptConsumables(Map<Path, Long> items, Set<Consumer<Path>> updateConsumer,
Set<Consumer<Path>> createConsumer,
Path path, BasicFileAttributes attrs) {
Long lastModifiedActual = attrs.lastModifiedTime().toMillis();
if (items.containsKey(path)) {
Long lastModifiedStored = items.get(path);
if (!lastModifiedActual.equals(lastModifiedStored)) {
updateConsumer.forEach(it -> it.accept(path));
items.put(path, lastModifiedActual);
}
} else {
createConsumer.forEach(it -> it.accept(path));
items.put(path, lastModifiedActual);
}
}
}