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