no.javatime.inplace.bundlemanager.BundleEventManager.java Source code

Java tutorial

Introduction

Here is the source code for no.javatime.inplace.bundlemanager.BundleEventManager.java

Source

/*******************************************************************************
 * Copyright (c) 2011, 2012 JavaTime project and others
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    JavaTime project, Eirik Gronsund - initial implementation
 *******************************************************************************/
package no.javatime.inplace.bundlemanager;

import java.util.Collection;
import java.util.Collections;

import no.javatime.inplace.InPlace;
import no.javatime.inplace.bundlejobs.ActivateBundleJob;
import no.javatime.inplace.bundlejobs.BundleJobListener;
import no.javatime.inplace.bundlejobs.DeactivateJob;
import no.javatime.inplace.bundlejobs.InstallJob;
import no.javatime.inplace.bundlejobs.UninstallJob;
import no.javatime.inplace.bundlejobs.UpdateScheduler;
import no.javatime.inplace.bundlemanager.BundleTransition.Transition;
import no.javatime.inplace.bundlemanager.BundleTransition.TransitionError;
import no.javatime.inplace.bundlemanager.state.ActiveState;
import no.javatime.inplace.bundlemanager.state.BundleNode;
import no.javatime.inplace.bundlemanager.state.BundleState;
import no.javatime.inplace.bundlemanager.state.BundleStateFactory;
import no.javatime.inplace.bundlemanager.state.InstalledState;
import no.javatime.inplace.bundlemanager.state.LazyState;
import no.javatime.inplace.bundlemanager.state.ResolvedState;
import no.javatime.inplace.bundlemanager.state.UninstalledState;
import no.javatime.inplace.bundleproject.ManifestUtil;
import no.javatime.inplace.bundleproject.OpenProjectHandler;
import no.javatime.inplace.bundleproject.ProjectProperties;
import no.javatime.inplace.dependencies.ProjectSorter;
import no.javatime.inplace.statushandler.BundleStatus;
import no.javatime.inplace.statushandler.IBundleStatus.StatusCode;
import no.javatime.util.messages.Category;
import no.javatime.util.messages.Message;
import no.javatime.util.messages.TraceMessage;
import no.javatime.util.messages.UserMessage;
import no.javatime.util.messages.WarnMessage;

import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.CommandEvent;
import org.eclipse.core.commands.ICommandListener;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.statushandlers.StatusManager;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.SynchronousBundleListener;

/**
 * Registers a bundle job listener and acts on events received from the OSGI framework, bundles and the log
 * service. Framework and bundle events are initiated by internal bundle operations in
 * {@link BundleCommandImpl} or from an external source. Internal bundle operations usually has its origin
 * from a bundle job.
 * <p>
 * Events not generated by BundleManager operations from {@link BundleCommandImpl} are marked as external
 * transitions and flagged as such if the user information option is switched on. BundleManager operations
 * spanning multiple transitions are adjusted according to an internal maintained state for each bundle.
 * <p>
 * If a bundle is uninstalled from an external source in an activated workspace the workspace is automatically
 * deactivated if automatic handling of external commands is switched on. If off, the user has the option to
 * deactivate the workspace or install, resolve and possibly start the uninstalled bundle again.
 * <p>
 * Installed and uninstalled bundles are registered and unregistered as workspace bundles respectively.
 * <p>
 * The design supports a concept of a region bounded bundle structure ({@link BundleWorkspaceImpl}) acted on
 * by bundle operations ({@link BundleCommandImpl}), which in turn creates a result (events) to interpret and
 * react upon ({@code BundleEventManager}). This interrelationship is not interpreted as a sequence or a flow,
 * although present, but as a structural coherence.
 * <p>
 */
class BundleEventManager implements FrameworkListener, SynchronousBundleListener, ICommandListener {

    private BundleJobListener jobListener = new BundleJobListener();
    private BundleWorkspaceImpl bundleRegion = BundleWorkspaceImpl.INSTANCE;
    private Command autoBuildCommand;
    BundleCommandImpl bundleCommand = BundleCommandImpl.INSTANCE;
    BundleTransitionImpl bundleTransition = BundleTransitionImpl.INSTANCE;

