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); } }