/******************************************************************************* * <copyright> * * Copyright (c) 2005, 2012 SAP AG. * 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: * SAP AG - initial API, implementation and documentation * mwenz - Felix Velasco - Bug 374918 - Let default paste use LocalSelectionTransfer * mwenz - Felix Velasco - Bug 361414 - Copy/paste : clipboard contents confuses the workbench * * </copyright> * *******************************************************************************/ package org.eclipse.graphiti.ui.internal.util.clipboard; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Platform; import org.eclipse.emf.common.command.CommandStack; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EClassifier; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecore.util.EcoreUtil.Copier; import org.eclipse.emf.transaction.RecordingCommand; import org.eclipse.emf.transaction.TransactionalEditingDomain; import org.eclipse.emf.transaction.util.TransactionUtil; import org.eclipse.graphiti.ui.internal.Messages; import org.eclipse.graphiti.ui.internal.T; import org.eclipse.graphiti.ui.internal.services.GraphitiUiInternal; import org.eclipse.graphiti.ui.internal.util.ReflectionUtil; import org.eclipse.jface.util.LocalSelectionTransfer; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.custom.BusyIndicator; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.FileTransfer; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.widgets.Display; /** * Provides a clipboard-like storage of EMF-related data based on SWT * {@link Clipboard}. * * @noinstantiate This class is not intended to be instantiated by clients. * @noextend This class is not intended to be subclassed by clients. */ public final class ModelClipboard { private static final String EMPTY_TOSTRING_PLACEHOLDER = "<empty>"; //$NON-NLS-1$ private static final EObject[] NO_E_OBJECTS = new EObject[0]; private static final ModelClipboard INSTANCE = new ModelClipboard(); /** * Creates a {@link ModelClipboard}. */ private ModelClipboard() { } /** * @return the default {@link ModelClipboard} instance to represent a global * {@link ModelClipboard} to the user, which is connected to the SWT * {@link Clipboard}. */ public static ModelClipboard getDefault() { return INSTANCE; } /** * Sets the content of the {@link Clipboard} and deletes all previous data. * Must be called in the UI thread. * * @param objects * the {@link EObject} <code>objects</code> to store * @throws IllegalStateException * if not called from UI thread * @throws IllegalArgumentException * if <code>objects</code> parameter is null */ public synchronized void setContent(EObject[] objects) throws IllegalStateException { if (objects == null) { throw new IllegalArgumentException("EObject[] objects must not be null"); //$NON-NLS-1$ } if (objects.length == 0) { return; } if (canUseNative()) { // must run in UI thread setNativeContentObjects(Arrays.asList(objects)); } } /** * Returns the SWT {@link Clipboard} content in form of {@link EObject}s. * * @param resourceSet * the ResourceSet to resolve the stored URI information * @return the content as live objects * @throws IllegalStateException * if not called from UI thread * @throws IllegalArgumentException * if <code>resourceSet</code> parameter is null */ public synchronized EObject[] getContentAsEObjects(ResourceSet resourceSet) throws IllegalStateException { if (resourceSet == null) { throw new IllegalArgumentException("ResourceSet resourceSet must not be null"); //$NON-NLS-1$ } final List<EObject> eObjectList; if (canUseNative()) { eObjectList = getLocalSelectionContent(); } else { eObjectList = Collections.emptyList(); } if (eObjectList.isEmpty()) { return NO_E_OBJECTS; } return eObjectList.toArray(new EObject[eObjectList.size()]); } private List<EObject> getLocalSelectionContent() { final Clipboard cb = new Clipboard(Display.getCurrent()); try { final ISelection contents = (ISelection) cb.getContents(LocalSelectionTransfer.getTransfer()); if (contents instanceof IStructuredSelection && !contents.isEmpty()) { List<?> list = ((IStructuredSelection) contents).toList(); for (Object o : list) { if (!(o instanceof EObject)) return Collections.emptyList(); } @SuppressWarnings("unchecked") List<EObject> localList = (List<EObject>) list; return localList; } return Collections.emptyList(); } finally { cb.dispose(); } } /** * Answers whether at least one of the given objects can be aggregated below * the given parent as composite children. This generic implementation * considers type compatibility and cardinalities but no additional domain * specific constraints. * * @param parent * the composite parent * @param objects * the objects to check * @return <code>true</code> if at least one object may be a composite child * of <code>parent</code> */ public boolean isCompositionAllowed(EObject parent, EObject[] objects) { for (final EObject object : objects) { // optimistic approach: if one association can aggregate one // element, we are OK final List<EReference> assocs = findUsableTargetAssociations(parent, object); if (assocs.size() > 0) { return true; } } return false; } /** * Duplicates the clipboard's content using EMF's deep copy service. Note * that only elements from the content that are {@link EObject}s are * considered, pure {@link EObject}s like packages cannot be duplicated. * * @param target * an object acting as composite parent for the copies. * <code>null</code> if the copied elements should be top-level * elements. * @param transactionalEditingDomain * the TransactionalEditingDomain to write the copies into. Must * not be <code>null</code> nor dead. * @return the copy result or <code>null</code> in case of an empty * clipboard * @throws IllegalStateException * if not called from UI thread * @throws IllegalArgumentException * if <code>transactionalEditingDomain</code> parameter is null * @throws IllegalArgumentException * if <code>transactionalEditingDomain</code> parameter is not * equal to the TransactionalEditingDomain of * <code>target</code> parameter * @see #isCompositionAllowed(EObject, EObject[]) * @see #getContentAsEObjects(ResourceSet) */ @SuppressWarnings("unchecked") public Collection<EObject> duplicateAndPaste(final Object target, TransactionalEditingDomain transactionalEditingDomain) throws IllegalStateException { if (transactionalEditingDomain == null) { throw new IllegalStateException("TransactionalEditingDomain targetConnection should not be null"); //$NON-NLS-1$ } final EObject parent = GraphitiUiInternal.getEmfService().getEObject(target); if (parent != null) { // detect TransactionalEditingDomain mismatch to prevent // modification of wrong object final TransactionalEditingDomain parentEditingDomain = TransactionUtil.getEditingDomain(parent); if (parentEditingDomain != null && !transactionalEditingDomain.equals(parentEditingDomain)) { throw new IllegalStateException( "Ambiguous TransactionalEditingDomains: transactionalEditingDomain: " + transactionalEditingDomain //$NON-NLS-1$ + " <-> TransactionalEditingDomain of target object: " + parentEditingDomain //$NON-NLS-1$ + ". Not clear which one to use for copy."); //$NON-NLS-1$ } } EObject[] srcObjects; try { srcObjects = getContentAsEObjects(transactionalEditingDomain.getResourceSet()); if (srcObjects.length == 0) { return null; // no or no resolvable objects in clipboard } } catch (final OperationCanceledException e) { // $JL-EXC$ return null; } catch (final Exception e) { // $JL-EXC$ T.racer().error(e.getMessage(), e); return null; } // subsequent operations run in a command and are rolled back in case of // errors final CommandStack commandStack = transactionalEditingDomain.getCommandStack(); final Collection<EObject>[] copyResults = new Collection[1]; try { final EObject[] srcObjectsFinal = srcObjects; final RecordingCommand command = new CopyCommand(transactionalEditingDomain, parent, srcObjectsFinal, copyResults); commandStack.execute(command); // commit return copyResults[0]; } catch (final OperationCanceledException e) { // $JL-EXC$ // user cancelled rollback(transactionalEditingDomain); return null; } catch (final Exception e) { // $JL-EXC$ // unspecific error T.racer().error(e.getMessage(), e); rollback(transactionalEditingDomain); return null; } } @SuppressWarnings("unchecked") private Collection<EObject> deepCopy(final EObject[] srcObjects) { if (srcObjects == null) { throw new IllegalArgumentException("EObject[] srcObjects must not be null"); //$NON-NLS-1$ } if (srcObjects.length == 0) { throw new IllegalArgumentException("EObject[] srcObjects.length must not be 0"); //$NON-NLS-1$ } final Collection<EObject>[] result = new Collection[1]; // in the case of a UI if (canUseUI()) { BusyIndicator.showWhile(Display.getCurrent(), new Runnable() { public void run() { final Copier copier = new Copier(true, true); result[0] = copier.copyAll(Arrays.asList(srcObjects)); copier.copyReferences(); } }); return result[0]; } // in the non-UI case final Copier copier = new Copier(true, true); result[0] = copier.copyAll(Arrays.asList(srcObjects)); copier.copyReferences(); return result[0]; } /** * Adds the given elements to <code>parent</code> as composite children.s * * @param parent * parent to add the objects to. Must not be <code>null</code>. * @param objects * the objects to add * @param association * an explicit association or <code>null</code> */ private void addToCompositeParent(EObject parent, EObject[] objects, EReference association) { if (parent == null) { throw new IllegalStateException("Parent must not be null"); //$NON-NLS-1$ } for (final EObject object : objects) { final EObject objectParent = object.eContainer(); if (objectParent != null) { if (T.racer().debug()) { final String msg = "Ignoring " + toObjectString(object) //$NON-NLS-1$ + " for parent assignment. Already assigned to " + toObjectString(objectParent); //$NON-NLS-1$ T.racer().debug(msg); } continue; } // Find the composition relationships to use between parent and // child final List<EReference> assocs = findUsableTargetAssociations(parent, object); EReference assoc; switch (assocs.size()) { case 0: final String msg = "No composite associations found for " //$NON-NLS-1$ + toObjectString(parent.eClass()) + " -> " //$NON-NLS-1$ + toObjectString(object.eClass()); // Don't issue an error here. The client might already have // composed the objects. T.racer().debug(msg); continue; case 1: assoc = assocs.get(0); break; default: // multiple associations if (association != null) { if (!assocs.contains(association)) { throw new IllegalStateException("Given association " + association.getName() //$NON-NLS-1$ + " not valid among " + toAssociationNames(assocs)); //$NON-NLS-1$ } assoc = association; break; } else { throw new IllegalStateException("Multiple associations available " //$NON-NLS-1$ + toAssociationNames(assocs)); } } // Use the obtained association to compose the child into the parent compose(parent, object, assoc); } } private static List<String> toAssociationNames(List<EReference> assocs) { final List<String> names = new ArrayList<String>(assocs.size()); for (final EReference assoc : assocs) { names.add(assoc.getName()); } return names; } private static String toObjectString(Object o) { if (o instanceof Collection<?>) { return toObjectsString((Collection<?>) o); } return toObjectsString(Collections.singleton(o)); } private static String toObjectsString(Collection<?> objects) { final StringBuilder b = new StringBuilder(); for (final Object o : objects) { if (o instanceof EObject) { final EObject object = (EObject) o; final String name = GraphitiUiInternal.getEmfService().getObjectName(object); final String type = object.eClass().getName(); b.append(type).append(" '").append(name).append("'"); //$NON-NLS-1$ //$NON-NLS-2$ } else { b.append(o); } } return b.toString(); } /** * Adds the given child to the given parent in the given association. */ @SuppressWarnings("unchecked") private static void compose(EObject parent, EObject child, EReference assoc) { final Object o = parent.eGet(assoc, true); if (o instanceof List<?>) { final List<EObject> list = (List<EObject>) o; list.add(child); } else { parent.eSet(assoc, child); } } /** * @return all copied elements from <code>result</code> that originate from * root elements in the source */ private Map<EObject, EObject> getTargetRootElements(Collection<EObject> result, EObject[] srcElements) { final Map<EObject, EObject> elements = new LinkedHashMap<EObject, EObject>(srcElements.length); EObject[] resultArray = new EObject[result.size()]; resultArray = result.toArray(resultArray); for (int i = 0; i < srcElements.length; i++) { elements.put(resultArray[i], srcElements[i]); } return elements; } /** * Finds all composite associations for the given <code>parent</code> object * that reference a child of type <code>child</code>. Associations that have * a upper cardinality of '1' are not contained in the returned * <code>List</code>. * * @param parent * the composite parent element. * @param child * the <code>EObject</code> defining the type of the composite * children. * @return the list containing all composite associations that fulfil the * selection criteria described above. */ static List<EReference> findUsableTargetAssociations(EObject parent, EObject child) { final List<EReference> compositeAssociations = new ArrayList<EReference>(); final Collection<EReference> contents = getContainmentReferences(parent.eClass()); for (final Iterator<EReference> iterator = contents.iterator(); iterator.hasNext();) { final EReference reference = iterator.next(); final EClassifier referenceType = reference.getEType(); if (referenceType.isInstance(child)) { final Object value = parent.eGet(reference); if (reference.getUpperBound() != 1 || value == null) { compositeAssociations.add(reference); } } } return compositeAssociations; } private static Collection<EReference> getContainmentReferences(EClass eclass) { final Collection<EReference> assocs = new ArrayList<EReference>(); final EList<EObject> contents = eclass.eContents(); for (final Iterator<EObject> iterator = contents.iterator(); iterator.hasNext();) { final EObject object = iterator.next(); if (object instanceof EReference) { final EReference reference = (EReference) object; if (reference.isContainment()) { assocs.add(reference); } } } final EList<EClass> superTypes = eclass.getESuperTypes(); for (final Iterator<EClass> iterator = superTypes.iterator(); iterator.hasNext();) { assocs.addAll(getContainmentReferences(iterator.next())); } return assocs; } /** * Reverts the active command group. */ private void rollback(final TransactionalEditingDomain targetTED) { try { targetTED.runExclusive(new Runnable() { public void run() { final EList<Resource> resources = targetTED.getResourceSet().getResources(); for (final Iterator<Resource> iterator = resources.iterator(); iterator.hasNext();) { final Resource resource = iterator.next(); resource.unload(); resource.setModified(false); } } }); } catch (final InterruptedException e) { } } private synchronized List<String> getContentAsStringList() { List<String> strings = Collections.emptyList(); if (canUseNative()) { // must run in UI thread strings = getNativeContent(); } return strings; } /** * Sets the content of the SWT {@link clipboard} with the given objects. */ private void setNativeContentObjects(List<EObject> objects) { final Map<Transfer, Object> nativeFormat = toTransferObjects(objects); final int size = nativeFormat.size(); if (size > 0) { final Object[] data = nativeFormat.values().toArray(new Object[size]); final Transfer[] dataTypes = nativeFormat.keySet().toArray(new Transfer[size]); final Clipboard cb = new Clipboard(Display.getCurrent()); try { cb.setContents(data, dataTypes); } finally { cb.dispose(); } } } /** * @returns currently the {@link URI} content as {@link UriTransfer}, * {@link TextTransfer}, {@link FileTransfer}, and Eclipse * {@link ResourceTransfer}. */ private synchronized Map<Transfer, Object> toTransferObjects(List<EObject> objects) { final Map<Transfer, Object> empty = Collections.emptyMap(); final int size = objects.size(); if (size == 0) { return empty; } final List<String> uriStrings = new ArrayList<String>(size); final List<IResource> files = new ArrayList<IResource>(size); final List<String> filePaths = new ArrayList<String>(size); for (int i = 0; i < size; i++) { final EObject o = objects.get(i); uriStrings.add(EcoreUtil.getURI(o).toString()); IFile file = null; if (isSoleContent(o)) { file = GraphitiUiInternal.getEmfService().getFile(o); } else { file = (IFile) Platform.getAdapterManager().getAdapter(o, IFile.class); } if (file != null && file.exists() && !files.contains(file)) { files.add(file); filePaths.add(file.getLocation().toOSString()); } } final Map<Transfer, Object> result = new HashMap<Transfer, Object>(7); final UriTransferData data = new UriTransferData(uriStrings); ISelection localSelection = new StructuredSelection(objects); LocalSelectionTransfer.getTransfer().setSelection(localSelection); result.put(LocalSelectionTransfer.getTransfer(), new Object()); result.put(UriTransfer.getInstance(), data); if (!filePaths.isEmpty()) { result.put(FileTransfer.getInstance(), filePaths.toArray(new String[filePaths.size()])); // Resource Transfer resides in org.eclipse.ui.ide. We need to // support an RCP scenario without having this plug-in installed. try { Transfer resourceTransfer = ReflectionUtil.getResourceTransfer(); if (resourceTransfer != null) result.put(resourceTransfer, files.toArray(new IResource[files.size()])); } catch (Exception e) { T.racer().debug(e.getMessage()); } } result.put(TextTransfer.getInstance(), toExtendedString(objects)); return result; } private boolean isSoleContent(final EObject o) { Resource res = o.eResource(); return (res != null && res.getContents().size() == 1); } private static List<String> getNativeContent() { final Clipboard cb = new Clipboard(Display.getCurrent()); try { final UriTransferData contents = (UriTransferData) cb.getContents(UriTransfer.getInstance()); if (contents != null) { return contents.getUriStrings(); } return Collections.emptyList(); } finally { cb.dispose(); } } private String toExtendedString(List<EObject> objects) { final StringBuilder result = new StringBuilder(); for (int i = 0; i < objects.size(); i++) { final EObject o = objects.get(i); GraphitiUiInternal.getEmfService().toString(o, result); if (i < objects.size() - 1) { result.append(UriTransferData.LINE_SEP); } } return result.toString(); } @Override public String toString() { if (!(getContentAsStringList().size() > 0)) { return EMPTY_TOSTRING_PLACEHOLDER; } final List<String> content = getContentAsStringList(); final String[] strings = content.toArray(new String[content.size()]); final StringBuilder b = new StringBuilder(); for (int i = 0; i < strings.length; i++) { final String s = strings[i]; b.append(s); if (i < strings.length - 1) { b.append(UriTransferData.LINE_SEP); } } return b.toString(); } /** * @return whether the SWT {@link Clipboard} can be accessed at all and in * the current thread */ private synchronized boolean canUseNative() { final boolean result = canUseUI(); if (!result) { throw new IllegalStateException("ModelClipboard must be called from UI thread."); //$NON-NLS-1$ } return result; } /** * @return whether Ui may be raised */ private static boolean canUseUI() { return Display.getCurrent() != null; } private final class CopyCommand extends RecordingCommand { private final EObject parent; private final EObject[] srcObjectsFinal; private final Collection<EObject>[] copyResults; private CopyCommand(TransactionalEditingDomain domain, EObject parent, EObject[] srcObjectsFinal, Collection<EObject>[] copyResults) { super(domain); this.parent = parent; this.srcObjectsFinal = srcObjectsFinal; this.copyResults = copyResults; } @Override public String getLabel() { return Messages.ModelClipBoardPasteAction_0_xfld; } @Override protected void doExecute() { // actual copy final Collection<EObject> copyResult = deepCopy(this.srcObjectsFinal); // Process along the root elements only, avoid the effect of a child // element to appear before its parent could be handled. final Map<EObject, EObject> targetRootElements = getTargetRootElements(copyResult, this.srcObjectsFinal); final Set<EObject> targetElements = targetRootElements.keySet(); final EObject[] elements = targetElements.toArray(new EObject[targetElements.size()]); if (this.parent != null) { addToCompositeParent(this.parent, elements, null); } this.copyResults[0] = copyResult; } } }