    /**
     * Default empty constructor.
     */
    public BundleEventManager() {
    }

    /**
     * Registers the framework, bundle and bundle job listeners. Other listeners are registered in the
     * {@linkplain no.javatime.inplace.InPlace activator}.
     */
    public void init() {

        bundleCommand.getContext().addFrameworkListener(this);
        bundleCommand.getContext().addBundleListener(this);
        Job.getJobManager().addJobChangeListener(jobListener);
        IWorkbench workbench = PlatformUI.getWorkbench();
        if (null != workbench) {
            ICommandService service = (ICommandService) workbench.getService(ICommandService.class);
            autoBuildCommand = service.getCommand("org.eclipse.ui.project.buildAutomatically");
            if (autoBuildCommand.isDefined()) {
                autoBuildCommand.addCommandListener(this);
            }
        }
    }

    /**
     * Removes the framework, bundle and bundle job listeners.
     */
    public void dispose() {

        bundleCommand.getContext().removeFrameworkListener(this);
        bundleCommand.getContext().removeBundleListener(this);
        Job.getJobManager().removeJobChangeListener(jobListener);
        if (autoBuildCommand.isDefined()) {
            autoBuildCommand.removeCommandListener(this);
        }
    }

    /**
     * Callback for the "Build Automatically" main menu option.
     * Auto build is set to true when "Build Automatically" is switched on.
     * <p>
     * When auto build is switched on the post builder is not invoked,
     * so an update job is scheduled here to update projects being built when
     * the auto build option is switched on.
     */
    @Override
    public void commandChanged(CommandEvent commandEvent) {
        InPlace activator = InPlace.getDefault();
        if (null == activator) {
            return;
        }
        IWorkbench workbench = activator.getWorkbench();
        if (!ProjectProperties.isProjectWorkspaceActivated() || (null != workbench && workbench.isClosing())) {
            return;
        }
        Command autoBuildCmd = commandEvent.getCommand();
        try {
            if (autoBuildCmd.isDefined() && !ProjectProperties.isAutoBuilding()) {
                if (InPlace.getDefault().getOptionsService().isUpdateOnBuild()) {
                    BundleManager.getRegion().setAutoBuild(true);
                    Collection<IProject> activatedProjects = ProjectProperties.getActivatedProjects();
                    Collection<IProject> pendingProjects = bundleTransition.getPendingProjects(activatedProjects,
                            Transition.BUILD);
                    Collection<IProject> pendingProjectsToUpdate = bundleTransition
                            .getPendingProjects(activatedProjects, Transition.UPDATE);
                    if (pendingProjectsToUpdate.size() > 0) {
                        pendingProjects.addAll(pendingProjectsToUpdate);
                    }
                    if (pendingProjects.size() > 0) {
                        UpdateScheduler.scheduleUpdateJob(pendingProjects, 1000);
                    }
                }
            } else {
                BundleManager.getRegion().setAutoBuild(false);
            }
        } catch (InPlaceException e) {
            StatusManager.getManager().handle(
                    new BundleStatus(StatusCode.EXCEPTION, InPlace.PLUGIN_ID, e.getMessage(), e),
                    StatusManager.LOG);
        }
    }

    /**
     * Trace events and report on framework errors.
     */
    @Override
    public void frameworkEvent(FrameworkEvent event) {

        if (Category.getState(Category.bundleEvents)) {
            TraceMessage.getInstance().getString("framework_event", BundleCommandImpl.INSTANCE.getStateName(event),
                    event.getBundle().getSymbolicName());
        }
        if ((event.getType() & (FrameworkEvent.ERROR)) != 0) {
            StatusManager.getManager().handle(new BundleStatus(StatusCode.EXCEPTION, InPlace.PLUGIN_ID,
                    event.getBundle().getBundleId(), null, event.getThrowable()), StatusManager.LOG);
        }
    }

