Package com.salas.bb.plugins

Source Code of com.salas.bb.plugins.Manager

// 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;
    }
}
TOP

Related Classes of com.salas.bb.plugins.Manager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.