1 /******************************************************************************* 2 * Copyright (c) 2000, 2017 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 *******************************************************************************/ 14 package org.eclipse.team.core.mapping.provider; 15 16 import java.util.Arrays; 17 import java.util.Collections; 18 import java.util.HashSet; 19 import java.util.Set; 20 21 import org.eclipse.core.resources.IProject; 22 import org.eclipse.core.resources.IResource; 23 import org.eclipse.core.resources.IWorkspace; 24 import org.eclipse.core.resources.IWorkspaceRunnable; 25 import org.eclipse.core.resources.ResourcesPlugin; 26 import org.eclipse.core.resources.mapping.IModelProviderDescriptor; 27 import org.eclipse.core.resources.mapping.ModelProvider; 28 import org.eclipse.core.resources.mapping.ResourceMapping; 29 import org.eclipse.core.resources.mapping.ResourceMappingContext; 30 import org.eclipse.core.resources.mapping.ResourceTraversal; 31 import org.eclipse.core.runtime.CoreException; 32 import org.eclipse.core.runtime.IProgressMonitor; 33 import org.eclipse.core.runtime.PlatformObject; 34 import org.eclipse.core.runtime.jobs.ISchedulingRule; 35 import org.eclipse.core.runtime.jobs.MultiRule; 36 import org.eclipse.team.core.mapping.ISynchronizationScope; 37 import org.eclipse.team.core.mapping.ISynchronizationScopeManager; 38 import org.eclipse.team.core.subscribers.SubscriberScopeManager; 39 import org.eclipse.team.internal.core.Policy; 40 import org.eclipse.team.internal.core.mapping.CompoundResourceTraversal; 41 import org.eclipse.team.internal.core.mapping.ResourceMappingScope; 42 import org.eclipse.team.internal.core.mapping.ScopeChangeEvent; 43 import org.eclipse.team.internal.core.mapping.ScopeManagerEventHandler; 44 45 /** 46 * Class for translating a set of <code>ResourceMapping</code> objects 47 * representing a view selection into the complete set of resources to be 48 * operated on. 49 * <p> 50 * Here's a summary of the scope generation algorithm: 51 * <ol> 52 * <li>Obtain selected mappings 53 * <li>Project mappings onto resources using the appropriate context(s) in 54 * order to obtain a set of ResourceTraverals 55 * <li>Determine what model providers are interested in the targeted resources 56 * <li>From those model providers, obtain the set of affected resource mappings 57 * <li>If the original set is the same as the new set, we are done. 58 * <li>If the set differs from the original selection, rerun the mapping 59 * process for any new mappings 60 * <ul> 61 * <li>Only need to query model providers for mappings for new resources 62 * <li>Keep repeating until no new mappings or resources are added 63 * </ul> 64 * </ol> 65 * <p> 66 * This implementation does not involve participants in the scope management 67 * process. It is up to subclasses that wish to support a longer life cycle for 68 * scopes to provide for participation. For example, the 69 * {@link SubscriberScopeManager} class includes participates in the scope 70 * management process. 71 * 72 * @see org.eclipse.core.resources.mapping.ResourceMapping 73 * @see SubscriberScopeManager 74 * 75 * @since 3.2 76 */ 77 public class SynchronizationScopeManager extends PlatformObject implements ISynchronizationScopeManager { 78 79 private static final int MAX_ITERATION = 10; 80 private final ResourceMappingContext context; 81 private final boolean consultModels; 82 private ISynchronizationScope scope; 83 private boolean initialized; 84 private ScopeManagerEventHandler handler; 85 private final String name; 86 87 /** 88 * Convenience method for obtaining the set of resource 89 * mappings from all model providers that overlap 90 * with the given resources. 91 * @param traversals the resource traversals 92 * @param context the resource mapping context 93 * @param monitor a progress monitor 94 * @return the resource mappings 95 * @throws CoreException if an error occurs 96 */ getMappingsFromProviders(ResourceTraversal[] traversals, ResourceMappingContext context, IProgressMonitor monitor)97 public static ResourceMapping[] getMappingsFromProviders(ResourceTraversal[] traversals, 98 ResourceMappingContext context, 99 IProgressMonitor monitor) throws CoreException { 100 Set<ResourceMapping> result = new HashSet<>(); 101 IModelProviderDescriptor[] descriptors = ModelProvider 102 .getModelProviderDescriptors(); 103 for (IModelProviderDescriptor descriptor : descriptors) { 104 ResourceMapping[] mappings = getMappings(descriptor, traversals, 105 context, monitor); 106 result.addAll(Arrays.asList(mappings)); 107 Policy.checkCanceled(monitor); 108 } 109 return result.toArray(new ResourceMapping[result.size()]); 110 } 111 getMappings(IModelProviderDescriptor descriptor, ResourceTraversal[] traversals, ResourceMappingContext context, IProgressMonitor monitor)112 private static ResourceMapping[] getMappings(IModelProviderDescriptor descriptor, 113 ResourceTraversal[] traversals, 114 ResourceMappingContext context, IProgressMonitor monitor) 115 throws CoreException { 116 ResourceTraversal[] matchingTraversals = descriptor.getMatchingTraversals( 117 traversals); 118 return descriptor.getModelProvider().getMappings(matchingTraversals, 119 context, monitor); 120 } 121 122 /** 123 * Create a scope manager that uses the given context to 124 * determine what resources should be included in the scope. 125 * If <code>consultModels</code> is <code>true</code> then 126 * the model providers will be queried in order to determine if 127 * additional mappings should be included in the scope 128 * @param name the name of the scope 129 * @param inputMappings the input mappings 130 * @param resourceMappingContext a resource mapping context 131 * @param consultModels whether model providers should be consulted 132 */ SynchronizationScopeManager(String name, ResourceMapping[] inputMappings, ResourceMappingContext resourceMappingContext, boolean consultModels)133 public SynchronizationScopeManager(String name, ResourceMapping[] inputMappings, ResourceMappingContext resourceMappingContext, boolean consultModels) { 134 this.name = name; 135 this.context = resourceMappingContext; 136 this.consultModels = consultModels; 137 scope = createScope(inputMappings); 138 } 139 140 @Override isInitialized()141 public boolean isInitialized() { 142 return initialized; 143 } 144 145 /** 146 * Return the scheduling rule that is used when initializing and refreshing 147 * the scope. By default, a rule that covers all projects for the input mappings 148 * of the scope is returned. Subclasses may override. 149 * 150 * @return the scheduling rule that is used when initializing and refreshing 151 * the scope 152 */ getSchedulingRule()153 public ISchedulingRule getSchedulingRule() { 154 Set<IProject> projects = new HashSet<>(); 155 ResourceMapping[] mappings = scope.getInputMappings(); 156 for (ResourceMapping mapping : mappings) { 157 Object modelObject = mapping.getModelObject(); 158 if (modelObject instanceof IResource) { 159 IResource resource = (IResource) modelObject; 160 if (resource.getType() == IResource.ROOT) 161 // If the workspace root is one of the inputs, 162 // then use the workspace root as the rule 163 return ResourcesPlugin.getWorkspace().getRoot(); 164 projects.add(resource.getProject()); 165 } else { 166 // If one of the inputs is not a resource, then use the 167 // root as the rule since we don't know whether projects 168 // can be added or removed 169 return ResourcesPlugin.getWorkspace().getRoot(); 170 } 171 } 172 return MultiRule.combine(projects.toArray(new IProject[projects.size()])); 173 } 174 175 @Override initialize( IProgressMonitor monitor)176 public void initialize( 177 IProgressMonitor monitor) throws CoreException { 178 ResourcesPlugin.getWorkspace().run((IWorkspaceRunnable) this::internalPrepareContext, 179 getSchedulingRule(), IResource.NONE, monitor); 180 } 181 182 @Override refresh(final ResourceMapping[] mappings, IProgressMonitor monitor)183 public ResourceTraversal[] refresh(final ResourceMapping[] mappings, IProgressMonitor monitor) throws CoreException { 184 // We need to lock the workspace when building the scope 185 final ResourceTraversal[][] traversals = new ResourceTraversal[][] { new ResourceTraversal[0] }; 186 IWorkspace workspace = ResourcesPlugin.getWorkspace(); 187 workspace.run((IWorkspaceRunnable) monitor1 -> traversals[0] = internalRefreshScope(mappings, true, monitor1), 188 getSchedulingRule(), IResource.NONE, monitor); 189 return traversals[0]; 190 } 191 internalPrepareContext(IProgressMonitor monitor)192 private void internalPrepareContext(IProgressMonitor monitor) throws CoreException { 193 if (initialized) 194 return; 195 monitor.beginTask(null, IProgressMonitor.UNKNOWN); 196 // Accumulate the initial set of mappings we need traversals for 197 ((ResourceMappingScope)scope).reset(); 198 ResourceMapping[] targetMappings = scope.getInputMappings(); 199 ResourceTraversal[] newTraversals; 200 boolean firstTime = true; 201 boolean hasAdditionalResources = false; 202 int count = 0; 203 do { 204 Policy.checkCanceled(monitor); 205 newTraversals = addMappingsToScope(targetMappings, 206 Policy.subMonitorFor(monitor, IProgressMonitor.UNKNOWN)); 207 if (newTraversals.length > 0 && consultModels) { 208 ResourceTraversal[] adjusted = adjustInputTraversals(newTraversals); 209 targetMappings = getMappingsFromProviders(adjusted, 210 context, 211 Policy.subMonitorFor(monitor, IProgressMonitor.UNKNOWN)); 212 if (firstTime) { 213 firstTime = false; 214 } else if (!hasAdditionalResources) { 215 hasAdditionalResources = newTraversals.length != 0; 216 } 217 } 218 } while (consultModels & newTraversals.length != 0 && count++ < MAX_ITERATION); 219 setHasAdditionalMappings(scope, consultModels && internalHasAdditionalMappings()); 220 setHasAdditionalResources(consultModels && hasAdditionalResources); 221 monitor.done(); 222 initialized = true; 223 fireMappingsChangedEvent(scope.getMappings(), scope.getTraversals()); 224 } 225 internalRefreshScope(ResourceMapping[] mappings, boolean checkForContraction, IProgressMonitor monitor)226 private ResourceTraversal[] internalRefreshScope(ResourceMapping[] mappings, boolean checkForContraction, IProgressMonitor monitor) throws CoreException { 227 monitor.beginTask(null, 100 * mappings.length + 100); 228 ScopeChangeEvent change = new ScopeChangeEvent(scope); 229 CompoundResourceTraversal refreshTraversals = new CompoundResourceTraversal(); 230 CompoundResourceTraversal removedTraversals = new CompoundResourceTraversal(); 231 for (ResourceMapping mapping : mappings) { 232 ResourceTraversal[] previousTraversals = scope.getTraversals(mapping); 233 ResourceTraversal[] mappingTraversals = mapping.getTraversals( 234 context, Policy.subMonitorFor(monitor, 100)); 235 refreshTraversals.addTraversals(mappingTraversals); 236 ResourceTraversal[] uncovered = getUncoveredTraversals(mappingTraversals); 237 if (checkForContraction && previousTraversals != null && previousTraversals.length > 0) { 238 ResourceTraversal[] removed = getUncoveredTraversals(mappingTraversals, previousTraversals); 239 removedTraversals.addTraversals(removed); 240 } 241 if (uncovered.length > 0) { 242 change.setExpanded(true); 243 ResourceTraversal[] result = performExpandScope(mapping, mappingTraversals, uncovered, monitor); 244 refreshTraversals.addTraversals(result); 245 } 246 } 247 248 if (checkForContraction && removedTraversals.getRoots().length > 0) { 249 // The scope may have contracted. The only way to handle this is to recalculate from scratch 250 // TODO: This may not be thread safe 251 ((ResourceMappingScope)scope).reset(); 252 internalRefreshScope(scope.getInputMappings(), false, monitor); 253 change.setContracted(true); 254 } 255 256 if (change.shouldFireChange()) 257 fireMappingsChangedEvent(change.getChangedMappings(), change.getChangedTraversals(refreshTraversals)); 258 monitor.done(); 259 return refreshTraversals.asTraversals(); 260 } 261 getUncoveredTraversals( ResourceTraversal[] newTraversals, ResourceTraversal[] previousTraversals)262 private ResourceTraversal[] getUncoveredTraversals( 263 ResourceTraversal[] newTraversals, 264 ResourceTraversal[] previousTraversals) { 265 CompoundResourceTraversal t = new CompoundResourceTraversal(); 266 t.addTraversals(newTraversals); 267 return t.getUncoveredTraversals(previousTraversals); 268 } 269 performExpandScope( ResourceMapping mapping, ResourceTraversal[] mappingTraversals, ResourceTraversal[] uncovered, IProgressMonitor monitor)270 private ResourceTraversal[] performExpandScope( 271 ResourceMapping mapping, ResourceTraversal[] mappingTraversals, 272 ResourceTraversal[] uncovered, IProgressMonitor monitor) 273 throws CoreException { 274 ResourceMapping ancestor = findAncestor(mapping); 275 if (ancestor == null) { 276 uncovered = addMappingToScope(mapping, mappingTraversals); 277 addResourcesToScope(uncovered, monitor); 278 return mappingTraversals; 279 } else { 280 ResourceTraversal[] ancestorTraversals = ancestor.getTraversals( 281 context, Policy.subMonitorFor(monitor, 100)); 282 uncovered = addMappingToScope(ancestor, ancestorTraversals); 283 addResourcesToScope(uncovered, monitor); 284 return ancestorTraversals; 285 } 286 } 287 findAncestor(ResourceMapping mapping)288 private ResourceMapping findAncestor(ResourceMapping mapping) { 289 ResourceMapping[] mappings = scope.getMappings(mapping.getModelProviderId()); 290 for (ResourceMapping m : mappings) { 291 if (m.contains(mapping)) { 292 return m; 293 } 294 } 295 return null; 296 } 297 getUncoveredTraversals(ResourceTraversal[] traversals)298 private ResourceTraversal[] getUncoveredTraversals(ResourceTraversal[] traversals) { 299 return ((ResourceMappingScope)scope).getCompoundTraversal().getUncoveredTraversals(traversals); 300 } 301 addResourcesToScope(ResourceTraversal[] newTraversals, IProgressMonitor monitor)302 private void addResourcesToScope(ResourceTraversal[] newTraversals, IProgressMonitor monitor) throws CoreException { 303 if (!consultModels) 304 return; 305 ResourceMapping[] targetMappings; 306 int count = 0; 307 do { 308 ResourceTraversal[] adjusted = adjustInputTraversals(newTraversals); 309 targetMappings = getMappingsFromProviders(adjusted, 310 context, Policy.subMonitorFor(monitor, IProgressMonitor.UNKNOWN)); 311 newTraversals = addMappingsToScope(targetMappings, 312 Policy.subMonitorFor(monitor, IProgressMonitor.UNKNOWN)); 313 } while (newTraversals.length != 0 && count++ < MAX_ITERATION); 314 if (!scope.hasAdditionalMappings()) { 315 setHasAdditionalMappings(scope, internalHasAdditionalMappings()); 316 } 317 if (!scope.hasAdditonalResources()) { 318 setHasAdditionalResources(true); 319 } 320 } 321 322 /* 323 * Fire a mappings changed event to any listeners on the scope. 324 * The new mappings are obtained from the scope. 325 * @param originalMappings the original mappings of the scope. 326 */ fireMappingsChangedEvent(ResourceMapping[] newMappings, ResourceTraversal[] newTraversals)327 private void fireMappingsChangedEvent(ResourceMapping[] newMappings, ResourceTraversal[] newTraversals) { 328 ((ResourceMappingScope)scope).fireTraversalsChangedEvent(newTraversals, newMappings); 329 } 330 331 /** 332 * Set whether the scope has additional mappings. This method is not 333 * intended to be overridden. 334 * 335 * @param hasAdditionalMappings a boolean indicating if the scope has 336 * additional mappings 337 */ setHasAdditionalMappings( ISynchronizationScope scope, boolean hasAdditionalMappings)338 protected final void setHasAdditionalMappings( 339 ISynchronizationScope scope, boolean hasAdditionalMappings) { 340 ((ResourceMappingScope)scope).setHasAdditionalMappings(hasAdditionalMappings); 341 } 342 343 /** 344 * Set whether the scope has additional resources. This method is not 345 * intended to be overridden. 346 * 347 * @param hasAdditionalResources a boolean indicating if the scope has 348 * additional resources 349 */ setHasAdditionalResources(boolean hasAdditionalResources)350 protected final void setHasAdditionalResources(boolean hasAdditionalResources) { 351 ((ResourceMappingScope)scope).setHasAdditionalResources(hasAdditionalResources); 352 } 353 354 /** 355 * Create the scope that will be populated and returned by the builder. This 356 * method is not intended to be overridden by clients. 357 * @param inputMappings the input mappings 358 * @return a newly created scope that will be populated and returned by the 359 * builder 360 */ createScope( ResourceMapping[] inputMappings)361 protected final ISynchronizationScope createScope( 362 ResourceMapping[] inputMappings) { 363 return new ResourceMappingScope(inputMappings, this); 364 } 365 366 /** 367 * Adjust the given set of input resources to include any additional 368 * resources required by a particular repository provider for the current 369 * operation. By default the original set is returned but subclasses may 370 * override. Overriding methods should return a set of resources that 371 * include the original resource either explicitly or implicitly as a child 372 * of a returned resource. 373 * <p> 374 * Subclasses may override this method to include additional resources. 375 * 376 * @param traversals the input resource traversals 377 * @return the input resource traversals adjusted to include any additional resources 378 * required for the current operation 379 */ adjustInputTraversals(ResourceTraversal[] traversals)380 protected ResourceTraversal[] adjustInputTraversals(ResourceTraversal[] traversals) { 381 return traversals; 382 } 383 addMappingsToScope( ResourceMapping[] targetMappings, IProgressMonitor monitor)384 private ResourceTraversal[] addMappingsToScope( 385 ResourceMapping[] targetMappings, 386 IProgressMonitor monitor) throws CoreException { 387 CompoundResourceTraversal result = new CompoundResourceTraversal(); 388 ResourceMappingContext context = this.context; 389 for (ResourceMapping mapping : targetMappings) { 390 if (scope.getTraversals(mapping) == null) { 391 ResourceTraversal[] traversals = mapping.getTraversals(context, 392 Policy.subMonitorFor(monitor, 100)); 393 ResourceTraversal[] newOnes = addMappingToScope(mapping, traversals); 394 result.addTraversals(newOnes); 395 } 396 Policy.checkCanceled(monitor); 397 } 398 return result.asTraversals(); 399 } 400 401 /** 402 * Add the mapping and its calculated traversals to the scope. Return the 403 * resources that were not previously covered by the scope. This method 404 * is not intended to be subclassed by clients. 405 * 406 * @param mapping the resource mapping 407 * @param traversals the resource mapping's traversals 408 * @return the resource traversals that were not previously covered by the scope 409 */ addMappingToScope( ResourceMapping mapping, ResourceTraversal[] traversals)410 protected final ResourceTraversal[] addMappingToScope( 411 ResourceMapping mapping, ResourceTraversal[] traversals) { 412 return ((ResourceMappingScope)scope).addMapping(mapping, traversals); 413 } 414 internalHasAdditionalMappings()415 private boolean internalHasAdditionalMappings() { 416 ResourceMapping[] inputMappings = scope.getInputMappings(); 417 ResourceMapping[] mappings = scope.getMappings(); 418 if (inputMappings.length == mappings.length) { 419 Set<ResourceMapping> testSet = new HashSet<>(); 420 Collections.addAll(testSet, mappings); 421 for (ResourceMapping mapping : inputMappings) { 422 if (!testSet.contains(mapping)) { 423 return true; 424 } 425 } 426 return false; 427 } 428 return true; 429 } 430 getContext()431 public ResourceMappingContext getContext() { 432 return context; 433 } 434 435 @Override getScope()436 public ISynchronizationScope getScope() { 437 return scope; 438 } 439 440 @Override dispose()441 public void dispose() { 442 if (handler != null) 443 handler.shutdown(); 444 } 445 446 /** 447 * Refresh the given mappings by recalculating the traversals for the 448 * mappings and adjusting the scope accordingly. 449 * @param mappings the mappings to be refreshed 450 */ refresh(ResourceMapping[] mappings)451 public void refresh(ResourceMapping[] mappings) { 452 getHandler().refresh(mappings); 453 } 454 getHandler()455 private synchronized ScopeManagerEventHandler getHandler() { 456 if (handler == null) 457 handler = new ScopeManagerEventHandler(this); 458 return handler; 459 } 460 461 /** 462 * Returns the human readable name of this manager. The name is never 463 * <code>null</code>. 464 * @return the name associated with this scope manager 465 */ getName()466 public String getName() { 467 return name; 468 } 469 } 470