    /**
     * Identify and adjust bundle state for external bundle operations, bundle operations spanning multiple
     * transitions and "on demand" class loading of lazy activated bundles.The current state of a bundle, which
     * changes after an internal (or external) workspace bundle operation is executed, is adjusted and recorded
     * along with the bundle in a bundle node. If received bundle events from the framework deviates from the
     * current recorded state of the bundle the transition (operation) who initiated the events is treated as an
     * external operation and actions are taken to update the bundle node with the new state caused by the
     * external transition.
     * <p>
     * An exception is when a bundle with lazy activation policy is started due to "on demand" class loading.
     * This is a transition generated by the framework, and the maintained state of the bundle is adjusted after
     * the fact from a lazy activation state to an active state.
     * <p>
     * It is allowed to execute external commands both in an activated and a deactivated workspace except for
     * uninstalling workspace bundles or resolving or starting deactivated bundles in an activated workspace.
     * Uninstalling workspace bundles in an activated workspace from an external source is acted upon and bundle
     * operations are executed - either automatically or based on user choice - to maintain the workspace
     * consistent with the definition of an activated workspace. In the second case, resolving or starting a
     * deactivated bundle from an external source in an activated workspace, is rejected by the resolver hook.
     */
    @Override
    public void bundleChanged(BundleEvent event) {

        final Bundle bundle = event.getBundle();
        // Only consider workspace bundles
        final IProject project = bundleRegion.getProject(bundle);
        if (null == project) {
            return; // jar bundle
        }

        // Get the recorded internal state of this bundle
        BundleState state = bundleRegion.getActiveState(bundle);
        final int eventType = event.getType();
        final String bundleLocation = bundle.getLocation();
        Transition transition = null;
        try {
            transition = bundleTransition.getTransition(bundleRegion.getProject(bundle));
        } catch (ProjectLocationException e) {
            return; // Avoid spam. Delegate the exception reporting to others
        }

        // If bundle has no recorded state and this is an internal install operation this method is called
        // synchronously by install and the bundle will be registered as an installed workspace bundle.
        if (null == state && Transition.INSTALL == transition) {
            bundleCommand.registerBundleNode(project, bundle, ProjectProperties.isProjectActivated(project));

            // Get the new current state (installed) of the registered bundle
            state = bundleRegion.getActiveState(bundle);
        }
        BundleNode bundleNode = bundleRegion.getBundleNode(bundle);
        // Examine all bundle events sent by the framework and update bundle state when external bundle
        // operations are executed, when lazy activated bundles are loaded on demand by the framework and
        // BundleManager commands (transitions) spans multiple states
        switch (eventType) {
        // @formatter:off 
        /*
         * Incoming transitions with Installed as the current state: 
         * Previous state: Uninstalled. Possible transitions: Install 
         * Previous state: Installed. Possible transitions: Update, Refresh
         * Comments: 
         * Updating an installed bundle is relevant when activating a bundle in an activated workspace.
         * Refresh on an installed bundle is not put to use
         */
        case BundleEvent.INSTALLED: {

            // Transition: Install. Source: External
            if (null == state || !(state instanceof InstalledState)) {
                // Register the external installed workspace bundle
                bundleNode = bundleCommand.registerBundleNode(project, bundle,
                        ProjectProperties.isProjectActivated(project));
                bundleTransition.setTransition(bundle, Transition.EXTERNAL);
                // External transition message
                if (Category.getState(Category.infoMessages)) {
                    final String originName = bundleRegion.getSymbolicKey(event.getOrigin(), null);
                    final String symbolicName = bundleRegion.getSymbolicKey(bundle, null);
                    final String stateName = bundleCommand.getStateName(event);
                    UserMessage.getInstance().getString("external_bundle_operation_origin", symbolicName, stateName,
                            originName, bundleLocation);
                }
            }
            break;
        }

        /*
         * Incoming transitions with Installed as the current state: 
         * Previous state: Resolved. Possible transitions: Refresh, Update, Uninstall 
         * Comments: 
         * Refresh unresolves and resolves a bundle. If the bundle is already deactivated it is rejected by the 
         * resolver hook and not resolved again. 
         * A bundle to update is unresolved before the installed bundle is updated.
         */
        case BundleEvent.UNRESOLVED: {
            if ((state instanceof InstalledState)) {
                // The bundle was initially uninstalled from state resolved. (Multiple states:
                // Resolved-Installed-Uninstalled)
                if (Transition.UNINSTALL == transition) {
                    state.uninstall(bundleNode);
                    // The bundle was initially refreshed from state resolved. (Multiple states:
                    // Resolved-Installed-Resolved)
                } else if (Transition.REFRESH == transition) {
                    state.refresh(bundleNode);
                }
            } else {
                bundleNode.setCurrentState(BundleStateFactory.INSTANCE.installedState);
                undefinedTransitions(event);
            }
            break;
        }
        /*
         * Incoming transitions with Installed as the current state: 
         * Previous state: Installed. Possible transitions: Update 
         * Comments: 
         * Update does not alter the state of the installed bundle.
         */
        case BundleEvent.UPDATED: {
            if (!(state instanceof InstalledState)) {
                bundleNode.setCurrentState(BundleStateFactory.INSTANCE.installedState);
                undefinedTransitions(event);
            }
            break;
        }
        /*
         * Incoming transitions with Uninstalled as the current state: 
         * Previous state: Installed. Possible transitions: Uninstall
         */
        case BundleEvent.UNINSTALLED: {
            if (!(state instanceof UninstalledState)) {
                if (null != bundleNode) {
                    bundleNode.setCurrentState(BundleStateFactory.INSTANCE.uninstalledState);
                }
                undefinedTransitions(event);
                // Uninstalling a bundle from an external source is not permitted in an activated workspace
                if (ProjectProperties.isProjectWorkspaceActivated()) {
                    externalUninstall(bundle, project);
                } else {
                    // Remove the externally uninstalled bundle from the workspace region
                    bundleCommand.unregisterBundle(bundle);
                }
            }
            break;
        }

        /*
         * Enters state <<LAZY>>
         */
        case BundleEvent.LAZY_ACTIVATION: {
            if (!(state instanceof LazyState)) {
                bundleNode.setCurrentState(BundleStateFactory.INSTANCE.lazyState);
            }
            break;
        }

        /*
         * Framework publish this event before calling BundleActivator.start
         * <p>
         * Activates a bundle with eager activation policy. Lazy bundles are activated
         * by the framework due to "on demand" class loading.
         */
        case BundleEvent.STARTING: {
            if (state instanceof LazyState) {
                bundleNode.setCurrentState(BundleStateFactory.INSTANCE.activeState);
                // Generate a transition on behalf of the framework and if the previous transition was
                // from an external source it follows that this one is external too
                bundleTransition.setTransition(bundle, Transition.LAZY_LOAD);
                if (Category.getState(Category.infoMessages)) {
                    UserMessage.getInstance().getString("on_demand_loading_bundle", bundle,
                            bundleCommand.getStateName(event));
                }
            } else if (!(state instanceof ActiveState)) {
                bundleNode.setCurrentState(BundleStateFactory.INSTANCE.activeState);
                undefinedTransitions(event);
            }
            break;
        }
        /*
         * Framework publish this event after calling BundleActivator.start
         * <p>
         * Incoming transitions with Active as the current state: 
         * Previous state: Starting. Possible transitions: Start
         */
        case BundleEvent.STARTED: {
            if (!(state instanceof ActiveState)) {
                bundleNode.setCurrentState(BundleStateFactory.INSTANCE.activeState);
                undefinedTransitions(event);
            }
            break;
        }
        /*
         * Framework publish this event before calling BundleActivator.stop
         * <p>
         * Incoming transitions with Stopping as the current state: 
         * Previous state: Active. Possible transitions: Stop
         */
        case BundleEvent.STOPPING:

            /*
             * Framework publish this event after calling BundleActivator.stop
             * <p>
             * This event is non deterministic in relation to external commands.
             * If the BundleActivator.start method throws an exception the framework publish a
             * STOPPED event but the internal transition was START (not STOP). This could also
             * be an external command stopping the bundle or starting the bundle throwing an exception
             * <p>
             * The solution is to assume an external command until the exception
             * is caught by the method that called the framework start method. If the transition 
             * was start the catch clause will set the transition to STOP, repealing the external
             * stop or external start command throwing an exception.  
             *  
             * Incoming transitions with Resolved as the current state: 
             * Previous state: Stopping. Possible transitions: Stop, Start
             */
        case BundleEvent.STOPPED: {
            if (transition == Transition.START) {
                // If not an external command, an exception was thrown in the start method
            }
            if (!(state instanceof ResolvedState)) {
                // Assume an external command
                bundleNode.setCurrentState(BundleStateFactory.INSTANCE.resolvedState);
                undefinedTransitions(event);
            }
            break;
        }
        /*
         * Incoming transitions with Resolved as the current state: 
         * Previous state: Resolved. Possible transitions: Refresh 
         * Previous state: Installed. Possible transitions: Resolve 
         * Comments: 
         * Resolving (and refreshing) bundles with lazy activation policy are moved to state 
         * Starting by the framework
         */
        case BundleEvent.RESOLVED: {
            if (state instanceof ResolvedState) {
                if (Transition.RESOLVE == transition) {
                    if (ManifestUtil.getlazyActivationPolicy(bundle)) {
                        bundleNode.setCurrentState(BundleStateFactory.INSTANCE.lazyState);
                    }
                } else if (Transition.REFRESH == transition) {
                    if (ManifestUtil.getlazyActivationPolicy(bundle)) {
                        bundleNode.setCurrentState(BundleStateFactory.INSTANCE.lazyState);
                    }
                }
            } else {
                if (ManifestUtil.getlazyActivationPolicy(bundle)) {
                    bundleNode.setCurrentState(BundleStateFactory.INSTANCE.lazyState);
                } else {
                    bundleNode.setCurrentState(BundleStateFactory.INSTANCE.resolvedState);
                }
                undefinedTransitions(event);
            }
            break;
        }
        default: {
            undefinedTransitions(event);
        }
        } // switch
          // @formatter:on
          // Event trace
        if (Category.getState(Category.bundleEvents)) {
            try {
                TraceMessage.getInstance().getString("bundle_event", bundle, bundleCommand.getStateName(event),
                        bundleCommand.getStateName(bundle), bundleTransition.getTransitionName(project));
            } catch (ProjectLocationException e) {
            }
        }
    }

