// BlogBridge -- RSS feed reader, manager, and web based service // Copyright (C) 2002-2006 by R. Pito Salas // // 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; // either version 2 of the License, or (at your option) any later version. // // 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., 59 Temple Place, // Suite 330, Boston, MA 02111-1307 USA // // Contact: R. Pito Salas // mailto:pitosalas@users.sourceforge.net // More information: about BlogBridge // http://www.blogbridge.com // http://sourceforge.net/projects/blogbridge // // $Id: Manager.java,v 1.21 2007/08/24 09:04:04 spyromus Exp $ // package com.salas.bb.plugins; import com.salas.bb.plugins.domain.*; import com.salas.bb.plugins.domain.Package; import com.salas.bb.utils.CommonUtils; import com.salas.bb.utils.FileUtils; import com.salas.bb.utils.StringUtils; import com.salas.bb.utils.i18n.Strings; import com.salas.bb.utils.xml.XmlReaderFactory; import com.salas.bbutilities.opml.utils.EmptyEntityResolver; import org.jdom.Document; import org.jdom.Element; import org.jdom.input.SAXBuilder; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.Preferences; /** * Central manager component. It's responsible for loading plug-in packages when * asked and reporting what's loaded. */ public class Manager { private static final Logger LOG = Logger.getLogger(Manager.class.getName()); static final String KEY_PLUGINS_PACKAGES = "plugins.packages"; static final String KEY_PLUGINS_PACKAGES_TS = "plugins.packages.ts"; private static final String KEY_PLUGINS_UNINSTALL = "plugins.uninstall"; private static final String PACKAGE_NAME_SEPARATOR = ";"; private static final String PACKAGE_XML = "package.xml"; private static final String PACKAGE_FILENAME_PATTERN = ".*\\.(jar|zip)\\s*$"; private static final String NODE_PACKAGE = "package"; private static final String ATTR_PACKAGE_NAME = "name"; private static final String ATTR_PACKAGE_DESCRIPTION = "description"; private static final String ATTR_PACKAGE_VERSION = "version"; private static final String ATTR_PACKAGE_AUTHOR = "author"; private static final String ATTR_PACKAGE_EMAIL = "email"; private static final String NODE_THEME = "theme"; private static final String NODE_ACTIONS = "actions"; private static final String NODE_RESOURCES = "resources"; private static final String NODE_STRINGS = "strings"; private static final String NODE_PREFERENCES = "preferences"; private static final String NODE_SMARTFEED = "smartfeed"; private static final String NODE_CODE = "code"; private static final String NODE_TOOLBAR = "toolbar"; private static final List<Package> INSTALLED_PACKAGES = new ArrayList<Package>(); private static boolean installedPackagesLoaded; private static File pluginDirectory; private static Preferences prefs; private static List<String> uninstallFilenames; private static List<Package> enabledPackages; /** * Initializes the manager. * * @param pluginDirectory plug-in directory (created if missing). * @param appPrefs application preferences. */ public static void initialize(File pluginDirectory, Preferences appPrefs) { Manager.pluginDirectory = pluginDirectory; Manager.prefs = appPrefs; if (!pluginDirectory.exists()) pluginDirectory.mkdirs(); doAutoDeployment(); doUninstall(); uninstallFilenames = new ArrayList<String>(); } /** * Loads packages. */ public static void loadPackages() { enabledPackages = new ArrayList<Package>(); // Load, initialize and populate the list List<File> packages = getPackages(); for (File pckg : packages) { Package p = load(pckg); if (p != null) { try { p.initialize(); } catch (Throwable e) { LOG.log(Level.SEVERE, "Failed to initailize the action.", e); } enabledPackages.add(p); } } } /** * Returns the list of enabled packages. * * @return enabled. */ public static List<Package> getEnabledPackages() { return Collections.unmodifiableList(enabledPackages); } /** * Sets the list of enabled packages. Re-loading doesn't happen. * * @param enabled list of enabled. */ public static void setEnabledPackages(List<Package> enabled) { enabledPackages = enabled; List<String> names = new ArrayList<String>(); if (enabled != null) for (Package pkg : enabled) names.add(pkg.getFileName()); prefs.put(KEY_PLUGINS_PACKAGES, StringUtils.join(names.iterator(), PACKAGE_NAME_SEPARATOR)); prefs.putLong(KEY_PLUGINS_PACKAGES_TS, System.currentTimeMillis()); } /** * Returns the list of all detected installed packages. * * @return packages. */ public static List<Package> getInstalledPackages() { synchronized (INSTALLED_PACKAGES) { if (!installedPackagesLoaded) { installedPackagesLoaded = true; INSTALLED_PACKAGES.clear(); File[] files = pluginDirectory.listFiles(); for (File file : files) { if ((file.isDirectory() || file.getName().matches(PACKAGE_FILENAME_PATTERN)) && !uninstallFilenames.contains(file.getName())) { Package p = load(file); if (p != null) INSTALLED_PACKAGES.add(p); } } } } return INSTALLED_PACKAGES; } /** * Reloads the list of installed packages. * * @return installed packages. */ public static List<Package> reloadInstalledPackages() { synchronized (INSTALLED_PACKAGES) { installedPackagesLoaded = false; return getInstalledPackages(); } } /** * Installs the package file into the plug-ins directory. * * @param packageFile package file or directory. * * @return error message or <code>NULL</code> if succeeded. */ public static String install(File packageFile) { String error = null; if (isPackage(packageFile)) { String name = packageFile.getName(); File dest = new File(pluginDirectory, name); if (dest.exists()) { if (uninstallFilenames.contains(name)) { error = Strings.message("plugin.manager.install.uninstall"); } else { error = Strings.message("plugin.manager.install.exists"); } } else { try { FileUtils.copyRec(packageFile, pluginDirectory); } catch (IOException e) { error = Strings.message("plugin.manager.install.failed"); LOG.log(Level.WARNING, error, e); } } } else error = Strings.message("plugin.manager.install.invalid"); return error; } /** * Uninstalls given packages or schedules it on the next restart. * * @param packages packages to uninstall. */ public static void uninstall(Package ... packages) { for (Package pkg : packages) { String fn = pkg.getFileName(); if (!uninstallFilenames.contains(fn)) uninstallFilenames.add(fn); } updateUninstallFilenamesProperty(); } /** * Updates the uninstall filenames property. */ private static void updateUninstallFilenamesProperty() { prefs.put(KEY_PLUGINS_UNINSTALL, StringUtils.join(uninstallFilenames.iterator(), PACKAGE_NAME_SEPARATOR)); } /** * Removes every package mentioned in the uninstall list. */ private static void doUninstall() { String[] names = getPackageNames(KEY_PLUGINS_UNINSTALL); prefs.remove(KEY_PLUGINS_UNINSTALL); for (String name : names) { File file = new File(pluginDirectory, name); if (!file.exists()) continue; if (file.isFile()) file.delete(); else FileUtils.rmdir(file); } } private static void doAutoDeployment() { ClassLoader loader = Manager.class.getClassLoader(); try { FilenameFilter pluginFilter = new FilenameFilter() { public boolean accept(File dir, String name) { return name != null && name.endsWith(".zip"); } }; String[] pluginNames = { "bb-connect.zip" }; // Find all existing plug-ins File[] deployedFiles = pluginDirectory.listFiles(pluginFilter); // Convert the list of files into the map on names to sizes Map<String, Long> ntsExisting = new HashMap<String, Long>(); for (File file : deployedFiles) ntsExisting.put(file.getName(), file.length()); // Walk through the list of plug-ins to deploy and find new / updated List<String> enPackNames = null; for (String name : pluginNames) { String resource = "resources/plug-ins/" + name; long size = getResourceSize(loader, resource); Long exSize = ntsExisting.get(name); if (exSize == null || exSize != size) { // New or updated CommonUtils.copyResourceToFile(resource, new File(pluginDirectory, name).getAbsolutePath()); // If new -- register as enabled if (exSize == null) { if (enPackNames == null) { List<String> list = Arrays.asList(getPackageNames(KEY_PLUGINS_PACKAGES)); enPackNames = new LinkedList<String>(list); } if (!enPackNames.contains(name)) enPackNames.add(name); } } } // If the package names list is initialized, it means there are new plug-ins if (enPackNames != null) { prefs.put(KEY_PLUGINS_PACKAGES, StringUtils.join(enPackNames.iterator(), PACKAGE_NAME_SEPARATOR)); prefs.putLong(KEY_PLUGINS_PACKAGES_TS, System.currentTimeMillis()); } } catch (Exception e) { LOG.log(Level.SEVERE, "Couldn't perform auto-deployment.", e); } } /** * Returns the length of the resource. * * @param loader loader to access the resource. * @param resource resource name. * * @return length. * * @throws IOException if data access fails. */ private static long getResourceSize(ClassLoader loader, String resource) throws IOException { URL url = loader.getResource(resource); URLConnection con = url.openConnection(); return (long)con.getContentLength(); } // ------------------------------------------------------------------------ // Synchronization // ------------------------------------------------------------------------ /** * Stores current state into preferences map. Simply transfers the keys. * * @param preferences preferences. */ public static void storeState(Map<String, Object> preferences) { String list = StringUtils.join(getPackageNames(KEY_PLUGINS_PACKAGES), PACKAGE_NAME_SEPARATOR); if (StringUtils.isNotEmpty(list)) { preferences.put(KEY_PLUGINS_PACKAGES, StringUtils.toUTF8(list)); // Last change timestamp long ts = prefs.getLong(KEY_PLUGINS_PACKAGES_TS, -1); if (ts != -1) preferences.put(KEY_PLUGINS_PACKAGES_TS, StringUtils.toUTF8(Long.toString(ts))); } } /** * Restores the state from the preferences. Compares the times of key * modifications and decides whether to update or not. * * @param preferences preferences. */ public static void restoreState(Map<String, Object> preferences) { String list = StringUtils.fromUTF8((byte[])preferences.get(KEY_PLUGINS_PACKAGES)); if (StringUtils.isNotEmpty(list)) { // Load TS long ts = -1; String tsS = StringUtils.fromUTF8((byte[])preferences.get(KEY_PLUGINS_PACKAGES_TS)); if (StringUtils.isNotEmpty(tsS)) ts = Long.parseLong(tsS); // Check if local data is more up-to-date long localTs = prefs.getLong(KEY_PLUGINS_PACKAGES_TS, -1); if (localTs < ts || localTs == -1) { // It is, update the preference property prefs.put(KEY_PLUGINS_PACKAGES, list); // Set the last change timestamp to the server-stored prefs.putLong(KEY_PLUGINS_PACKAGES_TS, ts); } } } // ------------------------------------------------------------------------ // Private stuff // ------------------------------------------------------------------------ /** * Returns the list of all packages mentioned in the preferences and existing * in the plug-ins directory. * * @return packages. */ private static List<File> getPackages() { List<File> packages = new ArrayList<File>(); // Get the list of enabled packages and prepend the name // of the recovery plug-in String[] names = getPackageNames(KEY_PLUGINS_PACKAGES); String[] names2 = new String[names.length + 2]; names2[0] = "bb-recovery"; names2[1] = "bb-recovery.zip"; System.arraycopy(names, 0, names2, 2, names.length); for (String name : names2) { File file = new File(pluginDirectory, name); if (file.exists()) packages.add(file); } return packages; } /** * Returns the list of package names read from the preferences by given key. * * @param key key. * * @return package names. */ private static String[] getPackageNames(String key) { String pluginPackageNames = prefs.get(key, ""); return StringUtils.split(pluginPackageNames, PACKAGE_NAME_SEPARATOR); } /** * Returns <code>TRUE</code> if the file is a valid package (the directory or * the archive with package.xml). * * @param file file. * * @return <code>TRUE</code> if the file is a valid package. */ private static boolean isPackage(File file) { boolean is = false; if (file.exists()) { try { // Evaluate package XML URL if (file.isDirectory()) { is = new File(file, PACKAGE_XML).exists(); } else if (file.getName().matches(PACKAGE_FILENAME_PATTERN)) { URL fileURL = file.toURL(); ClassLoader loader = new URLClassLoader(new URL[] { fileURL }, Manager.class.getClassLoader()); is = loader.getResourceAsStream(PACKAGE_XML) != null; } } catch (IOException e) { // Incorrect package e.printStackTrace(); } } return is; } /** * Makes and attempt to load a package from file. * * @param packageFile package file. * * @return loaded package or <code>NULL</code> if failed. */ private static Package load(File packageFile) { Package p = null; InputStream is = null; try { // Create loader for the package and initialize the stream ClassLoader loader = new URLClassLoader(new URL[] { packageFile.toURL() }, Manager.class.getClassLoader()); is = loader.getResourceAsStream(PACKAGE_XML); if (is != null) { // Parse the descriptor file SAXBuilder b = new SAXBuilder(false); b.setEntityResolver(EmptyEntityResolver.INSTANCE); Document doc = b.build(XmlReaderFactory.create(is)); // Convert the descriptor into the package p = descriptorToPackage(doc, packageFile, loader); } } catch (Exception e) { LOG.log(Level.WARNING, "Failed to load plug-in package: " + packageFile, e); } finally { try { if (is != null) is.close(); } catch (IOException e) { // Nothing to do here } } return p; } /** * Converts the descriptor document into package. * * @param doc package descriptor document. * @param packageFile package file. * @param loader class loader of the package. * * @return package. * * @throws LoaderException if something goes wrong. */ private static Package descriptorToPackage(Document doc, File packageFile, ClassLoader loader) throws LoaderException { Element elPackage = doc.getRootElement(); if (!NODE_PACKAGE.equals(elPackage.getName())) throw new LoaderException("Wrong root element"); // Mandatory attributes String name = elPackage.getAttributeValue(ATTR_PACKAGE_NAME); String desc = elPackage.getAttributeValue(ATTR_PACKAGE_DESCRIPTION); if (StringUtils.isEmpty(name)) throw new LoaderException("Package name isn't specified"); if (StringUtils.isEmpty(desc)) throw new LoaderException("Package description isn't specified"); Package p = new Package(packageFile.getName(), name, desc, elPackage.getAttributeValue(ATTR_PACKAGE_VERSION), elPackage.getAttributeValue(ATTR_PACKAGE_AUTHOR), elPackage.getAttributeValue(ATTR_PACKAGE_EMAIL)); List elements = elPackage.getChildren(); for (Object elementO : elements) { Element element = (Element)elementO; String elName = element.getName(); IPlugin plugin = null; if (NODE_THEME.equals(elName)) { plugin = parseTheme(element, loader); } else if (NODE_ACTIONS.equals(elName)) { plugin = parseActions(element, loader); } else if (NODE_RESOURCES.equals(elName)) { plugin = parseResources(element, loader); } else if (NODE_STRINGS.equals(elName)) { plugin = parseStrings(element, loader); } else if (NODE_PREFERENCES.equals(elName)) { plugin = parsePreferences(element); } else if (NODE_SMARTFEED.equals(elName)) { plugin = parseSmartFeed(element, loader); } else if (NODE_CODE.equals(elName)) { plugin = parseCode(element, loader); } else if (NODE_TOOLBAR.equals(elName)) { plugin = parseToolbar(element); } if (plugin != null) p.add(plugin); } return p; } /** * Loads theme plug-in. * * @param element theme element list. * @param loader the class loader to use for the resource access. * * @return the plug-in; */ private static IPlugin parseTheme(Element element, ClassLoader loader) { IPlugin tp = null; try { tp = ThemePlugin.create(element, loader); } catch (LoaderException e) { LOG.log(Level.WARNING, "Failed to load a theme", e); } return tp; } /** * Parses actions element. * * @param element element. * @param loader the class loader to use for the resource access. * * @return plugin. */ private static IPlugin parseActions(Element element, ClassLoader loader) { ActionsPlugin pl = null; try { pl = new ActionsPlugin(element, loader); } catch (IllegalArgumentException e) { LOG.log(Level.WARNING, "Failed to create plug-in.", e); } return pl; } /** * Parses resources element. * * @param element element. * @param loader the class loader to use for the resource access. * * @return plugin. */ private static IPlugin parseResources(Element element, ClassLoader loader) { ResourcesPlugin pl = null; try { pl = new ResourcesPlugin(element, loader); } catch (IllegalArgumentException e) { LOG.log(Level.WARNING, "Failed to create plug-in.", e); } return pl; } /** * Parses strings element. * * @param element element. * @param loader the class loader to use for the resource access. * * @return plugin. */ private static IPlugin parseStrings(Element element, ClassLoader loader) { StringsPlugin pl = null; try { pl = new StringsPlugin(element, loader); } catch (IllegalArgumentException e) { LOG.log(Level.WARNING, "Failed to create plug-in.", e); } return pl; } /** * Parses smartfeed element. * * @param element element. * @param loader the class loader to use for the resource access. * * @return plugin. */ private static IPlugin parseSmartFeed(Element element, ClassLoader loader) { IPlugin pl = null; try { pl = SmartFeedPlugin.create(element, loader); } catch (LoaderException e) { LOG.log(Level.WARNING, "Failed to load a smart feed plug-in", e); } return pl; } /** * Creates and returns advanced preferences plug-in. * * @param element element. * * @return plug-in. */ private static IPlugin parsePreferences(Element element) { return new AdvancedPreferencesPlugin(element); } /** * Parses code element. * * @param element element. * @param loader the class loader to use for the resource access. * * @return plugin. */ private static IPlugin parseCode(Element element, ClassLoader loader) { IPlugin pl = null; try { pl = CodePlugin.create(element, loader); } catch (LoaderException e) { LOG.log(Level.WARNING, "Failed to load a code plug-in", e); } return pl; } /** * Parses the plug-in element. * * @param element element. * * @return toolbar plug-in. */ private static IPlugin parseToolbar(Element element) { IPlugin pl = null; try { pl = new ToolbarPlugin(element); } catch (Exception e) { LOG.log(Level.WARNING, "Failed to load a code plug-in", e); } return pl; } }