### Eclipse Workspace Patch 1.0 #P org.eclipse.gmf.runtime.common.ui Index: src/org/eclipse/gmf/runtime/common/ui/util/UIModificationValidator.java =================================================================== RCS file: /cvsroot/modeling/org.eclipse.gmf/plugins/org.eclipse.gmf.runtime.common.ui/src/org/eclipse/gmf/runtime/common/ui/util/UIModificationValidator.java,v retrieving revision 1.4 diff -u -r1.4 UIModificationValidator.java --- src/org/eclipse/gmf/runtime/common/ui/util/UIModificationValidator.java 13 Oct 2006 19:53:22 -0000 1.4 +++ src/org/eclipse/gmf/runtime/common/ui/util/UIModificationValidator.java 10 Dec 2007 21:24:15 -0000 @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright (c) 2005, 2006 IBM Corporation and others. + * Copyright (c) 2005, 2007 IBM Corporation 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 @@ -16,9 +16,11 @@ import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.gmf.runtime.common.core.command.IModificationValidator; +import org.eclipse.gmf.runtime.common.core.command.FileModificationValidator.ISyncExecHelper; import org.eclipse.gmf.runtime.common.core.util.StringStatics; import org.eclipse.gmf.runtime.common.ui.internal.l10n.CommonUIMessages; import org.eclipse.gmf.runtime.common.ui.resources.FileModificationValidator; +import org.eclipse.jface.operation.ModalContext; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IWindowListener; @@ -117,22 +119,83 @@ } }); } - - /* (non-Javadoc) - * @see org.eclipse.gmf.runtime.common.core.command.IModificationValidator#validateEdit(org.eclipse.core.resources.IFile[]) - */ - public IStatus validateEdit(IFile[] files) { - Shell shell = listener == null ? null : listener.getShell(); + + /** + * Helper class that allows us to return status information + * in addition to providing the option of clearing the + * shell variable before running doValidate(). + * + * @author James Bruck (jbruck) + */ + class RunnableWithStatus implements Runnable { + + private final IFile[] files; + private IStatus status; + private Shell shell; + + RunnableWithStatus(IFile[] files, Shell shell) { + this.files = files; + this.shell = shell; + } + + public void run() { + status = doValidateEdit(files, shell); + } + public IStatus getResult() { + return status; + } + + public void setShell(Shell shell) { + this.shell = shell; + } + } + + /** + * This is the where the real call to validate the files takes place. + * + * @param files list of files to validate. + * @param shell the shell to use when displaying error messages. + * @return the status indicating whether the validate succeeded or not. + */ + protected IStatus doValidateEdit(IFile[] files, Shell shell) { + boolean ok = FileModificationValidator.getInstance().okToEdit(files, - CommonUIMessages.UIModificationValidator_ModificationMessage, shell); + CommonUIMessages.UIModificationValidator_ModificationMessage, + shell); + return ok ? Status.OK_STATUS : ERROR_STATUS; + } + + /* + * (non-Javadoc) + * @see org.eclipse.gmf.runtime.common.core.command.IModificationValidator#validateEdit(org.eclipse.core.resources.IFile[]) + */ + public IStatus validateEdit(IFile[] files) { + + Shell shell = listener == null ? null : listener.getShell(); + RunnableWithStatus r = new RunnableWithStatus(files, shell); + + ISyncExecHelper syncExecHelper = org.eclipse.gmf.runtime.common.core.command.FileModificationValidator.SyncExecHelper + .getInstance(); - return ok ? Status.OK_STATUS - : ERROR_STATUS; + if (ModalContext.isModalContextThread(Thread.currentThread())) { + Runnable safeRunnable = syncExecHelper.safeRunnable(r); + if( safeRunnable != null){ + Display.getDefault().syncExec(safeRunnable); + } else { + r.run(); + } + } else { + if (Display.getCurrent() == null) { + r.setShell(null); + } + r.run(); + } + return r.getResult(); } /** - * Disposes this UI modification validator. - */ + * Disposes this UI modification validator. + */ public void dispose() { if (listener != null) { Display.getDefault().asyncExec(new Runnable() { @@ -142,5 +205,4 @@ }); } } - } \ No newline at end of file Index: src/org/eclipse/gmf/runtime/common/ui/internal/l10n/CommonUIMessages.java =================================================================== RCS file: /cvsroot/modeling/org.eclipse.gmf/plugins/org.eclipse.gmf.runtime.common.ui/src/org/eclipse/gmf/runtime/common/ui/internal/l10n/CommonUIMessages.java,v retrieving revision 1.5 diff -u -r1.5 CommonUIMessages.java --- src/org/eclipse/gmf/runtime/common/ui/internal/l10n/CommonUIMessages.java 21 Nov 2006 22:47:08 -0000 1.5 +++ src/org/eclipse/gmf/runtime/common/ui/internal/l10n/CommonUIMessages.java 10 Dec 2007 21:24:15 -0000 @@ -95,6 +95,7 @@ public static String ActionAbandonedDialog_title; public static String SaveAllDirtyEditorsDialog_title; public static String SaveAllDirtyEditorsDialog_message; + public static String FileModificationValidator_OutOfSyncMessage; static { NLS.initializeMessages(BUNDLE_NAME, CommonUIMessages.class); Index: src/org/eclipse/gmf/runtime/common/ui/internal/l10n/CommonUIMessages.properties =================================================================== RCS file: /cvsroot/modeling/org.eclipse.gmf/plugins/org.eclipse.gmf.runtime.common.ui/src/org/eclipse/gmf/runtime/common/ui/internal/l10n/CommonUIMessages.properties,v retrieving revision 1.5 diff -u -r1.5 CommonUIMessages.properties --- src/org/eclipse/gmf/runtime/common/ui/internal/l10n/CommonUIMessages.properties 21 Nov 2006 22:47:08 -0000 1.5 +++ src/org/eclipse/gmf/runtime/common/ui/internal/l10n/CommonUIMessages.properties 10 Dec 2007 21:24:15 -0000 @@ -182,6 +182,9 @@ FileModificationValidator_OK=OK # {0} = Full path of the file including the file name FileModificationValidator_FileIsReadOnlyErrorMessage=File {0} is read-only. + +# Status message informing the user that files are out of sync with the workspace +FileModificationValidator_OutOfSyncMessage=The following files are out of synchronization with the workspace: # ==================================== END ================================================= # Popup Dialog Strings; should be translated Index: src/org/eclipse/gmf/runtime/common/ui/resources/FileModificationValidator.java =================================================================== RCS file: /cvsroot/modeling/org.eclipse.gmf/plugins/org.eclipse.gmf.runtime.common.ui/src/org/eclipse/gmf/runtime/common/ui/resources/FileModificationValidator.java,v retrieving revision 1.6 diff -u -r1.6 FileModificationValidator.java --- src/org/eclipse/gmf/runtime/common/ui/resources/FileModificationValidator.java 13 Oct 2006 19:53:22 -0000 1.6 +++ src/org/eclipse/gmf/runtime/common/ui/resources/FileModificationValidator.java 10 Dec 2007 21:24:15 -0000 @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright (c) 2002, 2006 IBM Corporation and others. + * Copyright (c) 2002, 2007 IBM Corporation 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 @@ -11,16 +11,26 @@ package org.eclipse.gmf.runtime.common.ui.resources; +import java.io.File; +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFileModificationValidator; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.gmf.runtime.common.ui.internal.CommonUIPlugin; import org.eclipse.gmf.runtime.common.ui.internal.l10n.CommonUIMessages; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.osgi.util.NLS; -import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.team.core.RepositoryProvider; import org.eclipse.ui.PlatformUI; @@ -87,36 +97,31 @@ * @return true if it is OK to edit the files. * @see org.eclipse.core.resources.IFileModificationValidator#validateEdit */ - public boolean okToEdit(final IFile[] files, final String modificationReason, Shell shell) { - if (PlatformUI.getWorkbench().getActiveWorkbenchWindow() != null && Display.getCurrent() != null) { - shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow() - .getShell(); - } + public boolean okToEdit(final IFile[] files, + final String modificationReason, final Shell shell) { - final IStatus status = ResourcesPlugin.getWorkspace().validateEdit( - files, shell); - if (status.isOK()) { - return true; - } else { - PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { - - public void run() { - MessageDialog - .openError( - PlatformUI.getWorkbench() - .getActiveWorkbenchWindow().getShell(), - NLS - .bind( - CommonUIMessages.FileModificationValidator_EditProblemDialogTitle, - modificationReason), - NLS - .bind( - CommonUIMessages.FileModificationValidator_EditProblemDialogMessage, - modificationReason, status.getMessage())); - } - }); + final IStatus fileStatus = validateEdit(files, shell); + + if (!fileStatus.isOK()) { + // Similar to the DefaultUIFileModificationValidator we check if the shell is not null before + // displaying messages. + if(shell != null){ + PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { + public void run() { + MessageDialog + .openError( + shell, + NLS.bind(CommonUIMessages.FileModificationValidator_EditProblemDialogTitle, + modificationReason), + NLS.bind(CommonUIMessages.FileModificationValidator_EditProblemDialogMessage, + modificationReason, + fileStatus.getMessage())); + } + }); + } return false; } + return true; } /** @@ -183,4 +188,169 @@ Status.OK, CommonUIMessages.FileModificationValidator_OK, null); } } + + /** + * Validates changes to the specified array of IFiles using the specified shell as a UI context. + * This code delegates the bulk of its processing to the {@link IWorkspace#validateEdit(IFile[], Object) + * method, but additionally checks to see if the specified IFiles are out of synchronization with + * the filesystem, and if so, returns an error status. + * + * @param files the array of files for which edit validation is requested + * @param shell a UI context (SWT shell) for the validation. Typed as object to avoid SWT dependency. + * @return IStatus, {@link IStatus#OK} if edit of the specified files may proceed, {@link IStatus#ERROR} + * or {@link IStatus#CANCEL} otherwise. + */ + public IStatus validateEdit(IFile[] files, Object shell) { + IStatus status = Status.OK_STATUS; + if (files == null || files.length == 0) { + return status; + } + Set unsynchedFiles = new HashSet(); + Map filesToModificationStamps = new HashMap(); + + for (int i = 0; i < files.length; i++) { + IFile file = files[i]; + filesToModificationStamps.put(file, new ModificationStamp(file)); + boolean inSync = file.isSynchronized(IResource.DEPTH_ZERO); + if (!inSync) { + unsynchedFiles.add(file); + } + } + if (!unsynchedFiles.isEmpty()) { + status = buildOutOfSyncStatus(unsynchedFiles); + } + + if (status.isOK()) { + status = ResourcesPlugin.getWorkspace().validateEdit(files, shell); + + for (Map.Entry entry : filesToModificationStamps.entrySet()) { + IFile file = entry.getKey(); + ModificationStamp stamp = entry.getValue(); + if (stamp.hasFileChanged()) { + unsynchedFiles.add(file); + } + } + if (!unsynchedFiles.isEmpty()) { + status = buildOutOfSyncStatus(unsynchedFiles); + } + + } + return status; + } + + /** + * Helper method to create a status for out of sync files. + * + * @param unsynchedFiles Files that may be out of sync. + * @return A status for out of sync files. + */ + private IStatus buildOutOfSyncStatus(Set unsynchedFiles) { + StringBuffer buf = new StringBuffer( + CommonUIMessages.FileModificationValidator_OutOfSyncMessage); + buf.append("\n"); //$NON-NLS-1$ + for (Iterator unsynched = unsynchedFiles.iterator(); unsynched + .hasNext();) { + IFile file = unsynched.next(); + buf.append(file.getFullPath().toString()); + buf.append("\n"); //$NON-NLS-1$ + } + return new Status(IStatus.ERROR, CommonUIPlugin.getPluginId(), 0, buf + .toString(), null); + } + + private static class ModificationStamp { + + /** + * The file that the modification stamp applies to. + */ + private IFile file; + /** + * Last modified date and time of the file + */ + private Timestamp lastModified = null; + /** + * The file's length. + */ + private long fileLength = 0L; + + public ModificationStamp(IFile file) { + assert file != null; + this.file = file; + IPath path = file.getLocation(); + if (path != null) { + File ioFile = path.toFile(); + if (ioFile != null) { + // new timestamp + lastModified = new Timestamp(ioFile.lastModified()); + lastModified.setNanos(0); + fileLength = ioFile.length(); + } else { + lastModified = new Timestamp(file.getModificationStamp()); + lastModified.setNanos(0); + } + } else { + lastModified = new Timestamp(file.getModificationStamp()); + lastModified.setNanos(0); + } + + } + + public Timestamp getLastModified() { + return lastModified; + } + + public long getFileLength() { + return fileLength; + } + + public IFile getFile() { + return file; + } + + /** + * Used in determining if two timestamps are equivalent + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof ModificationStamp) { + ModificationStamp stamp = (ModificationStamp) obj; + return file.equals(stamp.getFile()) + && fileLength == stamp.getFileLength() + && lastModified.equals(stamp.getLastModified()); + } + + return false; + } + + public int hashCode() { + return file.hashCode() + lastModified.hashCode() + + (int) (fileLength ^ (fileLength >>> 32)); + + } + + /** + * Determines if the file has changed. + * + * @return true if the file has changed. + */ + public boolean hasFileChanged() { + IPath path = file.getLocation(); + if (path == null) { + return false; + } + File ioFile = path.toFile(); + if (ioFile == null) { + return false; + } + + // new timestamp + Timestamp newTimestamp = new Timestamp(ioFile.lastModified()); + newTimestamp.setNanos(0); + + return !(lastModified.getTime() == newTimestamp.getTime() && fileLength == ioFile + .length()); + } + } + } \ No newline at end of file #P org.eclipse.gmf.runtime.common.core Index: src/org/eclipse/gmf/runtime/common/core/command/FileModificationValidator.java =================================================================== RCS file: /cvsroot/modeling/org.eclipse.gmf/plugins/org.eclipse.gmf.runtime.common.core/src/org/eclipse/gmf/runtime/common/core/command/FileModificationValidator.java,v retrieving revision 1.1 diff -u -r1.1 FileModificationValidator.java --- src/org/eclipse/gmf/runtime/common/core/command/FileModificationValidator.java 13 Feb 2006 19:11:59 -0000 1.1 +++ src/org/eclipse/gmf/runtime/common/core/command/FileModificationValidator.java 10 Dec 2007 21:24:16 -0000 @@ -62,13 +62,67 @@ return validator; } + public static IStatus approveFileModification(IFile[] files) { + return getValidator().validateEdit(files); + } + /** - * Checks that the files may be modified. + * This interface works in conjuction with the {@link SyncExecHelper} + * to bridge entities knowing of UI and those with knowledge of editing domains. * - * @return the approval status + * @author James Bruck (jbruck@ca.ibm.com) + * */ - public static IStatus approveFileModification(IFile[] files) { - return getValidator().validateEdit(files); + public interface ISyncExecHelper { + + /** + * Will wrap the input runnable with one that is thread safe. + * + * @param runnable + */ + public Runnable safeRunnable(Runnable runnable); + } + + /** + * + * Utility class that is used to bridge those entities that + * have knowledge of UI and those that have knowledge of editing domains. + * Uses {@link ISyncExecHelper}. + * It is always initialized so we don't have to worry about null checking. + * + * @author James Bruck (jbruck@ca.ibm.com) + */ + public static class SyncExecHelper implements ISyncExecHelper { + + private static ISyncExecHelper INSTANCE; + static { + SyncExecHelper.setInstance(new SyncExecHelper()); + } + + /** + * The result of this method is guaranteed to be + * non-null since we initialize it with a default implementation. + * + * @return the ISyncExecHelper instance + */ + public static synchronized ISyncExecHelper getInstance() { + return INSTANCE; + } + + private SyncExecHelper(){ + // not intended to be called externally + } + + public static synchronized void setInstance(ISyncExecHelper instance) { + SyncExecHelper.INSTANCE = instance; + } + + /** + * Provides a default implementation. + */ + public Runnable safeRunnable(Runnable runnable) { + return runnable; + } } } #P org.eclipse.gmf.runtime.emf.core Index: src/org/eclipse/gmf/runtime/emf/core/GMFEditingDomainFactory.java =================================================================== RCS file: /cvsroot/modeling/org.eclipse.gmf/plugins/org.eclipse.gmf.runtime.emf.core/src/org/eclipse/gmf/runtime/emf/core/GMFEditingDomainFactory.java,v retrieving revision 1.4 diff -u -r1.4 GMFEditingDomainFactory.java --- src/org/eclipse/gmf/runtime/emf/core/GMFEditingDomainFactory.java 12 May 2006 14:56:12 -0000 1.4 +++ src/org/eclipse/gmf/runtime/emf/core/GMFEditingDomainFactory.java 10 Dec 2007 21:24:17 -0000 @@ -11,10 +11,24 @@ package org.eclipse.gmf.runtime.emf.core; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + import org.eclipse.core.commands.operations.IOperationHistory; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.transaction.Transaction; import org.eclipse.emf.transaction.TransactionalEditingDomain; +import org.eclipse.emf.transaction.impl.InternalTransactionalEditingDomain; +import org.eclipse.emf.transaction.util.TransactionUtil; import org.eclipse.emf.workspace.WorkspaceEditingDomainFactory; +import org.eclipse.emf.workspace.util.WorkspaceValidateEditSupport; +import org.eclipse.gmf.runtime.common.core.command.FileModificationValidator; +import org.eclipse.gmf.runtime.common.core.command.FileModificationValidator.ISyncExecHelper; +import org.eclipse.gmf.runtime.common.core.command.FileModificationValidator.SyncExecHelper; import org.eclipse.gmf.runtime.emf.core.internal.resources.PathmapManager; import org.eclipse.gmf.runtime.emf.core.util.CrossReferenceAdapter; @@ -28,9 +42,13 @@ * * @author Christian W. Damus (cdamus) */ -public class GMFEditingDomainFactory - extends WorkspaceEditingDomainFactory { +public class GMFEditingDomainFactory extends WorkspaceEditingDomainFactory { + static public TransactionalSyncExecHelper transactionalSyncExecHelper = new TransactionalSyncExecHelper(); + static { + SyncExecHelper.setInstance(transactionalSyncExecHelper); + } + /** * The single shared instance of the GMF editing domain factory. */ @@ -75,17 +93,116 @@ * * @param domain the new editing domain */ - protected void configure(TransactionalEditingDomain domain) { + protected void configure(final TransactionalEditingDomain domain) { ResourceSet rset = domain.getResourceSet(); - + // ensure that the cross-referencing adapter is installed if (CrossReferenceAdapter.getExistingCrossReferenceAdapter(rset) == null) { rset.eAdapters().add(new CrossReferenceAdapter()); } - + // ensure that the path map manager is installed if (PathmapManager.getExistingPathmapManager(rset) == null) { rset.eAdapters().add(new PathmapManager()); } + + TransactionalEditingDomain.DefaultOptions options = (TransactionalEditingDomain.DefaultOptions) (TransactionUtil + .getAdapter(domain, + TransactionalEditingDomain.DefaultOptions.class)); + + Map aMap = new HashMap(); + aMap.put(Transaction.OPTION_VALIDATE_EDIT, + new WorkspaceValidateEditSupport() { + + @SuppressWarnings("unchecked") + protected IStatus doValidateEdit(Transaction transaction, + Collection resources, Object context) { + return GMFEditingDomainFactory.transactionalSyncExecHelper + .approveFileModification(getFiles(resources), + domain); + } + }); + + options.setDefaultTransactionOptions(aMap); + } + + + /** + * A helper that knows about the specific editing domain. + * During the approval process, calls to validateEdit() will require the + * domain in order to execute in a thread safe manner. + * + * @author James Bruck (jbruck) + * + */ + public static class TransactionalSyncExecHelper implements ISyncExecHelper { + + private final ThreadLocal domain = new ThreadLocal(); + + private void setDomain(TransactionalEditingDomain domain) { + this.domain.set(domain); + } + + /** + * Sets the thread specific transactional domain before the approval + * process since subsequent calls to validateEdit() requires it and + * clears it afterward. + * + * @param files + * The files to be validated. + * + * @param transactionalDomain + * The current editing domain. + * + * @return The resulting status. + */ + public IStatus approveFileModification(IFile[] files, + TransactionalEditingDomain transactionalDomain) { + + setDomain(transactionalDomain); + IStatus status = Status.OK_STATUS; + try { + status = FileModificationValidator + .approveFileModification(files); + } finally { + setDomain(null); + } + return status; + } + + /** + * Delegates to the specified domain to obtain a thread safe wrapper + * for the specified runnable + * + * @param runnable a runnable to execute in the context of the active + * transaction, on any thread + * + * @return the privileged runnable if the transaction is on the current + * thread, otherwise just return itself. + */ + public Runnable safeRunnable(Runnable runnable) { + if( isTransactionOnCurrentThread()) { + return domain.get().createPrivilegedRunnable(runnable); + } + return null; + } + + /** + * Checks if the active transaction is on the current thread. + * + * @return true if the active transaction is on the current thread. + */ + private boolean isTransactionOnCurrentThread() { + if (domain.get() != null) { + + Transaction tx = ((InternalTransactionalEditingDomain) domain + .get()).getActiveTransaction(); + + return ((tx != null) && (tx.getOwner() == Thread + .currentThread())); + } + return false; + } } + }