com.sslexplorer.extensions.ExtensionBundle.java Source code

Java tutorial

Introduction

Here is the source code for com.sslexplorer.extensions.ExtensionBundle.java

Source

/*
*  SSL-Explorer
*
*  Copyright (C) 2003-2006 3SP LTD. All Rights Reserved
*
*  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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

package com.sslexplorer.extensions;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom.Attribute;
import org.jdom.DataConversionException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

import com.sslexplorer.boot.Util;
import com.sslexplorer.boot.VersionInfo;
import com.sslexplorer.boot.VersionInfo.Version;
import com.sslexplorer.extensions.store.ExtensionStore;
import com.sslexplorer.extensions.types.Plugin;
import com.sslexplorer.extensions.types.PluginType;

/**
 * Extension bundles describe a collection of <i>Extension Descriptors</i>.
 * <p>
 * A bundle may come from one of two places, either the <i>3SP Application Store</i>
 * or from an installed extension bundle retrieved via the <i>Repository</i>.
 * <p>
 * If from the 3SP Application Store, the descriptor will not contain all
 * details, only enough to determine the name, description, versions,
 * dependencies and a few others pieces of information. Such bundles do not have
 * a life cycle in the way local extension bundles do (i.e. be <i>Started</i>,
 * <i>Stopped</i> or <i>Activated</i>).
 * <p>
 * Local extension bundles must go through 3 or 4 phases.
 * <ul>
 * <li>Phase 1. Loading. The XML extension descriptors are loaded and parsed.
 * No change the state of SSL-Explorer is made at this stage. Once this is
 * complete we know the order the extensions should be started</li>
 * <li>Phase 2. Starting. All extensions now have their
 * {@link ExtensionDescriptor#start()} method called. This in turn delegates the
 * start to the <i>Extension Type</i> implementation in use. For example, the
 * {@link PluginType} would create the plug-in instance and invoke the
 * {@link Plugin#startPlugin(com.sslexplorer.extensions.types.PluginDefinition, ExtensionDescriptor, Element)}.
 * If starting fails, an exception is thrown and the extension will be
 * <i>Stopped</i>. method on it.</li>
 * <li>Phase 3. Activation. Only the <i>Plug-ins</i> really use this as they
 * require two phases of initialisation. If activate fails, an exception is
 * thrown and the extension will be <i>Stopped</i>. </li>
 * <li>Phase 4. Stopping. May occur either after <i>Starting</i> or
 * <i>Activation</i>. During this phase the extension should clean up as much
 * as possible (deregister property definitions, user databases or any other
 * extension point).
 * </ul>
 */
public class ExtensionBundle extends ArrayList<ExtensionDescriptor> implements Comparable {

    final static Log log = LogFactory.getLog(ExtensionBundle.class);

    /**
     * Extension 'type' code indicating a new version is available from the 3SP
     * Application Store
     */
    public static final int TYPE_UPDATEABLE = 0;

    /**
     * Extension 'type' code indicating the most up to date version in 3SP
     * Application Store is already correctly installed 
     */
    public static final int TYPE_INSTALLED = 1;

    /**
     * Extension 'type' code indicating the bundle is not currently installed
     * but available from the 3SP Application Store
     */
    public static final int TYPE_INSTALLABLE = 2;

    /**
     * Extension 'type' code indicating the bundle is not an installable bundle,
     * merely a pointer to further instructions as to how to create a complete
     * bundle (this is for bundles that we cannot legally distribute all
     * components)
     */
    public static final int TYPE_CONFIGUREABLE = 3;

    /**
     * Extension 'type' code indicating the bundle has been removed by the
     * administrator but cannot yet be deleted from the local file system as it
     * contains plug-ins that are in use.
     */
    public static final int TYPE_PENDING_REMOVAL = 4;

    /**
     * Extension 'type' code indicating a new bundle has been installed but
     * cannot be started because it contains plug-ins. The administrator should
     * restart the entire server to complete the installation.
     */
    public static final int TYPE_PENDING_INSTALLATION = 5;

    /**
     * Extension 'type' code indicating a bundle has been updated but cannot be
     * restarted because it contains plug-ins. The administrator should restart
     * the entire server to complete the update.
     */
    public static final int TYPE_PENDING_UPDATE = 6;

