org.polymap.core.operation.OperationSupport.java Source code

Java tutorial

Introduction

Here is the source code for org.polymap.core.operation.OperationSupport.java

Source

/* 
 * polymap.org
 * Copyright (C) 2009-2013, Polymap GmbH. All rights reserved.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 */
package org.polymap.core.operation;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.DefaultOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.ObjectUndoContext;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.jobs.IJobChangeListener;
import org.eclipse.core.runtime.jobs.ISchedulingRule;

import org.polymap.core.CorePlugin;
import org.polymap.core.Messages;
import org.polymap.core.runtime.Polymap;
import org.polymap.core.runtime.SessionSingleton;
import org.polymap.core.runtime.UIJob;
import org.polymap.core.workbench.PolymapWorkbench;

/**
 * The API and implementation of the operations system.
 * <p/>
 * Besides the undo/redo history the <code>OperationSupport</code> also provides
 * the API and SPI to handle Save/Revert/Merge changes to the domain model of
 * the systems.
 * 
 * @author <a href="http://www.polymap.de">Falko Braeutigam</a>
 * @since 3.0
 */
public class OperationSupport extends SessionSingleton {

    private static Log log = LogFactory.getLog(OperationSupport.class);

    /**
     * Scheduling rule that allows just the associated (save) Job to be executed.
     * Used for the the {@link OperationSupport#saveChanges()} job.
     */
    class OneSaver implements ISchedulingRule {

        public boolean isConflicting(ISchedulingRule rule) {
            return rule == this || rule instanceof OneSaver || rule instanceof MultipleOperations;
        }

        public boolean contains(ISchedulingRule rule) {
            return rule == this;
        }
    }

    /**
     * Scheduling rule that allows multiple worker jobs to be executed but conflicts
     * with {@link OneSaver} jobs.
     */
    class MultipleOperations implements ISchedulingRule {
        @Override
        public boolean isConflicting(ISchedulingRule rule) {
            return rule == this || rule instanceof OneSaver;
        }

        @Override
        public boolean contains(ISchedulingRule rule) {
            return rule instanceof MultipleOperations
                    // allow for nested resource opne/update
                    || rule instanceof IResource;
        }
    }

    // instance *******************************************

    private IUndoContext context;

    private DefaultOperationHistory history;

    // private AdvancedValidationUserApprover approver;

    private List<IOperationSaveListener> saveListeners = new LinkedList();

    /**
     *
     * <p>
     * This method must be called from the UI thread.
     */
    public static OperationSupport instance() {
        return instance(OperationSupport.class);
    }

    protected OperationSupport() {
        // context
        context = new ObjectUndoContext(this, "Workbench Context");

        // history
        history = new DefaultOperationHistory();
        history.setLimit(context, 25);
    }

    /**
     * Disposes of anything created by the operation support.
     */
    public void dispose() {
        history.dispose(context, true, true, true);
    }

    public boolean canUndo() {
        return history.canUndo(context);
    }

    public boolean canRedo() {
        return history.canRedo(context);
    }

    /**
     * Get the operation that will next be undone in the given undo context.
     * 
     * @return the operation to be undone or null if there is no operation
     *         available. There is no guarantee that the available operation is
     *         valid for the undo.
     */
    public IUndoableOperation getUndoOperation() {
        return history.getUndoOperation(context);
    }

    /**
     * Get the operation that will next be redone in the given undo context.
     * 
     * @return the operation to be redone or null if there is no operation
     *         available. There is no guarantee that the available operation is
     *         valid for the undo.
     */
    public IUndoableOperation getRedoOperation() {
        return history.getRedoOperation(context);
    }

    public int undoHistorySize() {
        return history.getUndoHistory(context).length;
    }

    public IOperationHistory getOperationHistory() {
        return history;
    }

    public void addOperationHistoryListener(IOperationHistoryListener l) {
        history.addOperationHistoryListener(l);
    }

