blob: 84d94a936417eb97d8fec1c4792d1ff3af09f513 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011-2020 The University of York, Aston University.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
*
* Contributors:
* Konstantinos Barmpis - initial API and implementation
* Antonio Garcia-Dominguez - use Java 7 Path instead of File+string processing,
* use MapDB, refactor into shared part with LocalFile, add file filtering
******************************************************************************/
package org.eclipse.hawk.localfolder;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.VcsChangeType;
import org.eclipse.hawk.core.VcsCommit;
import org.eclipse.hawk.core.VcsCommitItem;
import org.eclipse.hawk.core.VcsRepositoryDelta;
import org.mapdb.DB;
import org.mapdb.DBMaker;
/**
* VCS manager that watches over the contents of a directory, including its
* subdirectories. Revisions are based on "last modified" timestamps, which
* have millisecond resolution in Linux from Java 9 onwards.
*/
public class LocalFolder extends FileBasedLocation {
private long maxLastModified = 0;
private final class LastModifiedFileVisitor implements FileVisitor<Path> {
private boolean alter;
public LastModifiedFileVisitor(boolean alter) {
this.alter = alter;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
final File f = dir.toFile();
final long currentlatest = getRevisionFromFileMetadata(f);
final Long lastRev = recordedModifiedDates.get(dir.toString());
if (lastRev == null || lastRev < currentlatest) {
if (alter) {
recordedModifiedDates.put(dir.toString(), currentlatest);
}
maxLastModified = Math.max(f.lastModified(), maxLastModified);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (isFileInteresting(dir.toFile())) {
return FileVisitResult.CONTINUE;
} else {
return FileVisitResult.SKIP_SUBTREE;
}
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
final File f = file.toFile();
if (isFileInteresting(f)) {
final long currentlatest = getRevisionFromFileMetadata(f);
final Long lastRev = recordedModifiedDates.get(file.toString());
if (lastRev == null || lastRev < currentlatest) {
if (alter) {
recordedModifiedDates.put(file.toString(), currentlatest);
}
maxLastModified = Math.max(f.lastModified(), maxLastModified);
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
}
private Path rootLocation;
private long lastIndexedRevision = 0;
private Set<File> previousFiles;
private Map<String, Long> recordedModifiedDates;
private Function<File, Boolean> fileFilter;
/**
* MapDB database: using file-backed Java collections allows us to save memory
* when handling folders with a large number of files.
*/
private DB db;
@Override
public void init(String vcsloc, IModelIndexer indexer) throws Exception {
final File fMapDB = File.createTempFile("localfolder", "mapdb");
db = DBMaker.newFileDB(fMapDB).deleteFilesAfterClose().closeOnJvmShutdown().make();
previousFiles = db.createHashSet("previousFiles").make();
recordedModifiedDates = db.createHashMap("recordedModifiedDates").make();
console = indexer.getConsole();
// Accept both regular paths and file:// URIs
Path path;
try {
path = Paths.get(new URI(vcsloc));
} catch (URISyntaxException | IllegalArgumentException ex) {
path = Paths.get(vcsloc);
}
final File canonicalFile = path.toFile().getCanonicalFile();
rootLocation = canonicalFile.toPath();
String repositoryURI = rootLocation.toUri().toString();
// If the file doesn't exist, it might be because this is a local folder in
// a remote server - try to preserve the provided vcsloc as is. Otherwise,
// if the server and the client use different operating systems we could end
// up with an unusable URL in the server.
if (canonicalFile.exists()) {
repositoryURL = repositoryURI;
} else {
repositoryURL = vcsloc;
}
// dont decode it to ensure consistency with other managers
// URLDecoder.decode(repositoryURI.replace("+", "%2B"), "UTF-8");
}
protected String getCurrentRevision(boolean alter) {
try {
final LastModifiedFileVisitor visitor = new LastModifiedFileVisitor(alter);
Files.walkFileTree(rootLocation, visitor);
long ret = maxLastModified;
if (alter) {
lastIndexedRevision = ret;
}
return ret + "";
} catch (IOException ex) {
console.printerrln(ex);
return FIRST_REV;
}
}
@Override
public File importFile(String revision, String p, File temp) {
try {
final String path = URLDecoder.decode(p.replace("+", "%2B"), "UTF-8");
final Path resolvedPath = rootLocation.resolve(path.startsWith("/") ? path.replaceFirst("/", "") : path);
return resolvedPath.toFile();
} catch (Exception e) {
console.printerrln(e);
return null;
}
}
@Override
public boolean isActive() {
return rootLocation == null ? false : rootLocation.toFile().exists();
}
@Override
public void shutdown() {
rootLocation = null;
if (!db.isClosed()) {
db.close();
}
}
@Override
public String getHumanReadableName() {
return "Local Folder Monitor";
}
@Override
public VcsRepositoryDelta getDelta(String startRevision, String endRevision) throws Exception {
List<VcsCommit> commits = new ArrayList<>();
VcsRepositoryDelta delta = new VcsRepositoryDelta(commits);
delta.setManager(this);
// Update maxLastModified (there may be deletes, updating the folder timestamp)
getCurrentRevision();
Set<File> files = new HashSet<>();
addAllFiles(rootLocation.toFile(), files);
previousFiles.removeAll(files);
for (File f : previousFiles) {
VcsCommit commit = new VcsCommit();
commit.setAuthor("i am a local folder driver - no authors recorded");
commit.setJavaDate(new Date());
commit.setMessage("i am a local folder driver - no messages recorded");
commit.setRevision(maxLastModified + "");
commits.add(commit);
VcsCommitItem c = new VcsCommitItem();
c.setChangeType(VcsChangeType.DELETED);
c.setCommit(commit);
Path path = f.toPath();
// Don't decode it to ensure consistency with other managers
String relativepath = makeRelative(repositoryURL,
f.toPath().toUri().toString()
);
c.setPath(relativepath.startsWith("/") ? relativepath : "/" + relativepath);
commit.getItems().add(c);
recordedModifiedDates.remove(path.toString());
}
previousFiles.clear();
if (files != null && files.size() > 0) {
// long newLastModifiedRepository = lastModifiedRepository;
for (File f : files) {
previousFiles.add(f);
Path filePath = f.toPath();
final long latestRev = getRevisionFromFileMetadata(f);
final Long lastDate = recordedModifiedDates.get(filePath.toString());
if (lastDate != null && lastDate == latestRev) {
if ((lastIndexedRevision + "").equals(startRevision))
continue;
}
recordedModifiedDates.put(filePath.toString(), latestRev);
VcsCommit commit = new VcsCommit();
commit.setAuthor("i am a local folder driver - no authors recorded");
commit.setJavaDate(new Date());
commit.setMessage("i am a local folder driver - no messages recorded");
commit.setRevision(maxLastModified + "");
commits.add(commit);
VcsCommitItem c = new VcsCommitItem();
c.setChangeType(VcsChangeType.UPDATED);
c.setCommit(commit);
String relativepath = makeRelative(repositoryURL, f.toPath().toUri().toString());
c.setPath(relativepath.startsWith("/") ? relativepath : ("/" + relativepath));
commit.getItems().add(c);
}
}
// Update the latest revision seen
lastIndexedRevision = maxLastModified;
return delta;
}
public Function<File, Boolean> getFileFilter() {
return fileFilter;
}
public void setFileFilter(Function<File, Boolean> fileFilter) {
this.fileFilter = fileFilter;
}
protected void addAllFiles(File dir, Set<File> ret) {
File[] files = dir.listFiles();
if (files == null) {
// couldn't list files in that directory
console.printerrln("Could not list the entries of " + dir);
return;
}
for (File file : files) {
if (isFileInteresting(file)) {
if (!file.isDirectory()) {
ret.add(file);
} else {
addAllFiles(file, ret);
}
}
}
}
private long getRevisionFromFileMetadata(final File f) {
return f.lastModified();
}
private boolean isFileInteresting(File f) {
return fileFilter == null || fileFilter.apply(f);
}
@Override
public String getDefaultLocation() {
return "file://path/to/folder";
}
}