    /**
     * Extension 'type' code indicating a bundle state has been changed 
     * (i.e. enable or disable) but cannot be restarted because it contains 
     * plug-ins. The administrator should restart the entire server to complete 
     * the change.
     */
    public static final int TYPE_PENDING_STATE_CHANGE = 7;

    /**
     * Status of extension bundle
     */
    public enum ExtensionBundleStatus {
        /**
         * The extension is enabled, but stopped
         */
        ENABLED(0, "enabled"),

        /**
         * The extension is disabled
         */
        DISABLED(1, "disabled"),

        /**
         * The extension has been disabled by the system (cannot be enabled
         */
        SYSTEM_DISABLED(2, "systemDisabled"),

        /**
         * The extension is enabled and started
         */
        STARTED(3, "started"),

        /**
         * The extension is enabled and activated
         */
        ACTIVATED(4, "activated"),

        /**
        * The extension is errored
        */
        ERROR(6, "error");

        private String name;

        private ExtensionBundleStatus(int state, String name) {
            this.name = name;
        }

        /**
         * Determine if the state is {@link #STARTED} or {@link #ACTIVATED}.
         * 
         * @return started or activated
         */
        public boolean isStartedOrActivated() {
            return this == ACTIVATED || this == STARTED;
        }

        /**
         * Get the status name
         * 
         * @return name
         */
        public String getName() {
            return name;
        }

        /**
         * @return boolean
         */
        public boolean isDisabled() {
            return this == DISABLED || this == SYSTEM_DISABLED;
        }
    }

    // Private instance variables

    private File descriptor;
    private Document doc;
    private String description;
    private String license;
    private String productURL;
    private String instructionsURL;
    private VersionInfo.Version version;
    private int type;
    private int order;
    private String id;
    private String name;
    private boolean startOnSetupMode;
    private String licenseFilePath;
    private VersionInfo.Version requiredHostVersion;
    private ExtensionInstaller installer;
    private String category;
    private boolean mandatoryUpdate;
    private ExtensionBundleStatus status = ExtensionBundleStatus.ENABLED;
    private Collection<String> dependencyNames;
    private Throwable error;
    private boolean hidden;
    private boolean devExtension;
    private Element messageElement;
    private String changes;
    private VersionInfo.Version updateVersion;
    private String platform = "";
    private String arch = "";

    /**
     * Constructor for when creating a bundle without having a XML extension
     * descriptor stream.
     * 
     * @param version
     * @param type
     * @param id
     * @param name
     * @param description
     * @param license
     * @param productURL
     * @param instructionsURL
     * @param requiredHostVersion
     * @param dependencyNames collection of dependency names or
     *        <code>null</code> for no dependencies
     * @param category
     * @param mandatoryUpdate
     * @param order
     * @param changes 
     * @param password
     * @param arch
     */
    public ExtensionBundle(Version version, int type, String id, String name, String description, String license,
            String productURL, String instructionsURL, VersionInfo.Version requiredHostVersion,
            Collection<String> dependencyNames, String category, boolean mandatoryUpdate, int order, String changes,
            String platform, String arch) {
        this.version = version;
        this.type = type;
        this.id = id;
        this.name = name;
        this.description = description;
        this.license = license;
        this.productURL = productURL;
        this.order = order;
        this.instructionsURL = instructionsURL;
        this.requiredHostVersion = requiredHostVersion;
        this.dependencyNames = dependencyNames;
        this.category = category;
        this.mandatoryUpdate = mandatoryUpdate;
        this.changes = changes;
        this.platform = platform;
        this.arch = arch;

    }

    /**
     * Constructor for creating a new bundle given a file that contains an XML
     * extension bundle descriptor.
     * 
     * @param descriptor descriptor
     * @param devExtension loaded as dev extension
     */
    public ExtensionBundle(File descriptor, boolean devExtension) {
        this.descriptor = descriptor;
        this.devExtension = devExtension;
    }

    /**
     * Get if this bundle was loaded as a <i>devExtension</i>.
     * 
     * @return dev extension
     */
    public boolean isDevExtension() {
        return devExtension;
    }

