/* * PS3 Media Server, for streaming any medias to your PS3. * Copyright (C) 2008 A.Brochard * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms.external; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import javax.swing.JLabel; import net.pms.Messages; import net.pms.PMS; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import net.pms.external.URLResolver.URLResult; import net.pms.newgui.LooksFrame; import net.pms.util.FilePermissions; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class takes care of registering plugins. Plugin jars are loaded, * instantiated and stored for later retrieval. */ public class ExternalFactory { /** * For logging messages. */ private static final Logger LOGGER = LoggerFactory.getLogger(ExternalFactory.class); private static final PmsConfiguration configuration = PMS.getConfiguration(); /** * List of external listener class instances. */ private static List<ExternalListener> externalListeners = new ArrayList<>(); /** * List of external listener classes. */ private static List<Class<?>> externalListenerClasses = new ArrayList<>(); /** * List of external listener classes (not yet started). */ private static List<Class<?>> downloadedListenerClasses = new ArrayList<>(); /** * List of urlresolvers. */ private static List<URLResolver> urlResolvers = new ArrayList<>(); private static boolean allDone = false; /** * Returns the list of external listener class instances. * * @return The instances. */ public static List<ExternalListener> getExternalListeners() { return externalListeners; } /** * Stores the instance of an external listener in a list for later * retrieval. The same instance will only be stored once. * * @param listener The instance to store. */ public static void registerListener(ExternalListener listener) { if (!externalListeners.contains(listener)) { externalListeners.add(listener); if (listener instanceof URLResolver) { addURLResolver((URLResolver) listener); } } } /** * Stores the class of an external listener in a list for later retrieval. * The same class will only be stored once. * * @param clazz The class to store. */ private static void registerListenerClass(Class<?> clazz) { if (!externalListenerClasses.contains(clazz)) { externalListenerClasses.add(clazz); } } private static String getMainClass(URL jar) { URL[] jarURLs1 = {jar}; URLClassLoader classLoader = new URLClassLoader(jarURLs1); try { Enumeration<URL> resources; try { // Each plugin .jar file has to contain a resource named "plugin" // which should contain the name of the main plugin class. resources = classLoader.getResources("plugin"); if (resources.hasMoreElements()) { URL url = resources.nextElement(); char[] name; // Determine the plugin main class name from the contents of // the plugin file. try (InputStreamReader in = new InputStreamReader(url.openStream())) { name = new char[512]; in.read(name); } return new String(name).trim(); } } catch (IOException e) { LOGGER.error("Can't load plugin resources", e); } } finally { try { classLoader.close(); } catch (IOException e) { LOGGER.error("Error closing plugin finder: {}", e.getMessage()); LOGGER.trace("", e); } } return null; } private static boolean isLib(URL jar) { return (getMainClass(jar) == null); } public static void loadJARs(URL[] jarURLs, boolean download) { // find lib jars first ArrayList<URL> libs = new ArrayList<>(); for (URL jarURL : jarURLs) { if (isLib(jarURL)) { libs.add(jarURL); } } URL[] jarURLs1 = new URL[libs.size() + 1]; libs.toArray(jarURLs1); int pos = libs.size(); for (URL jarURL : jarURLs) { jarURLs1[pos] = jarURL; loadJAR(jarURLs1, download, jarURL); } } /** * This method loads the jar files found in the plugin dir * or if installed from the web. */ public static void loadJAR(URL[] jarURL, boolean download, URL newURL) { /* Create a classloader to take care of loading the plugin classes from * their URL. * * A not on the suppressed warning: The classloader need to remain open as long * as the loaded classes are in use - in our case forever. * @see http://stackoverflow.com/questions/13944868/leaving-classloader-open-after-first-use */ @SuppressWarnings("resource") URLClassLoader classLoader = new URLClassLoader(jarURL); Enumeration<URL> resources; try { // Each plugin .jar file has to contain a resource named "plugin" // which should contain the name of the main plugin class. resources = classLoader.getResources("plugin"); } catch (IOException e) { LOGGER.error("Can't load plugin resources: {}", e.getMessage()); LOGGER.trace("", e); try { classLoader.close(); } catch (IOException e2) { // Just swallow } return; } while (resources.hasMoreElements()) { URL url = resources.nextElement(); try { // Determine the plugin main class name from the contents of // the plugin file. char[] name; try (InputStreamReader in = new InputStreamReader(url.openStream())) { name = new char[512]; in.read(name); } String pluginMainClassName = new String(name).trim(); LOGGER.info("Found plugin: " + pluginMainClassName); if (download) { // Only purge code when downloading! purgeCode(pluginMainClassName, newURL); } // Try to load the class based on the main class name Class<?> clazz = classLoader.loadClass(pluginMainClassName); registerListenerClass(clazz); if (download) { downloadedListenerClasses.add(clazz); } } catch (Exception | NoClassDefFoundError e) { LOGGER.error("Error loading plugin", e); } } } private static void purgeCode(String mainClass, URL newUrl) { Class<?> clazz1 = null; for (Class<?> clazz : externalListenerClasses) { if (mainClass.equals(clazz.getCanonicalName())) { clazz1 = clazz; break; } } if (clazz1 == null) { return; } externalListenerClasses.remove(clazz1); ExternalListener remove = null; for (ExternalListener list : externalListeners ) { if (list.getClass().equals(clazz1)) { remove = list; break; } } RendererConfiguration.resetAllRenderers(); if (remove != null) { externalListeners.remove(remove); remove.shutdown(); LooksFrame frame = (LooksFrame) PMS.get().getFrame(); frame.getPt().removePlugin(remove); } for (int i = 0; i < 3; i++) { System.gc(); } URLClassLoader cl = (URLClassLoader) clazz1.getClassLoader(); URL[] urls = cl.getURLs(); for (URL url : urls) { String mainClass1 = getMainClass(url); if (mainClass1 == null || !mainClass.equals(mainClass1)) { continue; } File f = url2file(url); File f1 = url2file(newUrl); if (f1 == null || f ==null) { continue; } if (!f1.getName().equals(f.getName())) { addToPurgeFile(f); } } } private static File url2file(URL url) { File f; try { f = new File(url.toURI()); } catch(URISyntaxException e) { f = new File(url.getPath()); } return f; } private static void addToPurgeFile(File f) { try { try (FileWriter out = new FileWriter("purge", true)) { out.write(f.getAbsolutePath() + "\r\n"); out.flush(); } } catch (Exception e) { LOGGER.debug("purge file error " + e); } } private static void purgeFiles() { File purge = new File("purge"); String action = configuration.getPluginPurgeAction(); if (action.equalsIgnoreCase("none")) { purge.delete(); return; } try { try (FileInputStream fis = new FileInputStream(purge); BufferedReader in = new BufferedReader(new InputStreamReader(fis))) { String line; while ((line = in.readLine()) != null) { File f = new File(line); if (action.equalsIgnoreCase("delete")) { f.delete(); } else if(action.equalsIgnoreCase("backup")) { FileUtils.moveFileToDirectory(f, new File("backup"), true); f.delete(); } } } } catch (IOException e) { } purge.delete(); } /** * This method scans the plugins directory for ".jar" files and processes * each file that is found. First, a resource named "plugin" is extracted * from the jar file. Its contents determine the name of the main plugin * class. This main plugin class is then loaded and an instance is created * and registered for later use. */ public static void lookup() { // Start by purging files purgeFiles(); File pluginsFolder = new File(configuration.getPluginDirectory()); LOGGER.info("Searching for plugins in " + pluginsFolder.getAbsolutePath()); try { FilePermissions permissions = new FilePermissions(pluginsFolder); if (!permissions.isFolder()) { LOGGER.warn("Plugins folder is not a folder: " + pluginsFolder.getAbsolutePath()); return; } if (!permissions.isBrowsable()) { LOGGER.warn("Plugins folder is not readable: " + pluginsFolder.getAbsolutePath()); return; } } catch (FileNotFoundException e) { LOGGER.warn("Can't find plugins folder: {}", e.getMessage()); return; } // Find all .jar files in the plugin directory File[] jarFiles = pluginsFolder.listFiles( new FileFilter() { @Override public boolean accept(File file) { return file.isFile() && file.getName().toLowerCase().endsWith(".jar"); } } ); int nJars = (jarFiles == null) ? 0 : jarFiles.length; if (nJars == 0) { LOGGER.info("No plugins found"); return; } // To load a .jar file the filename needs to converted to a file URL List<URL> jarURLList = new ArrayList<>(); for (int i = 0; i < nJars; ++i) { try { jarURLList.add(jarFiles[i].toURI().toURL()); } catch (MalformedURLException e) { LOGGER.error("Can't convert file path " + jarFiles[i] + " to URL", e); } } URL[] jarURLs = new URL[jarURLList.size()]; jarURLList.toArray(jarURLs); // Load the jars loadJARs(jarURLs, false); // Instantiate the early external listeners immediately. instantiateEarlyListeners(); } /** * This method instantiates the external listeners that need to be * instantiated immediately so they can influence the PMS initialization * process. * <p> * Not all external listeners are instantiated immediately to avoid * premature initialization where other parts of PMS have not been * initialized yet. Those listeners are instantiated at a later time by * {@link #instantiateLateListeners()}. */ private static void instantiateEarlyListeners() { for (Class<?> clazz: externalListenerClasses) { // Skip the classes that should not be instantiated at this // time but rather at a later time. if (!AdditionalFolderAtRoot.class.isAssignableFrom(clazz) && !AdditionalFoldersAtRoot.class.isAssignableFrom(clazz)) { try { // Create a new instance of the plugin class and store it ExternalListener instance = (ExternalListener) clazz.newInstance(); registerListener(instance); } catch (InstantiationException | IllegalAccessException e) { LOGGER.error("Error instantiating plugin", e); } } } } /** * This method instantiates the external listeners whose class has not yet * been instantiated by {@link #instantiateEarlyListeners()}. */ public static void instantiateLateListeners() { for (Class<?> clazz: externalListenerClasses) { // Only AdditionalFolderAtRoot and AdditionalFoldersAtRoot // classes have been skipped by lookup(). if (AdditionalFolderAtRoot.class.isAssignableFrom(clazz) || AdditionalFoldersAtRoot.class.isAssignableFrom(clazz)) { try { // Create a new instance of the plugin class and store it ExternalListener instance = (ExternalListener) clazz.newInstance(); registerListener(instance); } catch (InstantiationException | IllegalAccessException e) { LOGGER.error("Error instantiating plugin", e); } } } allDone = true; } private static void postInstall(Class<?> clazz) { Method postInstall; try { postInstall = clazz.getDeclaredMethod("postInstall", (Class<?>[]) null); if (Modifier.isStatic(postInstall.getModifiers())) { postInstall.invoke((Object[]) null, (Object[]) null); } } // Ignore all errors catch (SecurityException | NoSuchMethodException | IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { } } private static void doUpdate(JLabel update, String text) { if (update == null) { return; } update.setText(text); } public static void instantiateDownloaded(JLabel update) { // These are found in the downloadedListenerClasses list for (Class<?> clazz: downloadedListenerClasses) { ExternalListener instance; try { doUpdate(update, Messages.getString("NetworkTab.48") + " " + clazz.getSimpleName()); postInstall(clazz); LOGGER.debug("do inst of " + clazz.getSimpleName()); instance = (ExternalListener) clazz.newInstance(); doUpdate(update,instance.name() + " " + Messages.getString("NetworkTab.49")); registerListener(instance); if (PMS.get().getFrame() instanceof LooksFrame) { LooksFrame frame = (LooksFrame) PMS.get().getFrame(); if (!frame.getPt().appendPlugin(instance)) { LOGGER.warn("Plugin limit of 30 has been reached"); } } } catch (InstantiationException | IllegalAccessException e) { LOGGER.error("Error instantiating plugin", e); } } downloadedListenerClasses.clear(); } public static boolean localPluginsInstalled() { return allDone; } private static boolean quoted(String s) { return s.startsWith("\"") && s.endsWith("\""); } private static String quote(String s) { if (quoted(s)) { return s; } return "\"" + s + "\""; } public static URLResult resolveURL(String url) { String quotedUrl = quote(url); for (URLResolver resolver : urlResolvers) { URLResult res = resolver.urlResolve(url); if (res != null) { if (StringUtils.isEmpty(res.url) || quotedUrl.equals(quote(res.url))) { res.url = null; } if (res.precoder != null && res.precoder.isEmpty()) { res.precoder = null; } if (res.args != null && res.args.isEmpty()) { res.args = null; } if (res.url != null || res.precoder != null || res.args != null) { LOGGER.debug(((ExternalListener)resolver).name() + " resolver:" + (res.url == null ? "" : " url=" + res.url) + (res.precoder == null ? "" : " precoder=" + res.precoder) + (res.args == null ? "" : " args=" + res.args)); return res; } } } return null; } public static void addURLResolver(URLResolver res) { if (urlResolvers.contains(res)) { return; } if (urlResolvers.isEmpty()) { urlResolvers.add(res); return; } String[] tmp = PMS.getConfiguration().getURLResolveOrder(); if (tmp.length == 0) { // no order at all, just add it urlResolvers.add(res); return; } int id = -1; for (int i = 0; i < tmp.length; i++) { if (tmp[i].equalsIgnoreCase(res.name())) { id = i; break; } } if (id == -1) { // no order here, just add it urlResolvers.add(res); return; } if (id > urlResolvers.size()) { // add it last urlResolvers.add(res); return; } urlResolvers.add(id, res); } }