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 extends Party> 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 extends Party> partyclass = (Class extends Party>) 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);
}
}