    /**
     * Get if this bundle is hidden
     * 
     * @return hidden
     */
    public boolean isHidden() {
        return hidden;
    }

    /**
     * Get the extension bundle ID.
     * 
     * @return bundle ID
     */
    public String getId() {
        return id;
    }

    /**
     * Determines if the extension bundle is updateable.
     * @return boolean
     */
    public boolean isUpdateable() {
        return getType() == ExtensionBundle.TYPE_UPDATEABLE;
    }

    /**
     * Get the order. This determines which order the bundle will get loaded
     * started and activated in (in relative to other extensions). The lower the
     * number the earlier the extension should be loaded
     * 
     * @return order
     */
    public int getOrder() {
        return order;
    }

    /**
     * Get the english name of this extension bundle.
     * 
     * @return english name of bundle
     */
    public String getName() {
        return name;
    }

    /**
     * Get the extension descriptor file that represents this bundle. If the
     * bundle is not loaded locally (i.e. it came from the 3SP Application
     * Store) then this will be <code>null</code>.
     * 
     * @return extension descriptor file
     */
    public File getFile() {
        return descriptor;
    }

    /**
     * Get the category for this bundle. This is used in the extension manager
     * front end to group available extensions.
     * 
     * @return category
     */
    public String getCategory() {
        return category;
    }

    /**
     * Get a collection of the names of the extensions bundles this bundle
     * depends on. If any of the dependencies are not satisfied the extension
     * may not be started. If there are no dependencies <code>null</code> will
     * be returned.
     * 
     * @return extension bundle dependency names
     */
    public Collection<String> getDependencies() {
        return dependencyNames;
    }

    /**
     * Start all extension bundles. This method will start all extension
     * descriptors it contains and is the second phase in an extensions life
     * cycle (after loading).
     * 
     * @throws ExtensionException if any bundle could not be started
     */
    public synchronized void start() throws ExtensionException {

        try {
            if (log.isInfoEnabled()) {
                log.info("Starting extension bundle " + getId());
            }

            // Check we are allowed to start this bundle
            if (getStatus() != ExtensionBundleStatus.ENABLED) {
                throw new ExtensionException(ExtensionException.INVALID_EXTENSION_BUNDLE_STATUS, getId(),
                        "Bundle is not in enabled state.");
            }

            // Check this bundles dependencies are installed and started
            checkDependenciesStarted(this);

            // Start all extensions in this bundle
            ExtensionException ee = null;
            for (Iterator i = iterator(); i.hasNext();) {
                ExtensionDescriptor d = (ExtensionDescriptor) i.next();
                try {
                    d.start();

                    // Set any bundle messages
                    setBundleMessages(d);

                    status = ExtensionBundleStatus.STARTED;
                } catch (ExtensionException ex) {
                    if (ee == null) {
                        ee = ex;
                    }
                } catch (Throwable t) {
                    if (ee == null) {
                        ee = new ExtensionException(ExtensionException.INTERNAL_ERROR, t);
                    }
                }
            }
            if (ee != null) {
                throw ee;
            }
        } catch (ExtensionException ee) {
            log.error("Failed to start extension. ", ee);
            error = ee;
            status = ExtensionBundleStatus.ERROR;
            throw ee;
        }

        error = null;
    }