    public void removeOperationHistoryListener(IOperationHistoryListener l) {
        history.removeOperationHistoryListener(l);
    }

    public void undo() throws ExecutionException {
        IUndoableOperation op = getUndoOperation();
        assert op != null && op.canUndo();

        OperationJob job = new OperationJob(op) {
            protected void run() throws Exception {
                // try to preset task name without beginTask()
                monitor.setTaskName(op.getLabel());
                history.undo(context, monitor, null);
            }
        };
        run(job, true, true);
    }

    public void redo() throws ExecutionException {
        IUndoableOperation op = getRedoOperation();
        assert op != null && op.canRedo();

        OperationJob job = new OperationJob(op) {
            protected void run() throws Exception {
                // try to preset task name without beginTask()
                monitor.setTaskName(op.getLabel());
                history.redo(context, monitor, null);
            }
        };
        run(job, true, true);
    }

    /**
     * Executes the given operation inside a {@link UIJob job}.
     * 
     * @param op
     * @param async Indicates that the calling thread should not block execution and
     *        return imediatelly.
     * @param progress Indicates the the operation is executed inside a progress
     *        dialog. Make sure that the operation does not open dialogs as they might get covered up
     *        by the progress dialog. 
     * @throws ExecutionException
     */
    public void execute(final IUndoableOperation op, boolean async, boolean progress,
            IJobChangeListener... listeners) throws ExecutionException {
        OperationJob job = new OperationJob(op) {
            protected void run() throws Exception {
                // try to preset task name without beginTask()
                monitor.setTaskName(op.getLabel());

                OperationExecutor executor = OperationExecutor.newInstance(op);
                IUndoableOperation executorOp = executor.getOperation();
                executorOp.addContext(context);

                history.execute(executorOp, monitor, executor.getInfo());
            }
        };
        for (IJobChangeListener l : listeners) {
            job.addJobChangeListenerWithContext(l);
        }

        // check nested
        UIJob parent = UIJob.forThread();

        if (parent != null && parent instanceof OperationJob) {
            log.info("Nested operation: " + op);
            run(job, true, progress);

            //            IUndoableOperation parentOp = ((OperationJob)parent).op;
            //            
            //            SubProgressMonitor subMonitor = new SubProgressMonitor( 
            //                    UIJob.monitorForThread(), IProgressMonitor.UNKNOWN, parentOp.getLabel() );
            //            
            //            OperationExecutor executor = OperationExecutor.newInstance( op );
            //            IUndoableOperation executorOp = executor.getOperation();
            //            executorOp.addContext( context );
            //            
            //            history.execute( executorOp, subMonitor, executor.getInfo() );
        }

        // start job
        else {
            run(job, async, progress);
        }
    }

    protected void run(OperationJob job, boolean async, boolean progress) throws ExecutionException {
        if (progress) {
            job.setShowProgressDialog(null, true);
        }

        job.schedule();

        if (!async) {
            log.info("Waiting for operation job to finish...");
            job.joinAndDispatch(3 * 60 * 1000);
        }
    }

    /*
     * 
     */
    abstract class OperationJob extends UIJob {

        protected IUndoableOperation op;

        protected IProgressMonitor monitor;

        public OperationJob(IUndoableOperation op) {
            super(op.getLabel());
            this.op = op;
            setPriority(LONG);
            setRule(new MultipleOperations());
        }

        @SuppressWarnings("hiding")
        protected void runWithException(IProgressMonitor monitor) throws Exception {
            this.monitor = monitor;
            run();
        }

        protected abstract void run() throws Exception;

    }

    // save / revert **************************************

    public void addOperationSaveListener(IOperationSaveListener listener) {
        List<IOperationSaveListener> newList = new LinkedList(saveListeners);
        newList.add(listener);
        saveListeners = newList;
    }

