package teamcomm;
import common.Log;
import data.SPLStandardMessage;
import java.io.File;
import java.io.FilenameFilter;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import teamcomm.data.AdvancedMessage;
import teamcomm.gui.drawings.Drawing;
import teamcomm.gui.drawings.PerPlayer;
import teamcomm.gui.drawings.Static;
/**
* Singleton class for managing the loading of plugins.
*
* @author Felix Thielke
*/
public class PluginLoader {
private static final String PLUGIN_PATH = "plugins/";
private static final String COMMON_DRAWINGS_PLUGIN = "common.jar";
/**
* Teamnumber used for common drawings.
*/
public static final int TEAMNUMBER_COMMON = -1;
private static final PluginLoader instance = new PluginLoader();
private final File pluginDir = new File(PLUGIN_PATH);
private final Map<Integer, Class<? extends AdvancedMessage>> messageClasses = new HashMap<>();
private final Map<Integer, Collection<Drawing>> drawings = new HashMap<>();
private PluginLoader() {
scanJar(new File(pluginDir, COMMON_DRAWINGS_PLUGIN), TEAMNUMBER_COMMON);
}
/**
* Returns the only instance of the plugin loader.
*
* @return instance
*/
public static PluginLoader getInstance() {
return instance;
}
/**
* Returns the class of messages from the given team.
*
* @param teamNumber number of the team
* @return class for instantiating messages from the given team
*/
public Class<? extends SPLStandardMessage> getMessageClass(final int teamNumber) {
final Class<? extends AdvancedMessage> c = messageClasses.get(teamNumber);
return c != null ? c : SPLStandardMessage.class;
}
/**
* Returns drawings for all teams.
*
* @return drawings
*/
public Collection<Drawing> getCommonDrawings() {
return getDrawings(TEAMNUMBER_COMMON);
}
/**
* Returns drawings for the given team.
*
* @param teamNumber number of the team
* @return drawings
*/
public Collection<Drawing> getDrawings(final int teamNumber) {
final Collection<Drawing> ds = drawings.get(teamNumber);
return ds != null ? ds : new ArrayList<Drawing>(0);
}
/**
* Dynamically loads/reloads plugins of the given teams.
*
* @param teamNumbers numbers of the teams
*/
public void update(final Integer... teamNumbers) {
update(new HashSet<>(Arrays.asList(teamNumbers)));
}
/**
* Dynamically loads/reloads plugins of the given teams.
*
* @param teamNumbers numbers of the teams
*/
public void update(final Set<Integer> teamNumbers) {
// Disallow reloading of plugins
// (maybe use a ServiceLoader in the future to allow reloading)
final Iterator<Integer> iter = teamNumbers.iterator();
while (iter.hasNext()) {
final int teamNumber = iter.next();
if (messageClasses.get(teamNumber) == null) {
final Collection<Drawing> ds = drawings.get(teamNumber);
if (ds != null && !ds.isEmpty()) {
iter.remove();
}
} else {
iter.remove();
}
}
// Find dirs that correspond to team numbers
final File[] pluginDirs = pluginDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(final File dir, final String name) {
try {
return teamNumbers.contains(Integer.parseInt(name));
} catch (NumberFormatException e) {
return false;
}
}
});
// Find jar files
for (final File pDir : pluginDirs) {
if (!pDir.isDirectory()) {
continue;
}
final int teamNumber = Integer.parseInt(pDir.getName());
final LinkedList<File> dirs = new LinkedList<>();
dirs.add(pDir);
final List<File> jars = new LinkedList<>();
// Scan plugin directory
while (!dirs.isEmpty()) {
final File dir = dirs.pollFirst();
for (final File file : dir.listFiles()) {
if (file.isDirectory()) {
dirs.addLast(file);
} else if (file.isFile() && file.getName().endsWith(".jar")) {
jars.add(file);
}
}
}
// Load jars
for (final File file : jars) {
scanJar(file, teamNumber);
}
}
}
private void scanJar(final File file, final int teamNumber) {
try {
final Set<String> classNames;
try (final JarFile jar = new JarFile(file)) {
classNames = new HashSet<>();
final Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
final JarEntry entry = entries.nextElement();
if (entry.getName().endsWith(".class")) {
classNames.add(entry.getName().substring(0, entry.getName().length() - 6).replaceAll("/", "\\."));
}
}
}
// Load classes from jar
try (final URLClassLoader loader = new URLClassLoader(new URL[]{file.toURI().toURL()})) {
classLoop:
for (final String className : classNames) {
final Class<?> cls = loader.loadClass(className);
if (AdvancedMessage.class.isAssignableFrom(cls)) {
// Class is a message class: set it as default if no
// other message class exists for the team
if (!messageClasses.containsKey(teamNumber)) {
try {
messageClasses.put(teamNumber, AdvancedMessage.class.cast(cls.newInstance()).getClass());
} catch (final Throwable e) {
Log.error(e.getClass().getSimpleName() + " was thrown while initializing custom message class " + cls.getName() + ": " + e.getMessage());
}
}
} else if (PerPlayer.class.isAssignableFrom(cls) || Static.class.isAssignableFrom(cls)) {
// Class is a drawing: add it to the team drawings
// if it does not yet exist
Collection<Drawing> drawingsForTeam = drawings.get(teamNumber);
if (drawingsForTeam == null) {
drawingsForTeam = new LinkedList<>();
drawings.put(teamNumber, drawingsForTeam);
}
for (final Drawing d : drawingsForTeam) {
if (cls.isInstance(d)) {
continue classLoop;
}
}
try {
final Drawing d = (Drawing) cls.newInstance();
d.setTeamNumber(teamNumber);
drawingsForTeam.add(d);
} catch (final Throwable e) {
Log.error(e.getClass().getSimpleName() + " was thrown while initializing custom drawing " + cls.getName() + ": " + e.getMessage());
}
}
}
}
} catch (final Exception ex) {
Log.error(ex.getClass().getSimpleName() + ": Could not open plugin " + file.getPath() + ": " + ex.getMessage());
}
}
}