    private void setBundleMessages(ExtensionDescriptor d) throws ExtensionException {
        if (messageElement != null) {
            for (Iterator i2 = messageElement.getChildren().iterator(); i2.hasNext();) {
                Element el = (Element) i2.next();
                if (!el.getName().equals("message")) {
                    throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                            "<messages> element may only contain <message> elements.");
                }
                String key = el.getAttributeValue("key");
                if (key == null) {
                    throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                            "<message> element must have a key attribute.");
                }
                String aKey = "application." + d.getId() + "." + key;
                if (d.getMessageResources() != null) {
                    if (!d.getMessageResources().isPresent(key)) {
                        d.getMessageResources().setMessage(el.getAttributeValue("locale"), aKey, el.getText());
                    }
                }
            }
        }
    }

    void checkDependenciesStarted(ExtensionBundle bundle) throws ExtensionException {
        if (bundle.getDependencies() != null) {
            for (String dep : bundle.getDependencies()) {
                if (!ExtensionStore.getInstance().isExtensionBundleLoaded(dep)) {
                    throw new ExtensionException(ExtensionException.DEPENDENCY_NOT_INSTALLED, dep, getId());
                }
                ExtensionBundle depBundle = ExtensionStore.getInstance().getExtensionBundle(dep);
                if (!depBundle.getStatus().isStartedOrActivated()) {
                    throw new ExtensionException(ExtensionException.DEPENDENCY_NOT_STARTED, dep, getId());
                }
                checkDependenciesStarted(depBundle);
            }
        }

    }

    void checkDependenciesActivated(ExtensionBundle bundle) throws ExtensionException {
        if (bundle.getDependencies() != null) {
            for (String dep : bundle.getDependencies()) {
                if (!ExtensionStore.getInstance().isExtensionBundleLoaded(dep)) {
                    throw new ExtensionException(ExtensionException.DEPENDENCY_NOT_INSTALLED, dep, getId());
                }
                ExtensionBundle depBundle = ExtensionStore.getInstance().getExtensionBundle(dep);
                if (depBundle.getStatus() != ExtensionBundleStatus.ACTIVATED) {
                    throw new ExtensionException(ExtensionException.DEPENDENCY_NOT_STARTED, dep, getId());
                }
                checkDependenciesActivated(depBundle);
            }
        }

    }

    /**
     * Activate all extension bundles. This method will active all extension
     * descriptors it contains and is the third phase in an extensions life
     * cycle (after starting).
     * 
     * @throws ExtensionException if any bundle could not be activated
     */
    public synchronized void activate() throws ExtensionException {
        try {
            if (isContainsPlugin() && getStatus() != ExtensionBundleStatus.STARTED) {
                throw new ExtensionException(ExtensionException.INVALID_EXTENSION_BUNDLE_STATUS, getId(),
                        "Bundle is not in started so cannot be activated.");
            }

            // Check this bundles dependencies are installed and started
            checkDependenciesActivated(this);

            ExtensionException ee = null;
            for (Iterator i = iterator(); i.hasNext();) {
                ExtensionDescriptor d = (ExtensionDescriptor) i.next();
                try {
                    d.activate();
                    status = ExtensionBundleStatus.ACTIVATED;
                } catch (ExtensionException ex) {
                    if (ee == null) {
                        ee = ex;
                    }
                } catch (Throwable t) {
                    if (ee == null) {
                        ee = new ExtensionException(ExtensionException.INTERNAL_ERROR, t);
                    }
                }
            }
            if (ee != null) {
                throw ee;
            }
        } catch (ExtensionException ee) {
            log.error("Failed to activate extension bundle. ", ee);
            error = ee;
            status = ExtensionBundleStatus.ERROR;
            throw ee;
        }
        error = null;
    }

    /**
     * Stop all extension bundles. This method will active all extension
     * descriptors it contains and is the second or third phase in an extensions
     * life cycle (after starting or activating).
     * 
     * @throws ExtensionException if any bundle could not be stopped
     */
    public synchronized void stop() throws ExtensionException {
        ExtensionException ee = null;
        try {
            for (Iterator i = iterator(); i.hasNext();) {
                ExtensionDescriptor d = (ExtensionDescriptor) i.next();
                try {
                    d.stop();
                } catch (ExtensionException ex) {
                    if (ee == null) {
                        ee = ex;
                    }
                    log.error("Failed to stop extension bundle. ", ex);
                }
            }
            if (ee != null) {
                throw ee;
            }
        } finally {
            status = ExtensionBundleStatus.ENABLED;
        }
    }

    /**
     * Stop all extension bundles. This method will load all extension
     * descriptors it contains and is the first phase in an extensions life
     * cycle.
     * 
     * @throws ExtensionException on any error loading bundles
     */
    public synchronized void load() throws ExtensionException {

        try {
            if (log.isInfoEnabled()) {
                log.info("Loading bundle from " + getFile().getAbsolutePath());
            }

            installer = new ExtensionInstaller(this);
            SAXBuilder sax = new SAXBuilder();
            try {
                doc = sax.build(descriptor);
            } catch (JDOMException jde) {
                jde.printStackTrace();
                throw new ExtensionException(ExtensionException.FAILED_TO_PARSE_DESCRIPTOR, jde);
            } catch (IOException ioe) {
                throw new ExtensionException(ExtensionException.INTERNAL_ERROR, ioe,
                        "Failed to load descriptor for parsing.");
            }

            hidden = "true".equals(doc.getRootElement().getAttributeValue("hidden"));
            license = doc.getRootElement().getAttributeValue("license");
            license = license == null || license.equals("") ? "Unknown" : license;
            if (log.isDebugEnabled())
                log.debug("Application bundle license is " + license);

            productURL = doc.getRootElement().getAttributeValue("productURL");
            instructionsURL = doc.getRootElement().getAttributeValue("instructionsURL");

            // Dependencies if any
            dependencyNames = null;
            String dependencies = doc.getRootElement().getAttributeValue("dependencies");
            if (dependencies != null) {
                log.warn("DEPRECATED. dependencies attribute in bundle " + getFile().getAbsolutePath()
                        + " should now use 'depends'.");
            } else {
                dependencies = doc.getRootElement().getAttributeValue("depends");
            }
            if (!Util.isNullOrTrimmedBlank(dependencies)) {
                dependencyNames = Arrays.asList(dependencies.split(","));
            }

            // Get the required host version
            String requiredHostVersionString = doc.getRootElement().getAttributeValue("requiredHostVersion");
            if (requiredHostVersionString != null && !"any".equalsIgnoreCase(requiredHostVersionString)) {
                requiredHostVersion = new VersionInfo.Version(requiredHostVersionString);
                int dif = requiredHostVersion.compareTo(VersionInfo.getVersion());
                if (dif > 0)
                    throw new ExtensionException(ExtensionException.INSUFFICIENT_SSLEXPLORER_HOST_VERSION,
                            getName(), requiredHostVersionString);

            } else {
                requiredHostVersion = null;
            }

            String ver = doc.getRootElement().getAttributeValue("version");
            if (ver == null || ver.equals("")) {
                throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                        "<applications> element requires the attribute 'version'.");
            }
            version = new VersionInfo.Version(ver);

            if (doc.getRootElement().getName().equals("bundle")) {

                Attribute a = doc.getRootElement().getAttribute("id");
                id = a == null ? null : a.getValue();

                if (id == null) {
                    throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                            "<bundle> element requires attribute 'id'");
                }
                if (log.isDebugEnabled())
                    log.debug("Application bundle id is " + id);

                name = doc.getRootElement().getAttribute("name").getValue();
                if (log.isDebugEnabled())
                    log.debug("Application bundle name is " + name);

                Attribute orderAttr = doc.getRootElement().getAttribute("order");
                if (orderAttr == null) {
                    log.warn("<bundle> element in " + getFile().getPath()
                            + " now requires attribute 'order'. Assuming 99999");
                    order = 99999;
                } else {
                    try {
                        order = orderAttr.getIntValue();
                    } catch (DataConversionException dce) {
                        throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                                "'order' attribute is invalid. " + dce.getMessage());
                    }
                }

                licenseFilePath = doc.getRootElement().getAttributeValue("licenseAgreement");

                if (name == null) {
                    throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                            "<bundle> element requires the attribute 'name'");
                }

                startOnSetupMode = "true".equals(doc.getRootElement().getAttributeValue("startOnSetupMode"));

                for (Iterator i = doc.getRootElement().getChildren().iterator(); i.hasNext();) {
                    Element e = (Element) i.next();
                    if (e.getName().equalsIgnoreCase("description")) {
                        description = Util.trimmedBothOrBlank(e.getText());
                    } else if (e.getName().equalsIgnoreCase("install")) {
                        processInstall(e);
                    } else if (e.getName().equalsIgnoreCase("messages")) {
                        // processed later
                        messageElement = e;
                    } else if (e.getName().equals("application") || e.getName().equals("extension")) {
                        ExtensionDescriptor desc = new ExtensionDescriptor();
                        desc.load(this, e);
                        add(desc);
                    } else {
                        throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                                "<bundle> element may only contain <description> or <extension> (or the deprecated <application>) elements.");
                    }
                }

            } else if (doc.getRootElement().getName().equals("application")
                    || doc.getRootElement().getName().equals("extension")) {
                log.warn("DEPRECATED. All extensions should now use the <bundle> tag, " + getFile().getPath()
                        + " is using not using this tag.");
                ExtensionDescriptor desc = new ExtensionDescriptor();
                desc.load(this, doc.getRootElement());
                id = desc.getId();
                name = desc.getName();
                description = desc.getDescription();
                order = 99999;
                dependencyNames = Arrays.asList(
                        new String[] { "sslexplorer-community-applications", "sslexplorer-community-tunnels" });
                add(desc);
            } else {
                throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                        "Application bundle root element must be <bundle> (or the deprecated <application> or <extension>) elements.");
            }

            // All we know is that the application is
            // installed until the application store is
            // available
            setType(TYPE_INSTALLED);
        } catch (ExtensionException ee) {
            error = ee;
            throw ee;
        }

        error = null;

    }

    /**
     * Get if this bundle contains any plugins.
     * 
     * @return contains plugins
     */
    public boolean isContainsPlugin() {
        boolean containsPlugin = false;
        for (ExtensionDescriptor descriptor : this) {
            if (descriptor.getTypeName().equals(PluginType.TYPE)) {
                containsPlugin = true;
            }
        }
        return containsPlugin;
    }

    /**
     * Remove this bundle. If it started, then it will be stopped first. If the
     * bundle contains plugins the actual removal of the files will be deferred
     * until restart.
     * 
     * @throws Exception
     */
    public void removeBundle() throws Exception {
        if (log.isInfoEnabled())
            log.info("Removing extension bundle " + getId());
        boolean containsPlugin = isContainsPlugin();
        try {
            // Stop the bundle if no plugins are contained
            if (!containsPlugin && getStatus().isStartedOrActivated()) {
                stop();
            }

            // 
            for (ExtensionDescriptor descriptor : this) {
                descriptor.removeDescriptor();
            }
        } finally {
            if (containsPlugin) {
                setType(ExtensionBundle.TYPE_PENDING_REMOVAL);
            }
        }
    }

    /**
     * Get the <i>Install</i> (if any) for this extension bundle.
     * <code>null</code> will be returned if this bundle has no installer.
     * 
     * @return extension installer
     */
    public ExtensionInstaller getInstaller() {
        return installer;
    }

    public String getInstructionsURL() {
        return instructionsURL;
    }

    public String getProductURL() {
        return productURL;
    }

    public VersionInfo.Version getVersion() {
        return version;
    }

    public VersionInfo.Version getDisplayVersion() {
        return isUpdateable() ? updateVersion : version;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public void setLicense(String license) {
        this.license = license;
    }

    public String getLicense() {
        return license;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public File getBaseDir() {
        return getFile() == null ? null : getFile().getParentFile();
    }

    /**
     * @param application
     * @return
     */
    public boolean containsApplication(String application) {
        for (Iterator i = iterator(); i.hasNext();) {
            if (((ExtensionDescriptor) i.next()).getId().equals(application)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param id
     * @return
     */
    public ExtensionDescriptor getApplicationDescriptor(String id) {
        for (Iterator i = iterator(); i.hasNext();) {
            ExtensionDescriptor app = (ExtensionDescriptor) i.next();
            if (app.getId().equals(id)) {
                return app;
            }
        }
        return null;
    }

    public int compareTo(Object arg0) {
        int c = getType() - ((ExtensionBundle) arg0).getType();
        return c != 0 ? c : name.compareTo(((ExtensionBundle) arg0).name);
    }

    public File getLicenseFile() {
        File baseDir = getBaseDir();
        return baseDir == null || licenseFilePath == null ? null : new File(baseDir, licenseFilePath);
    }

    public VersionInfo.Version getRequiredHostVersion() {
        return requiredHostVersion;
    }

    public void setRequiredHostVersion(VersionInfo.Version requiredHostVersion) {
        this.requiredHostVersion = requiredHostVersion;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public boolean isMandatoryUpdate() {
        return mandatoryUpdate;
    }

    public Throwable getError() {
        return error;
    }

    public boolean canStop() {
        boolean canStop = status.isStartedOrActivated();
        if (!canStop) {
            return false;
        }
        for (Iterator i = iterator(); i.hasNext();) {
            ExtensionDescriptor d = (ExtensionDescriptor) i.next();
            if (!d.canStop()) {
                return false;
            }
        }
        return true;

    }

    public boolean canStart() {
        return ExtensionBundleStatus.ENABLED.equals(status)
                && getType() != ExtensionBundle.TYPE_PENDING_INSTALLATION
                && getType() != ExtensionBundle.TYPE_PENDING_REMOVAL
                && getType() != ExtensionBundle.TYPE_PENDING_UPDATE
                && getType() != ExtensionBundle.TYPE_PENDING_STATE_CHANGE;
    }

    public boolean canDisable() {
        return !ExtensionBundleStatus.DISABLED.equals(status) && type != TYPE_CONFIGUREABLE
                && type != TYPE_INSTALLABLE && type != TYPE_PENDING_STATE_CHANGE;
    }

    public boolean canEnable() {
        return ExtensionBundleStatus.DISABLED.equals(status) && type != TYPE_PENDING_STATE_CHANGE;
    }

    public ExtensionBundleStatus getStatus() {
        return status;
    }

    public void setStatus(ExtensionBundleStatus status) {
        this.status = status;
    }

    private void processInstall(Element installElement) throws ExtensionException {
        String when = installElement.getAttributeValue("when");
        when = when == null ? ExtensionInstaller.ON_ACTIVATE : when;
        for (Iterator i = installElement.getChildren().iterator(); i.hasNext();) {
            Element e = (Element) i.next();
            if (e.getName().equalsIgnoreCase("mkdir")) {
                String dir = Util.trimmedBothOrBlank(e.getText());
                if (dir == null || dir.equals("")) {
                    throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                            "<mkdir> contents must be the name of a directory to create.");
                }
                installer.addOp(new ExtensionInstaller.MkdirInstallOp(when, dir));
            } else if (e.getName().equalsIgnoreCase("cp")) {
                String from = Util.trimmedBothOrBlank(e.getText());
                String to = e.getAttributeValue("to");
                String toDir = e.getAttributeValue("toDir");
                boolean overwrite = "true".equalsIgnoreCase(e.getAttributeValue("overwrite"));
                if (from == null || from.equals("")
                        || ((to == null || to.equals("")) && (toDir == null || toDir.equals("")))) {
                    throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                            "<cp> content must be the source path and the tag must have either a to or toDir attribute.");
                }
                installer.addOp(new ExtensionInstaller.CpInstallOp(when, from, to, toDir, overwrite));
            } else if (e.getName().equalsIgnoreCase("rm")) {
                String path = Util.trimmedBothOrBlank(e.getText());
                if (path == null || path.equals("")) {
                    throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                            "<rm> must a path as its content.");
                }
                installer.addOp(new ExtensionInstaller.RmInstallOp(when, path));
            } else if (e.getName().equalsIgnoreCase("custom")) {
                String clazz = Util.trimmedBothOrBlank(e.getText());
                if (clazz == null || clazz.equals("")) {
                    throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR,
                            "<custom> must provide a class name that implements ExtensionInstaller.ExtensionInstallOp as its content.");
                }
                try {
                    installer.addOp(new ExtensionInstaller.CustomInstallOpWrapper(when, clazz));
                } catch (Exception ex) {
                    throw new ExtensionException(ExtensionException.FAILED_TO_PROCESS_DESCRIPTOR, ex,
                            "Failed to create <custom> install op.");

                }
            }
        }
    }

    public String getChanges() {
        return changes == null ? "" : changes.trim();
    }

    public String toString() {
        return id + " " + version;
    }

    public VersionInfo.Version getUpdateVersion() {
        return updateVersion;
    }

    public void setUpdateVersion(VersionInfo.Version updateVersion) {
        this.updateVersion = updateVersion;
    }

    public void setChanges(String changes) {
        this.changes = changes;
    }

    public boolean isStartOnSetupMode() {
        return startOnSetupMode;
    }

    public String getPlatform() {
        return platform;
    }

    public String getArch() {
        return arch;
    }

}