| /******************************************************************************* |
| * Copyright (c) 2000, 2017 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.team.ui.synchronize; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.mapping.IModelProviderDescriptor; |
| import org.eclipse.core.resources.mapping.ModelProvider; |
| import org.eclipse.core.resources.mapping.ResourceMapping; |
| import org.eclipse.core.resources.mapping.ResourceTraversal; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.jface.window.Window; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.team.core.mapping.ISynchronizationContext; |
| import org.eclipse.team.core.mapping.ISynchronizationScope; |
| import org.eclipse.team.core.mapping.ISynchronizationScopeManager; |
| import org.eclipse.team.internal.core.Policy; |
| import org.eclipse.team.internal.core.mapping.CompoundResourceTraversal; |
| import org.eclipse.team.internal.ui.TeamUIMessages; |
| import org.eclipse.team.internal.ui.dialogs.AdditionalMappingsDialog; |
| import org.eclipse.team.ui.TeamOperation; |
| import org.eclipse.ui.IWorkbenchPart; |
| |
| /** |
| * An abstract operation that uses an {@link ISynchronizationScopeManager} to |
| * create an operation scope that includes the complete set of mappings that |
| * must be included in the operation to ensure model consistency. The scope |
| * generation phase will prompt the user if additional resources have been added |
| * to the scope. |
| * |
| * @since 3.2 |
| */ |
| public abstract class ModelOperation extends TeamOperation { |
| |
| private boolean previewRequested; |
| private ISynchronizationScopeManager manager; |
| |
| /** |
| * Return the list of provides sorted by their extends relationship. |
| * Extended model providers will appear later in the list then those |
| * that extends them. The order of model providers that independant |
| * (i.e. no extends relationship between them) will be indeterminate. |
| * @param providers the model providers |
| * @return the list of provides sorted by their extends relationship |
| */ |
| public static ModelProvider[] sortByExtension(ModelProvider[] providers) { |
| List<ModelProvider> result = new ArrayList<>(); |
| for (int i = 0; i < providers.length; i++) { |
| ModelProvider providerToInsert = providers[i]; |
| int index = result.size(); |
| for (int j = 0; j < result.size(); j++) { |
| ModelProvider provider = result.get(j); |
| if (extendsProvider(providerToInsert, provider)) { |
| index = j; |
| break; |
| } |
| } |
| result.add(index, providerToInsert); |
| } |
| return result.toArray(new ModelProvider[result.size()]); |
| } |
| |
| private static boolean extendsProvider(ModelProvider providerToInsert, ModelProvider provider) { |
| String[] extended = providerToInsert.getDescriptor().getExtendedModels(); |
| // First search immediate dependents |
| for (int i = 0; i < extended.length; i++) { |
| String id = extended[i]; |
| if (id.equals(provider.getDescriptor().getId())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Create a model operation that operates on the given scope. |
| * @param part the workbench part from which the merge was launched or <code>null</code> |
| * @param manager the scope manager for this operation |
| */ |
| protected ModelOperation(IWorkbenchPart part, ISynchronizationScopeManager manager) { |
| super(part); |
| this.manager = manager; |
| } |
| |
| /** |
| * Run the operation. This method first ensures that the scope is built |
| * by calling {@link #initializeScope(IProgressMonitor)} and then invokes the |
| * {@link #execute(IProgressMonitor)} method. |
| * @param monitor a progress monitor |
| * @see org.eclipse.jface.operation.IRunnableWithProgress#run(org.eclipse.core.runtime.IProgressMonitor) |
| */ |
| @Override |
| public final void run(IProgressMonitor monitor) throws InvocationTargetException, |
| InterruptedException { |
| try { |
| monitor.beginTask(null, 100); |
| beginOperation(Policy.subMonitorFor(monitor, 5)); |
| execute(Policy.subMonitorFor(monitor, 90)); |
| } finally { |
| endOperation(Policy.subMonitorFor(monitor, 5)); |
| monitor.done(); |
| } |
| } |
| |
| /** |
| * Method called from {@link #run(IProgressMonitor)} before |
| * the {@link #execute(IProgressMonitor)} method is invoked. |
| * This is done to give the operation a chance to initialize |
| * any state required to execute. By default, the |
| * {@link ISynchronizationScopeManager} for this operation |
| * is initialized if it was not previously initialized. |
| * @param monitor a progress monitor |
| * @throws InvocationTargetException |
| */ |
| protected void beginOperation(IProgressMonitor monitor) throws InvocationTargetException { |
| initializeScope(monitor); |
| } |
| |
| /** |
| * Method called from {@link #run(IProgressMonitor)} after the |
| * {@link #execute(IProgressMonitor)} completes of if an exception |
| * is thrown from the {@link #beginOperation(IProgressMonitor)} |
| * or the {@link #execute(IProgressMonitor)}. By default, |
| * this method does nothing. Subclasses may override. |
| * @param monitor a progress monitor |
| */ |
| protected void endOperation(IProgressMonitor monitor) throws InvocationTargetException { |
| // Do nothing by deafult |
| } |
| |
| /** |
| * Adjust the input of the operation according to the selected |
| * resource mappings and the set of interested participants. This method |
| * will prompt the user in the following cases: |
| * <ol> |
| * <li>The scope contains additional resources than those in the input. |
| * <li>The scope has additional mappings from a model in the input |
| * <li>The input contains elements from multiple models |
| * </ol> |
| * <p> |
| * The scope of this operation will only be prepared once. Subsequent |
| * calls to this method will do nothing. Also, if the scope was provided |
| * as an argument to a constructor, this method will do nothing (i.e. the |
| * scope will not be prepared again and no prompting will occur). |
| * <p> |
| * Subclasses can customize how the scope is generated by overriding |
| * the {@link #getScopeManager()} to return a custom scope manager. |
| * @param monitor a progress monitor |
| */ |
| protected final void initializeScope(IProgressMonitor monitor) throws InvocationTargetException { |
| try { |
| if (!manager.isInitialized()) { |
| manager.initialize(monitor); |
| promptIfInputChange(monitor); |
| } |
| } catch (CoreException e) { |
| throw new InvocationTargetException(e); |
| } |
| } |
| |
| /** |
| * Prompt the user by calling {@link #promptForInputChange(String, IProgressMonitor)} |
| * if the scope of the operation was expanded (as described in |
| * {@link #initializeScope(IProgressMonitor)}). |
| * @param monitor a progress monitor |
| */ |
| protected void promptIfInputChange(IProgressMonitor monitor) { |
| ISynchronizationScope inputScope = getScope().asInputScope(); |
| if (getScope().hasAdditionalMappings()) { |
| boolean prompt = false; |
| // There are additional mappings so we may need to prompt |
| ModelProvider[] inputModelProviders = inputScope.getModelProviders(); |
| if (hasAdditionalMappingsFromIndependantModel(inputModelProviders, getScope().getModelProviders())) { |
| // Prompt if the is a new model provider in the scope that is independant |
| // of any of the input mappings |
| prompt = true; |
| } else if (getScope().hasAdditonalResources()) { |
| // We definitely need to prompt to indicate that additional resources |
| prompt = true; |
| } else if (inputModelProviders.length == 1) { |
| // We may need to prompt depending on the nature of the additional mappings |
| // We need to prompt if the additional mappings are from the same model as |
| // the input or if they are from a model that has no relationship to the input model |
| String modelProviderId = inputModelProviders[0].getDescriptor().getId(); |
| ResourceMapping[] mappings = getScope().getMappings(); |
| for (int i = 0; i < mappings.length; i++) { |
| ResourceMapping mapping = mappings[i]; |
| if (inputScope.getTraversals(mapping) == null) { |
| // This mapping was not in the input |
| String id = mapping.getModelProviderId(); |
| if (id.equals(modelProviderId) && !modelProviderId.equals(ModelProvider.RESOURCE_MODEL_PROVIDER_ID)) { |
| prompt = true; |
| break; |
| } else if (isIndependantModel(modelProviderId, id)) { |
| prompt = true; |
| break; |
| } |
| } |
| } |
| } else { |
| // We need to prompt if there are additional mappings from an input |
| // provider whose traversals overlap those of the input mappings. |
| for (int i = 0; i < inputModelProviders.length; i++) { |
| ModelProvider provider = inputModelProviders[i]; |
| String id = provider.getDescriptor().getId(); |
| ResourceMapping[] inputMappings = inputScope.getMappings(id); |
| ResourceMapping[] scopeMappings = getScope().getMappings(id); |
| if (inputMappings.length != scopeMappings.length) { |
| // There are more mappings for this provider. |
| // We need to see if any of the new ones overlap the old ones. |
| for (int j = 0; j < scopeMappings.length; j++) { |
| ResourceMapping mapping = scopeMappings[j]; |
| ResourceTraversal[] inputTraversals = inputScope.getTraversals(mapping); |
| if (inputTraversals == null) { |
| // This mapping was not in the input. |
| // We need to prompt if the traversal for this mapping overlaps with |
| // the input mappings for the model provider |
| // TODO could check for project overlap first |
| ResourceTraversal[] scopeTraversals = getScope().getTraversals(mapping); |
| ResourceTraversal[] inputModelTraversals = getTraversals(inputScope, inputMappings); |
| if (overlaps(scopeTraversals, inputModelTraversals)) { |
| prompt = true; |
| break; |
| } |
| } |
| } |
| } |
| } |
| } |
| if (prompt) { |
| String previewMessage = getPreviewRequestMessage(); |
| previewRequested = promptForInputChange(previewMessage, monitor); |
| } |
| } |
| } |
| |
| /** |
| * Return a string to be used in the preview request on the scope prompt |
| * or <code>null</code> if a preview of the operation results is not possible. |
| * By default, <code>null</code> is returned but subclasses may override. |
| * @return a string to be used in the preview request on the scope prompt |
| * or <code>null</code> if a preview of the operation results is not possible |
| */ |
| protected String getPreviewRequestMessage() { |
| return null; |
| } |
| |
| private boolean hasAdditionalMappingsFromIndependantModel(ModelProvider[] inputModelProviders, ModelProvider[] modelProviders) { |
| ModelProvider[] additionalProviders = getAdditionalProviders(inputModelProviders, modelProviders); |
| for (int i = 0; i < additionalProviders.length; i++) { |
| ModelProvider additionalProvider = additionalProviders[i]; |
| boolean independant = true; |
| // Return true if the new provider is independant of all input providers |
| for (int j = 0; j < inputModelProviders.length; j++) { |
| ModelProvider inputProvider = inputModelProviders[j]; |
| if (!isIndependantModel(additionalProvider.getDescriptor().getId(), inputProvider.getDescriptor().getId())) { |
| independant = false; |
| } |
| } |
| if (independant) |
| return true; |
| } |
| return false; |
| } |
| |
| private ModelProvider[] getAdditionalProviders(ModelProvider[] inputModelProviders, ModelProvider[] modelProviders) { |
| Set<ModelProvider> input = new HashSet<>(); |
| List<ModelProvider> result = new ArrayList<>(); |
| input.addAll(Arrays.asList(inputModelProviders)); |
| for (int i = 0; i < modelProviders.length; i++) { |
| ModelProvider provider = modelProviders[i]; |
| if (!input.contains(provider)) |
| result.add(provider); |
| } |
| return result.toArray(new ModelProvider[result.size()]); |
| } |
| |
| private boolean overlaps(ResourceTraversal[] scopeTraversals, ResourceTraversal[] inputModelTraversals) { |
| for (int i = 0; i < inputModelTraversals.length; i++) { |
| ResourceTraversal inputTraversal = inputModelTraversals[i]; |
| for (int j = 0; j < scopeTraversals.length; j++) { |
| ResourceTraversal scopeTraversal = scopeTraversals[j]; |
| if (overlaps(inputTraversal, scopeTraversal)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private boolean overlaps(ResourceTraversal inputTraversal, ResourceTraversal scopeTraversal) { |
| IResource[] inputRoots = inputTraversal.getResources(); |
| IResource[] scopeRoots = scopeTraversal.getResources(); |
| for (int i = 0; i < scopeRoots.length; i++) { |
| IResource scopeResource = scopeRoots[i]; |
| for (int j = 0; j < inputRoots.length; j++) { |
| IResource inputResource = inputRoots[j]; |
| if (overlaps(scopeResource, scopeTraversal.getDepth(), inputResource, inputTraversal.getDepth())) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean overlaps(IResource scopeResource, int scopeDepth, IResource inputResource, int inputDepth) { |
| if (scopeResource.equals(inputResource)) |
| return true; |
| if (scopeDepth == IResource.DEPTH_INFINITE && scopeResource.getFullPath().isPrefixOf(inputResource.getFullPath())) { |
| return true; |
| } |
| if (scopeDepth == IResource.DEPTH_ONE && scopeResource.equals(inputResource.getParent())) { |
| return true; |
| } |
| if (inputDepth == IResource.DEPTH_INFINITE && inputResource.getFullPath().isPrefixOf(scopeResource.getFullPath())) { |
| return true; |
| } |
| if (inputDepth == IResource.DEPTH_ONE && inputResource.equals(scopeResource.getParent())) { |
| return true; |
| } |
| return false; |
| } |
| |
| private ResourceTraversal[] getTraversals(ISynchronizationScope inputScope, ResourceMapping[] inputMappings) { |
| CompoundResourceTraversal result = new CompoundResourceTraversal(); |
| for (int i = 0; i < inputMappings.length; i++) { |
| ResourceMapping mapping = inputMappings[i]; |
| result.addTraversals(inputScope.getTraversals(mapping)); |
| } |
| return result.asTraversals(); |
| } |
| |
| private boolean isIndependantModel(String modelProviderId, String id) { |
| if (id.equals(modelProviderId)) |
| return false; |
| IModelProviderDescriptor desc1 = ModelProvider.getModelProviderDescriptor(modelProviderId); |
| IModelProviderDescriptor desc2 = ModelProvider.getModelProviderDescriptor(id); |
| return !(isExtension(desc1, desc2) || isExtension(desc2, desc1)); |
| } |
| |
| /* |
| * Return whether the desc1 model extends the desc2 model |
| */ |
| private boolean isExtension(IModelProviderDescriptor desc1, IModelProviderDescriptor desc2) { |
| String[] ids = desc1.getExtendedModels(); |
| // First check direct extension |
| for (int i = 0; i < ids.length; i++) { |
| String id = ids[i]; |
| if (id.equals(desc2.getId())) { |
| return true; |
| } |
| } |
| // Now check for indirect extension |
| for (int i = 0; i < ids.length; i++) { |
| String id = ids[i]; |
| IModelProviderDescriptor desc3 = ModelProvider.getModelProviderDescriptor(id); |
| if (isExtension(desc3, desc2)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Prompt the user to inform them that additional resource mappings |
| * have been included in the operations. |
| * @param requestPreviewMessage message to be displayed for the option to force a preview |
| * (or <code>null</code> if the preview option should not be presented |
| * @param monitor a progress monitor |
| * @return whether a preview of the operation results was requested |
| * @throws OperationCanceledException if the user choose to cancel |
| */ |
| protected boolean promptForInputChange(String requestPreviewMessage, IProgressMonitor monitor) { |
| return showAllMappings(requestPreviewMessage); |
| } |
| |
| private boolean showAllMappings(final String requestPreviewMessage) { |
| final boolean[] canceled = new boolean[] { false }; |
| final boolean[] forcePreview = new boolean[] { false }; |
| Display.getDefault().syncExec(() -> { |
| AdditionalMappingsDialog dialog = new AdditionalMappingsDialog(getShell(), TeamUIMessages.ResourceMappingOperation_0, getScope(), getContext()); |
| dialog.setPreviewMessage(requestPreviewMessage); |
| int result = dialog.open(); |
| canceled[0] = result != Window.OK; |
| if (requestPreviewMessage != null) { |
| forcePreview[0] = dialog.isForcePreview(); |
| } |
| }); |
| |
| if (canceled[0]) { |
| throw new OperationCanceledException(); |
| } |
| return forcePreview[0]; |
| } |
| |
| /** |
| * Return the synchronization context for the operation or <code>null</code> |
| * if the operation doesn't have one or if it has not yet been created. |
| * By default, the method always returns <code>null</code>. Subclasses may override. |
| * @return the synchronization context for the operation or <code>null</code> |
| */ |
| protected ISynchronizationContext getContext() { |
| return null; |
| } |
| |
| /** |
| * Execute the operation. This method is invoked after the |
| * scope has been generated. |
| * @param monitor a progress monitor |
| * @throws InvocationTargetException |
| * @throws InterruptedException |
| */ |
| protected abstract void execute(IProgressMonitor monitor) throws InvocationTargetException, |
| InterruptedException; |
| |
| /** |
| * Return the scope of this operation. |
| * @return the scope of this operation |
| */ |
| public ISynchronizationScope getScope() { |
| return manager.getScope(); |
| } |
| |
| /** |
| * Return whether a preview of the operation before it is performed is |
| * desired. |
| * @return whether a preview of the operation before it is performed is |
| * desired |
| */ |
| public boolean isPreviewRequested() { |
| return previewRequested; |
| } |
| |
| /** |
| * Return the scope manager for this operation. |
| * @return the scope manager for this operation. |
| */ |
| protected ISynchronizationScopeManager getScopeManager() { |
| return manager; |
| } |
| |
| } |