    /**
     * Register external and incomplete transitions and informs that they have been executed.
     * 
     * @param event bundle event after a bundle operation has been executed
     */
    private void undefinedTransitions(BundleEvent event) {
        Bundle bundle = event.getBundle();
        final String location = bundle.getLocation();
        BundleCommandImpl bundleCommand = BundleCommandImpl.INSTANCE;
        BundleTransitionImpl bundleTransition = BundleTransitionImpl.INSTANCE;
        final String symbolicName = bundleRegion.getSymbolicKey(bundle, null);
        final String stateName = bundleCommand.getStateName(event);
        if (bundleTransition.hasTransitionError(bundle)) {
            if (bundleTransition.getError(bundle) == TransitionError.INCOMPLETE) {
                if (Category.getState(Category.infoMessages)) {
                    UserMessage.getInstance().getString("incomplete_bundle_operation", symbolicName, stateName,
                            location);
                }
            }
        } else {
            bundleTransition.setTransition(bundle, Transition.EXTERNAL);
            if (Category.getState(Category.infoMessages)) {
                UserMessage.getInstance().getString("external_bundle_operation", symbolicName, stateName, location);
            }
        }
    }

    /**
     * Bundle has been uninstalled from an external source in an activated workspace. Either restore (activate
     * or install) the bundle or deactivate the workspace depending on default actions or user option/choice.
     * 
     * @param project the project to restore or deactivate
     * @param bundle the bundle to restore or deactivate
     */
    private void externalUninstall(final Bundle bundle, final IProject project) {

        final String symbolicName = bundle.getSymbolicName();
        final String location = bundle.getLocation();
        // After the fact
        InPlace.getDisplay().asyncExec(new Runnable() {
            @Override
            public void run() {
                BundleCommandImpl bundleManager = BundleCommandImpl.INSTANCE;
                int autoDependencyAction = 1; // Default auto dependency action
                new OpenProjectHandler().saveModifiedFiles();
                Boolean dependencies = false;
                Collection<IProject> reqProjects = Collections.emptySet();
                if (bundleRegion.isActivated(bundle)) {
                    ProjectSorter bs = new ProjectSorter();
                    reqProjects = bs.sortRequiringProjects(Collections.singletonList(project), Boolean.TRUE);
                    // Remove initial project from result set
                    reqProjects.remove(project);
                    dependencies = reqProjects.size() > 0;
                    if (dependencies) {
                        String msg = WarnMessage.getInstance().formatString("has_requiring_bundles",
                                bundleRegion.formatBundleList(bundleRegion.getBundles(reqProjects), true),
                                bundleRegion.getSymbolicKey(bundle, null));
                        StatusManager.getManager().handle(
                                new BundleStatus(StatusCode.WARNING, InPlace.PLUGIN_ID, msg), StatusManager.LOG);
                    }
                }
                // User choice to deactivate workspace or restore uninstalled bundle
                try {
                    if (!InPlace.getDefault().getOptionsService().isAutoHandleExternalCommands()) {
                        String question = null;
                        int index = 0;
                        if (dependencies) {
                            question = Message.getInstance().formatString("deactivate_question_requirements",
                                    symbolicName, location,
                                    bundleRegion.formatBundleList(bundleRegion.getBundles(reqProjects), true));
                            index = 1;
                        } else {
                            question = Message.getInstance().formatString("deactivate_question", symbolicName,
                                    location);
                        }
                        MessageDialog dialog = new MessageDialog(null, "InPlace Activator", null, question,
                                MessageDialog.QUESTION, new String[] { "Yes", "No" }, index);
                        autoDependencyAction = dialog.open();
                    }
                } catch (InPlaceException e) {
                    StatusManager.getManager().handle(
                            new BundleStatus(StatusCode.EXCEPTION, InPlace.PLUGIN_ID, e.getMessage(), e),
                            StatusManager.LOG);
                }
                bundleManager.unregisterBundle(bundle);
                if (autoDependencyAction == 0) {
                    if (ProjectProperties.isProjectActivated(project)) {
                        // Restore activated bundle and dependent bundles
                        ActivateBundleJob activateBundleJob = new ActivateBundleJob(
                                ActivateBundleJob.activateJobName, project);
                        if (dependencies) {
                            // Bring workspace back to a consistent state before restoring
                            UninstallJob uninstallJob = new UninstallJob(UninstallJob.uninstallJobName,
                                    reqProjects);
                            BundleManager.addBundleJob(uninstallJob, 0);
                            activateBundleJob.addPendingProjects(reqProjects);
                        }
                        BundleManager.addBundleJob(activateBundleJob, 0);
                    } else {
                        // Workspace is activated but bundle is not. Install the bundle
                        InstallJob installJob = new InstallJob(InstallJob.installJobName, project);
                        BundleManager.addBundleJob(installJob, 0);
                    }
                } else if (autoDependencyAction == 1) {
                    // Deactivate workspace to obtain a consistent state between all workspace bundles
                    DeactivateJob deactivateJob = new DeactivateJob(DeactivateJob.deactivateWorkspaceJobName);
                    deactivateJob.addPendingProjects(ProjectProperties.getActivatedProjects());
                    BundleManager.addBundleJob(deactivateJob, 0);
                }
            }
        });
    }
}