package geniusweb.partiesserver; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; 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.partiesserver.repository.AvailableJavaParty; import geniusweb.partiesserver.repository.AvailablePartiesRepo; import geniusweb.partiesserver.repository.AvailableParty; import geniusweb.party.Party; 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("partiesserver"); private final Listenable> watcher; private final Repository repository; /** * * @param repo the repo to be automatically updated. */ public AvailablePartiesUpdater(Repository repo) { 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 extension * */ private String getShortName(File jarfile) { String partyname = jarfile.getName(); return partyname.substring(0, partyname.length() - 4); } /** * Try to load a file. Report to logger if it fails to load. * * @param file the file to load. Supposedly a jar file but we will report if * not. */ private void tryLoad(File file) { try { repository.put( new AvailableJavaParty(getShortName(file), load(file))); } catch (Throwable e) { log.log(Level.WARNING, "Can not load class from file " + file, e); } } /** * Try to load jarfile, check that it contains required info and that the * class can be instantiated. * * @param jarfile the jarfile to load the class from * @return the loaded Class file from the jarfile (the class that is * assigned as 'main class' in the manifest). * @throws IOException * @throws IllegalAccessException * @throws InstantiationException * @throws ClassNotFoundException * @throws IllegalArgumentException if the given file is not a jar file or * can't be read. Or if the file has no * manifest containing a main class. * @throws RuntimeException if party throws something during * construction or while handling calls to * it. */ @SuppressWarnings("unchecked") private Class load(File jarfile) throws IOException, InstantiationException, IllegalAccessException, ClassNotFoundException { /** * We can not call loader.close() as instances of this class may need to * load more later on when they start running. Only when all instances * are dead and gone we could close but it's not clear how to track the * number of active instances nor how to get a callback on that here. */ JarClassLoader1 loader = new JarClassLoader1(toURL(jarfile), getClass().getClassLoader()); String mainclass = loader.getMainClassName(); if (mainclass == null) { throw new IllegalArgumentException( "jar file does not contain manifest with main class"); } Class partyclass = (Class) loader .loadClass(mainclass); partyclass.newInstance(); return partyclass; } private URL toURL(File file) throws MalformedURLException { return new URL("jar:" + file.toURI().toURL() + "!/"); } /** * 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); } }