    public void prependOperationSaveListener(IOperationSaveListener listener) {
        List<IOperationSaveListener> newList = new LinkedList(saveListeners);
        newList.add(0, listener);
        saveListeners = newList;
    }

    public void removeOperationSaveListener(IOperationSaveListener listener) {
        List<IOperationSaveListener> newList = new LinkedList(saveListeners);
        newList.remove(listener);
        saveListeners = newList;
    }

    /**
     * Notifies all {@link IOperationSaveListener}s to persistently save
     * changes. If successful the history of operations is disposed.
     * 
     * @throws Exception If the prepare save operation of any of the listeners
     *         failed.
     */
    public void saveChanges() throws Exception {
        UIJob job = new UIJob(Messages.get("OperationSupport_saveChanges")) {
            protected void runWithException(IProgressMonitor monitor) throws Exception {

                monitor.beginTask(getName(), saveListeners.size() * 11);
                try {
                    // prepare
                    for (IOperationSaveListener listener : saveListeners) {
                        SubProgressMonitor subMon = new SubProgressMonitor(monitor, 10, "Preparing");
                        listener.prepareSave(OperationSupport.this, subMon);
                        if (monitor.isCanceled()) {
                            throw new OperationCanceledException("Operation wurde abgebrochen.");
                        }
                        subMon.done();
                    }
                    // commit
                    for (IOperationSaveListener listener : saveListeners) {
                        SubProgressMonitor subMon = new SubProgressMonitor(monitor, 1, "Committing");
                        listener.save(OperationSupport.this, subMon);
                        subMon.done();
                    }
                    history.dispose(context, true, true, false);
                } catch (final Throwable e) {
                    // rollback
                    for (IOperationSaveListener listener : saveListeners) {
                        SubProgressMonitor subMon = new SubProgressMonitor(monitor, 1, "Rolling back");
                        listener.rollback(OperationSupport.this, subMon);
                        subMon.done();
                    }
                    Polymap.getSessionDisplay().asyncExec(new Runnable() {
                        public void run() {
                            PolymapWorkbench.handleError(CorePlugin.PLUGIN_ID, this, e.getLocalizedMessage(), e);
                        }
                    });
                }
            }
        };
        // don't block the UI thread but use rules to prevent other
        // operations to change things during save
        job.setRule(new OneSaver());
        job.setShowProgressDialog(null, true);
        job.schedule();
    }

    /**
     * Notifies all {@link IOperationSaveListener}s to revert changes.
     * Afterwards the history of operations is disposed.
     */
    public void revertChanges() {
        UIJob job = new UIJob(Messages.get("OperationSupport_saveChanges")) {
            protected void runWithException(IProgressMonitor monitor) throws Exception {

                List<Exception> exceptions = new ArrayList();
                monitor.beginTask(getName(), saveListeners.size() * 10);

                for (IOperationSaveListener listener : saveListeners) {
                    try {
                        SubProgressMonitor subMon = new SubProgressMonitor(monitor, 10, "Revert");
                        listener.revert(OperationSupport.this, subMon);
                        subMon.done();
                    } catch (final Exception e) {
                        exceptions.add(e);
                    }
                }
                history.dispose(context, true, true, true);

                if (!exceptions.isEmpty()) {
                    throw exceptions.get(0);
                }
            }
        };
        job.setRule(new OneSaver());
        job.setShowProgressDialog(null, true);
        job.schedule();
    }

    /*
     * 
     */
    class SubProgressMonitor extends org.eclipse.core.runtime.SubProgressMonitor {

        private String taskPrefix;

        public SubProgressMonitor(IProgressMonitor monitor, int ticks, String taskPrefix) {
            super(monitor, ticks, PREPEND_MAIN_LABEL_TO_SUBTASK);
            this.taskPrefix = taskPrefix;
        }

        public void beginTask(String name, int totalWork) {
            super.beginTask(taskPrefix + " : " + name, totalWork);
        }

    }

}