package geniusweb.partiesserver;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.LinkedList;
import java.util.logging.Level;
import geniusweb.javavenv.PythonPartyFactory;
import geniusweb.partiesserver.repository.AvailablePartiesRepo;
import geniusweb.partiesserver.repository.AvailableParty;
import geniusweb.partiesserver.repository.AvailablePythonParty;
import tudelft.utilities.files.FileInfo;
import tudelft.utilities.files.FileWatcher;
import tudelft.utilities.immutablelist.Tuple;
import tudelft.utilities.listener.Listenable;
import tudelft.utilities.logging.ReportToLogger;
import tudelft.utilities.logging.Reporter;
import tudelft.utilities.repository.Repository;
/**
*
* This object keeps the {@link AvailablePartiesRepo} in sync with the file
* system. To work, this must be started in a global background thread,
* independent of current sessions etc. This thread is created when the
* constructor is called.
*
* The {@link #PARTIESREPO} points to a directory that must exist. All the jar
* files in there are collected and tested if they can be loaded and are then
* added to the list of available parties.
*
* Please make only 1 instance of this class. Double updates on the
* {@link AvailablePartiesRepo} is not useful
*/
public class AvailablePartiesUpdater {
private static final String PARTIESREPO = "partiesrepo";
private final Path rootdir;
private final Reporter log = new ReportToLogger("pypartiesserver");
private final Listenable> watcher;
private final Repository repository;
/**
*
* @param repo the repo to be automatically updated.
*/
public AvailablePartiesUpdater(Repository repo) {
log.log(Level.INFO, "START Python parties updater");
this.repository = repo;
this.rootdir = getRootDir();
log.log(Level.INFO, "Repository located at " + this.rootdir);
watcher = getFileWatcher(rootdir.toFile());
watcher.addListener(info -> update(info));
// manually trigger the first event.
update(new Tuple(null,
new FileInfo(rootdir.toFile(), 2)));
}
/**
* Called when some file that may be relevant has changed. Synchronized to
* avoid weird states if filesystem changes rapidly. We ignore everything
* but jar files.
*/
private synchronized void update(Tuple info) {
log.log(Level.INFO, "change: " + info);
File oldfile = info.get1() == null ? null : info.get1().getFile();
File newfile = info.get2() == null ? null : info.get2().getFile();
File file = oldfile != null ? oldfile : newfile;
Path relpath = file.toPath().relativize(rootdir);
// workaround: root gives "" with depth 1 instead of 0.
if (relpath.toString().isEmpty()) {
reset();
return;
}
// it's file or dir inside the root directory
if (newfile == null) {
// file removed
repository.remove(getShortName(file));
} else {
tryLoad(file);
}
}
/**
* @param jarfile a jarfile
* @return the filename only, without .tar.gz extension
*
*/
private String getShortName(File jarfile) {
String partyname = jarfile.getName();
return partyname.substring(0, partyname.length() - 7);
}
/**
* Try to load a file. Report to logger if it fails to load.
*
* @param file the file to load. Supposedly a tar.gz file but we will report
* if not.
*/
private void tryLoad(File file) {
try {
if (!file.getName().endsWith(("tar.gz"))) {
throw new IOException(
"Expected python package.tar.gz but found " + file);
}
repository.put(new AvailablePythonParty(getShortName(file),
new PythonPartyFactory(file)));
} catch (Throwable e) {
log.log(Level.WARNING, "Can not load class from file " + file, e);
}
}
/**
* Remove all parties and reload them all from scratch
*/
private void reset() {
Collection toremove = new LinkedList<>(
repository.list());
for (AvailableParty party : toremove) {
repository.remove(party.getID());
}
File rootfile = rootdir.toFile();
if (rootfile.exists() && rootfile.canRead() && rootfile.isDirectory()) {
for (File file : rootfile.listFiles()) {
tryLoad(file);
}
} else {
log.log(Level.SEVERE, "Can not read repository root directory "
+ rootdir
+ ": it does not exist, can not be read or is not a directory.");
}
}
/**
* Search from location of this class upwards till we find a directory
* called "domainsrepo".
*
* @return the root dir of the repo. This is the 'database' where all known
* profiles are stored.
*/
protected Path getRootDir() {
// If you run this normal from tomcat we are somewhere inside
// projectroot/WEB_INF/classes/....
// Search back upwards to projectroot.
Path dir;
try {
dir = Paths.get(getClass().getProtectionDomain().getCodeSource()
.getLocation().toURI()).getParent();
} catch (URISyntaxException e) {
throw new RuntimeException("problem with path uri", e);
}
log.log(Level.INFO,
"searching for " + PARTIESREPO + " upwards from " + dir);
while (dir.getNameCount() > 1) {
Path repo = dir.resolve(PARTIESREPO);
if (repo.toFile().exists())
return repo;
log.log(Level.INFO, "Directory did not contain repo:" + dir);
dir = dir.getParent();
}
return Paths.get(PARTIESREPO); // bit silly, it's not there, but we need
// to do something.
}
/**
* Factory method that gives a watcher for file changes. See
* {@link FileWatcher}.
*
* @param directory the directory to watch
* @return a class that watches for file changes in the given directory.
* Should ignore changes in sub-directories
*/
protected Listenable> getFileWatcher(
File directory) {
return new FileWatcher(directory, 3000, 1);
}
}