/* * Copyright (c) 2012 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * HUMBOLDT EU Integrated Project #030962 * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.ui.service.align.internal; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import net.jcip.annotations.Immutable; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.commands.operations.AbstractOperation; import org.eclipse.core.commands.operations.IUndoableOperation; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.operations.IWorkbenchOperationSupport; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.align.model.Alignment; import eu.esdihumboldt.hale.common.align.model.Cell; import eu.esdihumboldt.hale.common.align.model.MutableAlignment; import eu.esdihumboldt.hale.common.align.model.MutableCell; import eu.esdihumboldt.hale.common.align.model.Priority; import eu.esdihumboldt.hale.common.align.model.impl.DefaultAlignment; import eu.esdihumboldt.hale.ui.service.align.AlignmentService; import eu.esdihumboldt.hale.ui.service.align.BaseAlignmentLoader; /** * Decorator that adds undo/redo support to an alignment service. * * @author Simon Templer */ public class AlignmentServiceUndoSupport extends AlignmentServiceDecorator { /** * Operation that replaces a cell in the alignment. */ public class ReplaceOperation extends AbstractOperation { private final BiMap<MutableCell, MutableCell> cells; /** * Create an operation that replaces a cell in the alignment. * * @param oldCell the cell to replace * @param newCell the new cell to add */ public ReplaceOperation(MutableCell oldCell, MutableCell newCell) { super("Replace an alignment cell"); cells = HashBiMap.create(1); cells.put(oldCell, newCell); } /** * Create an operation that replaces a cell in the alignment. * * @param cells mapping from replaced cells to new cells */ public ReplaceOperation(BiMap<MutableCell, MutableCell> cells) { super("Replace alignment cells"); this.cells = cells; } /** * @see AbstractOperation#execute(IProgressMonitor, IAdaptable) */ @Override public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.replaceCells(cells); return Status.OK_STATUS; } /** * @see AbstractOperation#redo(IProgressMonitor, IAdaptable) */ @Override public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { return execute(monitor, info); } /** * @see AbstractOperation#undo(IProgressMonitor, IAdaptable) */ @Override public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.replaceCells(cells.inverse()); return Status.OK_STATUS; } } /** * Operation that sets the priority of a cell. */ public class SetCellPropertyOperation extends AbstractOperation { private final Object oldProperty; private final Object newProperty; private final String mutableCellId; private final String propertyName; /** * Create an operation that sets the priority of a cell. * * @param mutableCellId the cell to set the property. * @param propertyName the name of the property to set. * @param oldProperty the old property value. * @param newProperty the new property value. * */ public SetCellPropertyOperation(String mutableCellId, String propertyName, Object oldProperty, Object newProperty) { super("Set a cell property."); this.mutableCellId = mutableCellId; this.propertyName = propertyName; this.oldProperty = oldProperty; this.newProperty = newProperty; } /** * @see AbstractOperation#execute(IProgressMonitor, IAdaptable) */ @Override public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.setCellProperty(mutableCellId, propertyName, newProperty); return Status.OK_STATUS; } /** * @see AbstractOperation#redo(IProgressMonitor, IAdaptable) */ @Override public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { return execute(monitor, info); } /** * @see AbstractOperation#undo(IProgressMonitor, IAdaptable) */ @Override public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.setCellProperty(mutableCellId, propertyName, oldProperty); return Status.OK_STATUS; } } /** * Operation that enables / disables a cell for a specific cell. */ public class DisableCellOperation extends AbstractOperation { private final boolean disable; private final String mutableCellId; private final Cell typeCell; /** * Create an operation that disables a cell. * * @param disable whether to disable or enable the cell * @param mutableCellId the cell to set the priority * @param typeCell the type cell for which to disable/enable the given * cell */ public DisableCellOperation(boolean disable, String mutableCellId, Cell typeCell) { super((disable ? "Disable" : "Enable") + " a cell"); this.disable = disable; this.mutableCellId = mutableCellId; this.typeCell = typeCell; } /** * @see AbstractOperation#execute(IProgressMonitor, IAdaptable) */ @Override public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.setCellProperty(mutableCellId, disable ? Cell.PROPERTY_DISABLE_FOR : Cell.PROPERTY_ENABLE_FOR, typeCell); return Status.OK_STATUS; } /** * @see AbstractOperation#redo(IProgressMonitor, IAdaptable) */ @Override public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { return execute(monitor, info); } /** * @see AbstractOperation#undo(IProgressMonitor, IAdaptable) */ @Override public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.setCellProperty(mutableCellId, disable ? Cell.PROPERTY_ENABLE_FOR : Cell.PROPERTY_DISABLE_FOR, typeCell); return Status.OK_STATUS; } } /** * Operation that cleans the alignment. */ public class CleanOperation extends AbstractOperation { private final MutableAlignment alignment; /** * Create an operation that cleans the alignment. * * @param currentAlignment the current alignment, that is to be restored * on undo */ public CleanOperation(MutableAlignment currentAlignment) { super("Clean the alignment"); this.alignment = currentAlignment; } /** * @see AbstractOperation#execute(IProgressMonitor, IAdaptable) */ @Override public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.clean(); return Status.OK_STATUS; } /** * @see AbstractOperation#redo(IProgressMonitor, IAdaptable) */ @Override public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { return execute(monitor, info); } /** * @see AbstractOperation#undo(IProgressMonitor, IAdaptable) */ @Override public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.addOrUpdateAlignment(alignment); return Status.OK_STATUS; } } /** * Operations that removes a cell from the alignment service. */ public class RemoveCellOperation extends AbstractOperation { private final Collection<MutableCell> cells; /** * Create an operation removing the given cell from the alignment * service. * * @param cells the cells */ public RemoveCellOperation(Collection<MutableCell> cells) { super("Remove alignment cell"); this.cells = cells; } /** * @see AbstractOperation#execute(IProgressMonitor, IAdaptable) */ @Override public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.removeCells(cells.toArray(new Cell[cells.size()])); return Status.OK_STATUS; } /** * @see AbstractOperation#redo(IProgressMonitor, IAdaptable) */ @Override public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { return execute(monitor, info); } /** * @see AbstractOperation#undo(IProgressMonitor, IAdaptable) */ @Override public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { MutableAlignment alignment = new DefaultAlignment(); for (MutableCell cell : cells) { alignment.addCell(cell); } alignmentService.addOrUpdateAlignment(alignment); return Status.OK_STATUS; } } /** * Operation that adds a cell to the alignment service. */ @Immutable public class AddCellOperation extends AbstractOperation { private final MutableCell cell; /** * Create an operation adding the given cell to the alignment service. * * @param cell the cell */ public AddCellOperation(MutableCell cell) { super("Add alignment cell"); this.cell = cell; } /** * @see AbstractOperation#execute(IProgressMonitor, IAdaptable) */ @Override public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.addCell(cell); return Status.OK_STATUS; } /** * @see AbstractOperation#redo(IProgressMonitor, IAdaptable) */ @Override public IStatus redo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { return execute(monitor, info); } /** * @see AbstractOperation#undo(IProgressMonitor, IAdaptable) */ @Override public IStatus undo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { alignmentService.removeCells(cell); return Status.OK_STATUS; } } private static final ALogger log = ALoggerFactory.getLogger(AlignmentServiceUndoSupport.class); /** * Create undo/redo support for the given alignment service. * * @param alignmentService the alignment service */ public AlignmentServiceUndoSupport(AlignmentService alignmentService) { super(alignmentService); } /** * Execute an operation. * * @param operation the operation to execute */ protected void executeOperation(IUndoableOperation operation) { IWorkbenchOperationSupport operationSupport = PlatformUI.getWorkbench() .getOperationSupport(); // service is workbench wide, so the operation should also be workbench // wide operation.addContext(operationSupport.getUndoContext()); // operation.addContext(new ObjectUndoContext(alignmentService, "Alignment service")); try { // OperationHistoryFactory.getOperationHistory().execute(operation, null, null); operationSupport.getOperationHistory().execute(operation, null, null); } catch (ExecutionException e) { log.error("Error executing operation on alignment service", e); } } /** * @see AlignmentServiceDecorator#addOrUpdateAlignment(MutableAlignment) */ @Override public synchronized void addOrUpdateAlignment(MutableAlignment alignment) { // TODO implement undo support? super.addOrUpdateAlignment(alignment); } /** * @see AlignmentServiceDecorator#addCell(MutableCell) */ @Override public synchronized void addCell(MutableCell cell) { if (cell == null) { return; } IUndoableOperation operation = new AddCellOperation(cell); executeOperation(operation); } /** * @see AlignmentServiceDecorator#removeCells(Cell[]) */ @Override public synchronized void removeCells(Cell... cells) { if (cells == null || cells.length == 0) { return; } List<MutableCell> contained = new ArrayList<MutableCell>(); for (Cell cell : cells) { if (cell instanceof MutableCell && getAlignment().getCells().contains(cell)) { contained.add((MutableCell) cell); } } if (!contained.isEmpty()) { /* * Cells must be contained in the current alignment, else the redo * would do something unexpected (readding a cell that was not * previously there). * * Also, as long as there is no copy constructor in DefaultCell, * undo only for removing MutableCells supported. */ IUndoableOperation operation = new RemoveCellOperation(contained); executeOperation(operation); } else { super.removeCells(cells); } } /** * @see AlignmentServiceDecorator#clean() */ @Override public synchronized void clean() { // XXX problem: what about cleans that should not be undone? e.g. when // the schemas have changed // XXX -> currently on project clean the workbench history is reset Alignment alignment = getAlignment(); if (alignment.getCells().isEmpty()) { return; } if (alignment instanceof MutableAlignment) { /* * As long as there is no copy constructor in DefaultAlignment, undo * only supported if the current alignment is a MutableAlignment. */ IUndoableOperation operation = new CleanOperation((MutableAlignment) alignment); executeOperation(operation); } else { super.clean(); } } /** * @see AlignmentServiceDecorator#replaceCell(Cell, MutableCell) */ @Override public synchronized void replaceCell(Cell oldCell, MutableCell newCell) { if (oldCell != null && newCell != null) { if (oldCell != newCell) { boolean contains = getAlignment().getCells().contains(oldCell); if (!contains) { /* * Cell must be contained in the current alignment, else the * redo would do something unexpected (reading a cell that * was not previously there). */ addCell(newCell); } else { if (oldCell instanceof MutableCell) { /* * As long as there is no copy constructor in * DefaultCell, undo only supported for MutableCells to * be replaced. */ IUndoableOperation operation = new ReplaceOperation((MutableCell) oldCell, newCell); executeOperation(operation); } else { super.replaceCell(oldCell, newCell); } } } } else if (newCell != null) { addCell(newCell); } else if (oldCell != null) { removeCells(oldCell); } } /** * @see eu.esdihumboldt.hale.ui.service.align.AlignmentService#replaceCells(java.util.Map) */ @Override public void replaceCells(Map<? extends Cell, MutableCell> cells) { BiMap<MutableCell, MutableCell> map = HashBiMap.create(cells.size()); for (Entry<? extends Cell, MutableCell> e : cells.entrySet()) { if (e.getKey() instanceof MutableCell && e.getValue() != null && getAlignment().getCells().contains(e.getKey())) map.put((MutableCell) e.getKey(), e.getValue()); else { log.warn("Replaced cells contains at least one cell which " + "is either not mutable or not in the alignment. " + "No undo/redo possible."); super.replaceCells(cells); return; } } IUndoableOperation operation = new ReplaceOperation(map); executeOperation(operation); } /** * @see eu.esdihumboldt.hale.ui.service.align.AlignmentService#setCellProperty(java.lang.String, * java.lang.String, java.lang.Object) */ @Override public void setCellProperty(String cellId, String propertyName, Object property) { if (Cell.PROPERTY_DISABLE_FOR.equals(propertyName) || Cell.PROPERTY_ENABLE_FOR.equals(propertyName)) { IUndoableOperation operation = new DisableCellOperation( Cell.PROPERTY_DISABLE_FOR.equals(propertyName), cellId, (Cell) property); executeOperation(operation); } else if (Cell.PROPERTY_PRIORITY.equals(propertyName)) { if (property instanceof Priority) { Priority newPriority = (Priority) property; Cell cell = getAlignment().getCell(cellId); Priority oldPriority = cell.getPriority(); IUndoableOperation operation = new SetCellPropertyOperation(cellId, propertyName, oldPriority, newPriority); executeOperation(operation); } } else if (Cell.PROPERTY_TRANSFORMATION_MODE.equals(propertyName)) { Cell cell = getAlignment().getCell(cellId); Object oldValue = cell.getTransformationMode(); IUndoableOperation operation = new SetCellPropertyOperation(cellId, propertyName, oldValue, property); executeOperation(operation); } else { log.warn("An unknown cell property is set. No undo support."); alignmentService.setCellProperty(cellId, propertyName, property); } } /** * @see eu.esdihumboldt.hale.ui.service.align.internal.AlignmentServiceDecorator#addBaseAlignment(eu.esdihumboldt.hale.ui.service.align.BaseAlignmentLoader) */ @Override public boolean addBaseAlignment(BaseAlignmentLoader loader) { // TODO implement undo support? return alignmentService.addBaseAlignment(loader); } }