1 /*******************************************************************************
2  * Copyright (c) 2007, 2020 IBM Corporation and others.
3  * All rights reserved.
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License 2.0 which accompanies this distribution,
6  * 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 implementation and ideas
13  *     Sonatype, Inc. - ongoing development
14  *     RedHat, Inc. - Bug 397216, Bug 460967
15  ******************************************************************************/
16 package org.eclipse.equinox.internal.p2.reconciler.dropins;
17 
18 import java.io.*;
19 import java.net.*;
20 import java.util.*;
21 import java.util.Map.Entry;
22 import org.eclipse.core.runtime.*;
23 import org.eclipse.equinox.internal.p2.core.helpers.*;
24 import org.eclipse.equinox.internal.p2.director.ProfileChangeRequest;
25 import org.eclipse.equinox.internal.p2.extensionlocation.Constants;
26 import org.eclipse.equinox.internal.provisional.configurator.Configurator;
27 import org.eclipse.equinox.internal.provisional.p2.directorywatcher.RepositoryListener;
28 import org.eclipse.equinox.p2.core.IProvisioningAgent;
29 import org.eclipse.equinox.p2.core.ProvisionException;
30 import org.eclipse.equinox.p2.engine.*;
31 import org.eclipse.equinox.p2.engine.query.IUProfilePropertyQuery;
32 import org.eclipse.equinox.p2.metadata.*;
33 import org.eclipse.equinox.p2.planner.IPlanner;
34 import org.eclipse.equinox.p2.planner.ProfileInclusionRules;
35 import org.eclipse.equinox.p2.query.*;
36 import org.eclipse.equinox.p2.repository.IRepository;
37 import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
38 import org.eclipse.equinox.p2.repository.artifact.IFileArtifactRepository;
39 import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
40 import org.eclipse.osgi.service.environment.EnvironmentInfo;
41 import org.eclipse.osgi.util.NLS;
42 import org.osgi.framework.BundleContext;
43 import org.osgi.framework.ServiceReference;
44 
45 /**
46  * Synchronizes a profile with a set of repositories.
47  */
48 public class ProfileSynchronizer {
49 	private static final String RECONCILER_APPLICATION_ID = "org.eclipse.equinox.p2.reconciler.application"; //$NON-NLS-1$
50 	private static final String TIMESTAMPS_FILE_PREFIX = "timestamps"; //$NON-NLS-1$
51 	private static final String PROFILE_TIMESTAMP = "PROFILE"; //$NON-NLS-1$
52 	private static final String NO_TIMESTAMP = "-1"; //$NON-NLS-1$
53 	private static final String PROP_FROM_DROPINS = "org.eclipse.equinox.p2.reconciler.dropins"; //$NON-NLS-1$
54 	private static final String INCLUSION_RULES = "org.eclipse.equinox.p2.internal.inclusion.rules"; //$NON-NLS-1$
55 	private static final String INCLUSION_OPTIONAL = "OPTIONAL"; //$NON-NLS-1$
56 	private static final String INCLUSION_STRICT = "STRICT"; //$NON-NLS-1$
57 
58 	private static final String CACHE_EXTENSIONS = "org.eclipse.equinox.p2.cache.extensions"; //$NON-NLS-1$
59 	private static final String PIPE = "|"; //$NON-NLS-1$
60 	private static final String EXPLANATION = "org.eclipse.equinox.p2.director.explain"; //$NON-NLS-1$
61 
62 	static final String PROP_IGNORE_USER_CONFIGURATION = "eclipse.ignoreUserConfiguration"; //$NON-NLS-1$
63 
64 	final IProfile profile;
65 
66 	final Map<String, IMetadataRepository> repositoryMap;
67 	private Map<String, String> timestamps;
68 	private final IProvisioningAgent agent;
69 
70 	/*
71 	 * Specialized profile change request so we can keep track of IUs which have moved
72 	 * locations on disk.
73 	 */
74 	static class ReconcilerProfileChangeRequest extends ProfileChangeRequest {
75 		List<IInstallableUnit> toMove = new ArrayList<>();
76 
ReconcilerProfileChangeRequest(IProfile profile)77 		public ReconcilerProfileChangeRequest(IProfile profile) {
78 			super(profile);
79 		}
80 
moveAll(Collection<IInstallableUnit> list)81 		void moveAll(Collection<IInstallableUnit> list) {
82 			toMove.addAll(list);
83 		}
84 
getMoves()85 		Collection<IInstallableUnit> getMoves() {
86 			return toMove;
87 		}
88 	}
89 
90 	/*
91 	 * Constructor for the class.
92 	 */
ProfileSynchronizer(IProvisioningAgent agent, IProfile profile, Collection<IMetadataRepository> repositories)93 	public ProfileSynchronizer(IProvisioningAgent agent, IProfile profile, Collection<IMetadataRepository> repositories) {
94 		this.agent = agent;
95 		this.profile = profile;
96 		this.repositoryMap = new HashMap<>();
97 		for (IMetadataRepository repository : repositories) {
98 			repositoryMap.put(repository.getLocation().toString(), repository);
99 		}
100 	}
101 
102 	/*
103 	 * Synchronize the profile with the list of metadata repositories.
104 	 * TODO fix progress monitoring (although in practice the user doesn't see it or have a chance to cancel)
105 	 */
synchronize(IProgressMonitor monitor)106 	public IStatus synchronize(IProgressMonitor monitor) {
107 		readTimestamps();
108 		if (isUpToDate())
109 			return Status.OK_STATUS;
110 
111 		ProvisioningContext context = getContext();
112 		context.setProperty(EXPLANATION, Boolean.valueOf(Tracing.DEBUG_RECONCILER).toString());
113 
114 		String updatedCacheExtensions = synchronizeCacheExtensions();
115 
116 		// figure out if we really have anything to install/uninstall.
117 		ReconcilerProfileChangeRequest request = createProfileChangeRequest(context);
118 		if (request == null) {
119 			if (updatedCacheExtensions == null)
120 				return Status.OK_STATUS;
121 			IStatus engineResult = setProperty(CACHE_EXTENSIONS, updatedCacheExtensions, context, null);
122 			if (engineResult.getSeverity() != IStatus.ERROR && engineResult.getSeverity() != IStatus.CANCEL)
123 				writeTimestamps();
124 			return engineResult;
125 		}
126 		if (updatedCacheExtensions != null)
127 			request.setProfileProperty(CACHE_EXTENSIONS, updatedCacheExtensions);
128 
129 		// if some of the IUs move locations then construct a special plan and execute that first
130 		IStatus moveResult = performRemoveForMovedIUs(request, context, monitor);
131 		if (moveResult.getSeverity() == IStatus.ERROR || moveResult.getSeverity() == IStatus.CANCEL)
132 			return moveResult;
133 
134 		if (!request.getRemovals().isEmpty()) {
135 			Collection<IRequirement> requirements = new ArrayList<>();
136 			for (IInstallableUnit unit : request.getRemovals()) {
137 				IRequirement req = MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, unit.getId(), new VersionRange(unit.getVersion(), true, unit.getVersion(), true), null, 0, 0, false);
138 				requirements.add(req);
139 			}
140 			request.addExtraRequirements(requirements);
141 		}
142 
143 		// now create a plan for the rest of the work and execute it
144 		IStatus addRemoveResult = performAddRemove(request, context, monitor);
145 		if (addRemoveResult.getSeverity() == IStatus.ERROR || addRemoveResult.getSeverity() == IStatus.CANCEL)
146 			return addRemoveResult;
147 
148 		// write out the new timestamps (for caching) and apply the configuration
149 		writeTimestamps();
150 		IStatus applyResult = applyConfiguration(false);
151 
152 		// Mark the state update as hidden so it does not appear in the Installation History UI list
153 		// TODO We need to determine if it is ok to use this copy of the profile.
154 		// See https://bugs.eclipse.org/334670
155 		IProfileRegistry profileRegistry = agent.getService(IProfileRegistry.class);
156 		if (profileRegistry != null) {
157 			IStatus result = profileRegistry.setProfileStateProperty(profile.getProfileId(), profile.getTimestamp(), IProfile.STATE_PROP_HIDDEN, Boolean.TRUE.toString());
158 			if (!result.isOK()) {
159 				// we don't get here but if we do, we will ignore the problem and continue. We
160 				// still want the install operation to succeed. The consequence of this failure is the
161 				// profile state appears in the UI in the Install History page, which isn't horrible.
162 				LogHelper.log(result);
163 			}
164 		}
165 
166 		return applyResult;
167 	}
168 
169 	/*
170 	 * Return a list of the roots in the profile.
171 	 */
getStrictRoots()172 	private IQueryResult<IInstallableUnit> getStrictRoots() {
173 		return profile.query(new IUProfilePropertyQuery(INCLUSION_RULES, INCLUSION_STRICT), null);
174 	}
175 
176 	/*
177 	 * Convert the profile change request into operands and have the engine execute them. There
178 	 * is fancy logic here in case we are trying to remove IUs which are depended on by something
179 	 * which is installed via the UI. Since the bundle has been removed from the file-system it is a forced
180 	 * removal so we have to uninstall the UI-installed IU.
181 	 */
performAddRemove(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor)182 	private IStatus performAddRemove(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor) {
183 		// if we have moves then we have previously removed them.
184 		// now we need to add them back (at the new location)
185 		for (IInstallableUnit iu : request.getMoves()) {
186 			request.add(iu);
187 			request.setInstallableUnitProfileProperty(iu, PROP_FROM_DROPINS, Boolean.TRUE.toString());
188 			request.setInstallableUnitInclusionRules(iu, ProfileInclusionRules.createOptionalInclusionRule(iu));
189 			request.setInstallableUnitProfileProperty(iu, IProfile.PROP_PROFILE_LOCKED_IU, Integer.toString(IProfile.LOCK_UNINSTALL));
190 		}
191 
192 		Collection<IInstallableUnit> additions = request.getAdditions();
193 		Collection<IInstallableUnit> removals = request.getRemovals();
194 		// see if there is any work to do
195 		if (additions.isEmpty() && removals.isEmpty())
196 			return Status.OK_STATUS;
197 
198 		// TODO See bug 270195. Eventually we will attempt to remove strictly installed IUs if their
199 		// dependent bundles have been deleted.
200 		boolean removeStrictRoots = false;
201 		if (removeStrictRoots)
202 			return performStrictRootRemoval(request, context, monitor);
203 		IProvisioningPlan plan = createProvisioningPlan(request, context, monitor);
204 		debug(request, plan);
205 		return executePlan(plan, context, monitor);
206 	}
207 
208 	// TODO re-enable after resolving bug 270195.
performStrictRootRemoval(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor)209 	private IStatus performStrictRootRemoval(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor) {
210 		Collection<IInstallableUnit> removals = request.getRemovals();
211 		// if we don't have any removals then we don't have to worry about potentially
212 		// invalidating things we already have installed, removal of roots, etc so just
213 		// create a regular plan.
214 		if (removals.isEmpty()) {
215 			IProvisioningPlan plan = createProvisioningPlan(request, context, monitor);
216 			debug(request, plan);
217 			return executePlan(plan, context, monitor);
218 		}
219 
220 		// We are now creating a backup of the original request that will be used to create the final plan (where no optional magic is used)
221 		ProfileChangeRequest finalRequest = request.clone();
222 
223 		// otherwise collect the roots, pretend they are optional, and see
224 		// if the resulting plan affects them
225 		Set<IInstallableUnit> strictRoots = getStrictRoots().toUnmodifiableSet();
226 		Collection<IRequirement> forceNegation = new ArrayList<>(removals.size());
227 		for (IInstallableUnit iu : removals)
228 			forceNegation.add(createNegation(iu));
229 		request.addExtraRequirements(forceNegation);
230 
231 		// set all the profile roots to be optional to see how they would be effected by the plan
232 		for (IInstallableUnit iu : strictRoots)
233 			request.setInstallableUnitProfileProperty(iu, INCLUSION_RULES, INCLUSION_OPTIONAL);
234 
235 		// get the tentative plan back from the planner
236 		IProvisioningPlan plan = createProvisioningPlan(request, context, monitor);
237 		debug(request, plan);
238 		if (!plan.getStatus().isOK())
239 			return plan.getStatus();
240 
241 		// Analyze the plan to see if any of the strict roots are being uninstalled.
242 		int removedRoots = 0;
243 		for (IInstallableUnit initialRoot : strictRoots) {
244 			// if the root wasn't uninstalled, then continue
245 			if (plan.getRemovals().query(QueryUtil.createIUQuery(initialRoot), null).isEmpty())
246 				continue;
247 			// otherwise add its removal to the change request, along with a negation and
248 			// change of strict to optional for their inclusion rule.
249 			finalRequest.remove(initialRoot);
250 			finalRequest.setInstallableUnitProfileProperty(initialRoot, INCLUSION_RULES, INCLUSION_OPTIONAL);
251 			IRequirement negation = createNegation(initialRoot);
252 			Collection<IRequirement> extra = new ArrayList<>();
253 			extra.add(negation);
254 			request.addExtraRequirements(extra);
255 			LogHelper.log(new Status(IStatus.INFO, Activator.ID, NLS.bind(Messages.remove_root, initialRoot.getId(), initialRoot.getVersion())));
256 			removedRoots++;
257 		}
258 
259 		// Check for the case where all the strict roots are being removed.
260 		if (removedRoots == strictRoots.size())
261 			return new Status(IStatus.ERROR, Activator.ID, Messages.remove_all_roots);
262 		plan = createProvisioningPlan(finalRequest, context, monitor);
263 		if (!plan.getStatus().isOK()) {
264 			System.out.println("original request"); //$NON-NLS-1$
265 			System.out.println(request);
266 			System.out.println("final request"); //$NON-NLS-1$
267 			System.out.println(finalRequest);
268 			throw new IllegalStateException("The second plan is not resolvable."); //$NON-NLS-1$
269 		}
270 
271 		// execute the plan and return the status
272 		return executePlan(plan, context, monitor);
273 	}
274 
275 	/*
276 	 * If the request contains IUs to be moved then create and execute a plan which
277 	 * removes them. Otherwise just return.
278 	 */
performRemoveForMovedIUs(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor)279 	private IStatus performRemoveForMovedIUs(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor) {
280 		Collection<IInstallableUnit> moves = request.getMoves();
281 		if (moves.isEmpty())
282 			return Status.OK_STATUS;
283 		IEngine engine = agent.getService(IEngine.class);
284 		IProvisioningPlan plan = engine.createPlan(profile, context);
285 		for (IInstallableUnit unit : moves)
286 			plan.removeInstallableUnit(unit);
287 		return executePlan(plan, context, monitor);
288 	}
289 
290 	/*
291 	 * Write out the timestamps of various repositories and folders/file to help
292 	 * us cache and detect cases where we don't have to perform a reconciliation.
293 	 */
writeTimestamps()294 	private void writeTimestamps() {
295 		timestamps.clear();
296 		timestamps.put(PROFILE_TIMESTAMP, Long.toString(profile.getTimestamp()));
297 		for (Entry<String, IMetadataRepository> entry : repositoryMap.entrySet()) {
298 			IMetadataRepository repository = entry.getValue();
299 			Map<String, String> props = repository.getProperties();
300 			String timestamp = null;
301 			if (props != null)
302 				timestamp = props.get(IRepository.PROP_TIMESTAMP);
303 			if (timestamp == null)
304 				timestamp = NO_TIMESTAMP;
305 
306 			timestamps.put(entry.getKey(), timestamp);
307 		}
308 
309 		try {
310 			File file = Activator.getContext().getDataFile(TIMESTAMPS_FILE_PREFIX + profile.getProfileId().hashCode());
311 			Activator.trace("Writing timestamp file to : " + file.getAbsolutePath()); //$NON-NLS-1$
312 			try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
313 				CollectionUtils.storeProperties(timestamps, os, "Timestamps for " + profile.getProfileId()); //$NON-NLS-1$
314 				if (Tracing.DEBUG_RECONCILER) {
315 					for (String key : timestamps.keySet()) {
316 						Object value = timestamps.get(key);
317 						Activator.trace(key + '=' + value);
318 					}
319 				}
320 			}
321 		} catch (FileNotFoundException e) {
322 			//Ignore
323 		} catch (IOException e) {
324 			//Ignore
325 		}
326 	}
327 
328 	/*
329 	 * Check timestamps and return true if the profile is considered to be up-to-date or
330 	 * false if we should perform a reconciliation.
331 	 */
isUpToDate()332 	private boolean isUpToDate() {
333 		// the user might want to force a reconciliation
334 		if ("true".equals(Activator.getContext().getProperty("osgi.checkConfiguration"))) { //$NON-NLS-1$//$NON-NLS-2$
335 			Activator.trace("User requested forced reconciliation via \"osgi.checkConfiguration=true\" System property."); //$NON-NLS-1$
336 			Activator.trace("Performing reconciliation."); //$NON-NLS-1$
337 			return false;
338 		}
339 
340 		String lastKnownProfileTimeStamp = timestamps.remove(PROFILE_TIMESTAMP);
341 		if (lastKnownProfileTimeStamp == null) {
342 			Activator.trace("Profile timestamp not found in cache."); //$NON-NLS-1$
343 			Activator.trace("Performing reconciliation."); //$NON-NLS-1$
344 			return false;
345 		}
346 		String currentProfileTimestamp = Long.toString(profile.getTimestamp());
347 		if (!lastKnownProfileTimeStamp.equals(currentProfileTimestamp)) {
348 			Activator.trace("Profile timestamps not equal, expected: " + lastKnownProfileTimeStamp + ", actual=" + currentProfileTimestamp); //$NON-NLS-1$ //$NON-NLS-2$
349 			Activator.trace("Performing reconciliation."); //$NON-NLS-1$
350 			return false;
351 		}
352 
353 		//When we get here the timestamps map only contains information related to repos
354 		for (Entry<String, IMetadataRepository> entry : repositoryMap.entrySet()) {
355 			IMetadataRepository repository = entry.getValue();
356 
357 			Map<String, String> props = repository.getProperties();
358 			String currentTimestamp = null;
359 			if (props != null)
360 				currentTimestamp = props.get(IRepository.PROP_TIMESTAMP);
361 
362 			if (currentTimestamp == null)
363 				currentTimestamp = NO_TIMESTAMP;
364 
365 			String key = entry.getKey();
366 			String lastKnownTimestamp = timestamps.remove(key);
367 			//A repo has been added
368 			if (lastKnownTimestamp == null) {
369 				Activator.trace("No cached timestamp found for: " + key); //$NON-NLS-1$
370 				Activator.trace("Performing reconciliation."); //$NON-NLS-1$
371 				return false;
372 			}
373 			if (!lastKnownTimestamp.equals(currentTimestamp)) {
374 				Activator.trace("Timestamps not equal for file: " + key + ", expected: " + lastKnownTimestamp + ", actual: " + currentTimestamp); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
375 				Activator.trace("Performing reconciliation."); //$NON-NLS-1$
376 				return false;
377 			}
378 		}
379 		if (timestamps.size() == 0) {
380 			Activator.trace("Timestamps valid."); //$NON-NLS-1$
381 			Activator.trace("Skipping reconciliation."); //$NON-NLS-1$
382 			return true;
383 		}
384 
385 		//A repo has been removed
386 		if (Tracing.DEBUG_RECONCILER) {
387 			Activator.trace("Extra values in timestamp file:"); //$NON-NLS-1$
388 			for (String string : timestamps.keySet())
389 				Activator.trace(string);
390 			Activator.trace("Performing reconciliation."); //$NON-NLS-1$
391 		}
392 		return false;
393 	}
394 
395 	/*
396 	 * Read the values of the stored timestamps that we use for caching.
397 	 */
readTimestamps()398 	private void readTimestamps() {
399 		if (Boolean.TRUE.toString().equalsIgnoreCase(System.getProperty(PROP_IGNORE_USER_CONFIGURATION))) {
400 			timestamps = new HashMap<>();
401 			Activator.trace("Master profile changed."); //$NON-NLS-1$
402 			Activator.trace("Performing reconciliation."); //$NON-NLS-1$
403 			return;
404 		}
405 		File file = Activator.getContext().getDataFile(TIMESTAMPS_FILE_PREFIX + profile.getProfileId().hashCode());
406 		try {
407 			try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
408 				timestamps = CollectionUtils.loadProperties(is);
409 			}
410 		} catch (FileNotFoundException e) {
411 			//Ignore
412 			timestamps = new HashMap<>();
413 			Activator.trace("Timestamp file does not exist."); //$NON-NLS-1$
414 			Activator.trace("Performing reconciliation."); //$NON-NLS-1$
415 		} catch (IOException e) {
416 			//Ignore
417 			timestamps = new HashMap<>();
418 			Activator.trace("Exception loading timestamp file: " + e.getMessage()); //$NON-NLS-1$
419 			Activator.trace("Performing reconciliation."); //$NON-NLS-1$
420 		}
421 	}
422 
getContext()423 	private ProvisioningContext getContext() {
424 		ArrayList<URI> repoURLs = new ArrayList<>();
425 		for (String string : repositoryMap.keySet()) {
426 			try {
427 				repoURLs.add(new URI(string));
428 			} catch (URISyntaxException e) {
429 				//ignore
430 			}
431 		}
432 		ProvisioningContext result = new ProvisioningContext(agent);
433 		result.setMetadataRepositories(repoURLs.toArray(new URI[repoURLs.size()]));
434 		result.setArtifactRepositories(new URI[0]);
435 		return result;
436 	}
437 
synchronizeCacheExtensions()438 	private String synchronizeCacheExtensions() {
439 		List<String> currentExtensions = new ArrayList<>();
440 		StringBuilder buffer = new StringBuilder();
441 
442 		List<String> repositories = new ArrayList<>(repositoryMap.keySet());
443 		URL installArea = Activator.getOSGiInstallArea();
444 		final String OSGiInstallArea;
445 		try {
446 			// The OSGi install area is an unencoded URL and repository locations are encoded URIs
447 			// so make them the same so we can compare them.
448 			// See https://bugs.eclipse.org/346565.
449 			OSGiInstallArea = URIUtil.toURI(installArea).toString() + Constants.EXTENSION_LOCATION;
450 			// Sort the repositories so the extension location at the OSGi install folder is first.
451 			// See https://bugs.eclipse.org/246310.
452 			repositories.sort((left, right) -> {
453 				if (OSGiInstallArea.equals(left))
454 					return -1;
455 				if (OSGiInstallArea.equals(right))
456 					return 1;
457 				return left.compareTo(right);
458 			});
459 		} catch (URISyntaxException e) {
460 			// This shouldn't happen but if it does we will log the error and continue
461 			// with the repositories in the default order.
462 			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Unable to convert OSGi install area: " + installArea + " into URI.", e)); //$NON-NLS-1$ //$NON-NLS-2$
463 		}
464 		for (Iterator<String> it = repositories.iterator(); it.hasNext();) {
465 			String repositoryId = it.next();
466 			try {
467 				IArtifactRepository repository = Activator.loadArtifactRepository(new URI(repositoryId), null);
468 				if (repository instanceof IFileArtifactRepository) {
469 					currentExtensions.add(escapePipe(repositoryId));
470 					buffer.append(repositoryId);
471 					if (it.hasNext())
472 						buffer.append(PIPE);
473 				}
474 			} catch (ProvisionException e) {
475 				// ignore
476 			} catch (URISyntaxException e) {
477 				// unexpected
478 				e.printStackTrace();
479 			}
480 		}
481 		String currentExtensionsProperty = (buffer.length() == 0) ? null : buffer.toString();
482 
483 		List<String> previousExtensions = new ArrayList<>();
484 		String previousExtensionsProperty = profile.getProperty(CACHE_EXTENSIONS);
485 		if (previousExtensionsProperty != null) {
486 			StringTokenizer tokenizer = new StringTokenizer(previousExtensionsProperty, PIPE);
487 			while (tokenizer.hasMoreTokens()) {
488 				previousExtensions.add(tokenizer.nextToken());
489 			}
490 		}
491 
492 		if (previousExtensions.size() == currentExtensions.size() && previousExtensions.containsAll(currentExtensions))
493 			return null;
494 
495 		return currentExtensionsProperty;
496 	}
497 
498 	/**
499 	 * Escapes the pipe ('|') character in a URI using the standard URI escape sequence.
500 	 * This is done because the pipe character is used as the delimiter between locations
501 	 * in the cache extensions profile property.
502 	 */
escapePipe(String location)503 	private String escapePipe(String location) {
504 		String result = location;
505 		int pipeIndex;
506 		while ((pipeIndex = result.indexOf(',')) != -1)
507 			result = result.substring(0, pipeIndex) + "%7C" + result.substring(pipeIndex + 1); //$NON-NLS-1$
508 		return result;
509 	}
510 
511 	/*
512 	 * Return a map of all the IUs in the profile
513 	 * Use a map here so we have a copy of the original IU from the profile... we will need it later.
514 	 */
getProfileIUs()515 	private Map<IInstallableUnit, IInstallableUnit> getProfileIUs() {
516 		IQueryResult<IInstallableUnit> profileQueryResult = profile.query(QueryUtil.createIUAnyQuery(), null);
517 		Map<IInstallableUnit, IInstallableUnit> result = new HashMap<>();
518 		for (IInstallableUnit iu : profileQueryResult) {
519 			result.put(iu, iu);
520 		}
521 		return result;
522 	}
523 
524 	/*
525 	 * Return a map of all the IUs available in the profile. This takes the shared parents into consideration, if applicable.
526 	 * Use a map here so we have a copy of the original IU from the profile... we will need it later.
527 	 */
getAvailableProfileIUs()528 	private Map<IInstallableUnit, IInstallableUnit> getAvailableProfileIUs() {
529 		IQueryResult<IInstallableUnit> profileQueryResult = profile.available(QueryUtil.createIUAnyQuery(), null);
530 		Map<IInstallableUnit, IInstallableUnit> result = new HashMap<>();
531 		for (IInstallableUnit iu : profileQueryResult) {
532 			result.put(iu, iu);
533 		}
534 		return result;
535 	}
536 
537 	/*
538 	 * Return the profile change requests that we need to execute in order to install everything from the
539 	 * dropins folder(s). (or uninstall things that have been removed) We use a collection here because if
540 	 * the user has moved bundles from the dropins to the plugins (for instance) then we need to uninstall
541 	 * the old bundle and then re-install the new one. This is because the IUs for the moved bundles are
542 	 * considered the same but they really differ in an IU property. (file location, which is not considered
543 	 * as part of equality)
544 	 */
createProfileChangeRequest(ProvisioningContext context)545 	public ReconcilerProfileChangeRequest createProfileChangeRequest(ProvisioningContext context) {
546 		ReconcilerProfileChangeRequest request = new ReconcilerProfileChangeRequest(profile);
547 
548 		boolean resolve = Boolean.parseBoolean(profile.getProperty("org.eclipse.equinox.p2.resolve")); //$NON-NLS-1$
549 		if (resolve)
550 			request.removeProfileProperty("org.eclipse.equinox.p2.resolve"); //$NON-NLS-1$
551 
552 		List<IInstallableUnit> toRemove = new ArrayList<>();
553 		List<IInstallableUnit> toMove = new ArrayList<>();
554 
555 		boolean foundIUsToAdd = false;
556 		Map<IInstallableUnit, IInstallableUnit> profileIUs = getProfileIUs();
557 
558 		// we use IProfile.available(...) here so that we also gather any shared IUs
559 		Map<IInstallableUnit, IInstallableUnit> availableProfileIUs = getAvailableProfileIUs();
560 
561 		// get all IUs from all our repos
562 		IQueryResult<IInstallableUnit> allIUs = getAllIUsFromRepos();
563 		for (Iterator<IInstallableUnit> iter = allIUs.iterator(); iter.hasNext();) {
564 			final IInstallableUnit iu = iter.next();
565 			IInstallableUnit existing = profileIUs.get(iu);
566 			// check to see if this IU has moved locations
567 			if (existing != null) {
568 				// if the IU is already installed in the profile then check to see if it was moved.
569 				String one = iu.getProperty(RepositoryListener.FILE_NAME);
570 				String two = existing.getProperty(RepositoryListener.FILE_NAME);
571 				// cheat here... since we always set the filename property for bundles in the dropins,
572 				// if the existing IU's filename is null then it isn't from the dropins. a better
573 				// (and more expensive) way to find this out is to do an IU profile property query.
574 				if (two == null) {
575 					// the IU is already installed so don't mark it as a dropin now - see bug 404619.
576 					iter.remove();
577 					continue;
578 				}
579 				// if we have an IU which has been moved, keep track of it.
580 				if (one != null && !one.equals(two)) {
581 					toMove.add(iu);
582 					continue;
583 				}
584 			}
585 			// even though we are adding all IUs below, we need to explicitly set the properties for
586 			// them as well. Do that here.
587 			if (QueryUtil.isGroup(iu))
588 				request.setInstallableUnitProfileProperty(iu, IProfile.PROP_PROFILE_ROOT_IU, Boolean.TRUE.toString());
589 			// mark all IUs with special property
590 			request.setInstallableUnitProfileProperty(iu, PROP_FROM_DROPINS, Boolean.TRUE.toString());
591 			request.setInstallableUnitInclusionRules(iu, ProfileInclusionRules.createOptionalInclusionRule(iu));
592 			request.setInstallableUnitProfileProperty(iu, IProfile.PROP_PROFILE_LOCKED_IU, Integer.toString(IProfile.LOCK_UNINSTALL));
593 
594 			// as soon as we find something locally that needs to be installed, then
595 			// everything from the parent's dropins must be installed locally as well.
596 			if (!foundIUsToAdd && availableProfileIUs.get(iu) == null) {
597 				foundIUsToAdd = true;
598 			}
599 		}
600 
601 		// get all IUs from profile with marked property (existing)
602 		IQueryResult<IInstallableUnit> dropinIUs = profile.query(new IUProfilePropertyQuery(PROP_FROM_DROPINS, Boolean.TRUE.toString()), null);
603 		Set<IInstallableUnit> all = allIUs.toUnmodifiableSet();
604 		for (IInstallableUnit iu : dropinIUs) {
605 			// the STRICT policy is set when we install things via the UI, we use it to differentiate between IUs installed
606 			// via the dropins and the UI. (dropins are considered optional) If an IU has both properties set it means that
607 			// it was initially installed via the dropins but then upgraded via the UI. (properties are copied from the old IU
608 			// to the new IU during an upgrade) In this case we want to remove the "from dropins" property so the upgrade
609 			// will stick.
610 			if ("STRICT".equals(profile.getInstallableUnitProperty(iu, "org.eclipse.equinox.p2.internal.inclusion.rules"))) { //$NON-NLS-1$//$NON-NLS-2$
611 				request.removeInstallableUnitProfileProperty(iu, PROP_FROM_DROPINS);
612 				request.removeInstallableUnitProfileProperty(iu, IProfile.PROP_PROFILE_LOCKED_IU);
613 				continue;
614 			}
615 			// if the IU from the profile is in the "all available" list, then it is already added
616 			// otherwise if it isn't in the repo then we have to remove it from the profile.
617 			if (!all.contains(iu))
618 				toRemove.add(iu);
619 		}
620 
621 		if (!foundIUsToAdd && toRemove.isEmpty() && !resolve && toMove.isEmpty()) {
622 			if (Tracing.DEBUG_RECONCILER)
623 				Tracing.debug("[reconciler] Nothing to do."); //$NON-NLS-1$
624 			return null;
625 		}
626 
627 		// everything from the drop-ins must be considered for addition/removal everytime so add all here
628 		request.addAll(all);
629 		request.removeAll(toRemove);
630 		request.moveAll(toMove);
631 
632 		debug(request);
633 		return request;
634 	}
635 
636 	/*
637 	 * Create and return a negated requirement saying that the given IU must not exist in the profile.
638 	 */
createNegation(IInstallableUnit unit)639 	private IRequirement createNegation(IInstallableUnit unit) {
640 		return MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, unit.getId(), //
641 				new VersionRange(unit.getVersion(), true, unit.getVersion(), true), null, 0, 0, false);
642 	}
643 
644 	/*
645 	 * If in debug mode, print out information which tells us whether or not the given
646 	 * provisioning plan matches the request.
647 	 */
debug(ReconcilerProfileChangeRequest request, IProvisioningPlan plan)648 	private void debug(ReconcilerProfileChangeRequest request, IProvisioningPlan plan) {
649 		if (!Tracing.DEBUG_RECONCILER)
650 			return;
651 		final String PREFIX = "[reconciler] [plan] "; //$NON-NLS-1$
652 		// get the request
653 		List<IInstallableUnit> toAdd = new ArrayList<>(request.getAdditions());
654 		List<IInstallableUnit> toRemove = new ArrayList<>(request.getRemovals());
655 		List<IInstallableUnit> toMove = new ArrayList<>(request.getMoves());
656 
657 		// remove from the request everything that is in the plan
658 		for (IInstallableUnit iu : plan.getRemovals().query(QueryUtil.createIUAnyQuery(), null)) {
659 			if (!toRemove.remove(iu)) {
660 				Tracing.debug(PREFIX + iu + " will be removed"); //$NON-NLS-1$
661 			}
662 		}
663 		for (IInstallableUnit iu : plan.getAdditions().query(QueryUtil.createIUAnyQuery(), null)) {
664 			if (!toAdd.remove(iu)) {
665 				Tracing.debug(PREFIX + iu + " will be added"); //$NON-NLS-1$
666 			}
667 		}
668 		// Move operations are treated as doing a remove/add. The removes have already happened
669 		// and at this point we are adding the moved IUs back at their new location. Remove the moved
670 		// IUs from the added list because this will just confuse the user.
671 		toAdd.removeAll(toMove);
672 
673 		// if anything is left in the request, then something is wrong with the plan
674 		if (toAdd.size() == 0 && toRemove.size() == 0)
675 			Tracing.debug(PREFIX + "Plan matches the request."); //$NON-NLS-1$
676 		if (toAdd.size() != 0) {
677 			Tracing.debug(PREFIX + "Some units will not be installed, because they are already installed or there are dependency issues:"); //$NON-NLS-1$
678 			for (IInstallableUnit unit : toAdd)
679 				Tracing.debug(PREFIX + unit);
680 		}
681 		if (toRemove.size() != 0) {
682 			Tracing.debug(PREFIX + "Some units will not be uninstalled:"); //$NON-NLS-1$
683 			for (IInstallableUnit unit : toRemove)
684 				Tracing.debug(PREFIX + unit);
685 		}
686 	}
687 
688 	/*
689 	 * If debugging is turned on, then print out the details for the given profile change request.
690 	 */
debug(ReconcilerProfileChangeRequest request)691 	private void debug(ReconcilerProfileChangeRequest request) {
692 		if (!Tracing.DEBUG_RECONCILER)
693 			return;
694 		final String PREFIX = "[reconciler] "; //$NON-NLS-1$
695 		Collection<IInstallableUnit> toAdd = request.getAdditions();
696 		if (toAdd == null || toAdd.size() == 0) {
697 			Tracing.debug(PREFIX + "No installable units to add."); //$NON-NLS-1$
698 		} else {
699 			for (IInstallableUnit add : toAdd) {
700 				Tracing.debug(PREFIX + "Adding IU: " + add.getId() + ' ' + add.getVersion()); //$NON-NLS-1$
701 			}
702 		}
703 		Map<IInstallableUnit, Map<String, String>> propsToAdd = request.getInstallableUnitProfilePropertiesToAdd();
704 		if (propsToAdd == null || propsToAdd.isEmpty()) {
705 			Tracing.debug(PREFIX + "No IU properties to add."); //$NON-NLS-1$
706 		} else {
707 			for (Entry<IInstallableUnit, Map<String, String>> entry : propsToAdd.entrySet()) {
708 				Tracing.debug(PREFIX + "Adding IU property: " + entry.getKey() + "->" + entry.getValue()); //$NON-NLS-1$ //$NON-NLS-2$
709 			}
710 		}
711 
712 		Collection<IInstallableUnit> toRemove = request.getRemovals();
713 		if (toRemove == null || toRemove.size() == 0) {
714 			Tracing.debug(PREFIX + "No installable units to remove."); //$NON-NLS-1$
715 		} else {
716 			for (IInstallableUnit remove : toRemove) {
717 				Tracing.debug(PREFIX + "Removing IU: " + remove.getId() + ' ' + remove.getVersion()); //$NON-NLS-1$
718 			}
719 		}
720 		Map<IInstallableUnit, List<String>> propsToRemove = request.getInstallableUnitProfilePropertiesToRemove();
721 		if (propsToRemove == null || propsToRemove.isEmpty()) {
722 			Tracing.debug(PREFIX + "No IU properties to remove."); //$NON-NLS-1$
723 		} else {
724 			for (Entry<IInstallableUnit, List<String>> entry : propsToRemove.entrySet()) {
725 				Tracing.debug(PREFIX + "Removing IU property: " + entry.getKey() + "->" + entry.getValue()); //$NON-NLS-1$ //$NON-NLS-2$
726 			}
727 		}
728 
729 		Collection<IInstallableUnit> toMove = request.getMoves();
730 		if (toMove == null || toMove.isEmpty()) {
731 			Tracing.debug(PREFIX + "No installable units to move."); //$NON-NLS-1$
732 		} else {
733 			for (IInstallableUnit move : toMove)
734 				Tracing.debug(PREFIX + "Moving IU: " + move.getId() + ' ' + move.getVersion()); //$NON-NLS-1$
735 		}
736 
737 		Collection<IRequirement> extra = request.getExtraRequirements();
738 		if (extra == null || extra.isEmpty()) {
739 			Tracing.debug(PREFIX + "No extra requirements."); //$NON-NLS-1$
740 		} else {
741 			for (IRequirement requirement : extra)
742 				Tracing.debug(PREFIX + "Extra requirement: " + requirement); //$NON-NLS-1$
743 		}
744 	}
745 
746 	/*
747 	 * Return all of the IUs available in all of our repos. This usually includes the dropins and plugins folders
748 	 * as well as any sites specified in the platform.xml file.
749 	 */
getAllIUsFromRepos()750 	private IQueryResult<IInstallableUnit> getAllIUsFromRepos() {
751 		// TODO: Should consider using a sequenced iterator here instead of collecting
752 		Collector<IInstallableUnit> allRepos = new Collector<>();
753 		for (IMetadataRepository repository : repositoryMap.values()) {
754 			allRepos.addAll(repository.query(QueryUtil.createIUAnyQuery(), null));
755 		}
756 		return allRepos;
757 	}
758 
759 	/*
760 	 * Create and return a provisioning plan for the given change request.
761 	 */
createProvisioningPlan(ProfileChangeRequest request, ProvisioningContext provisioningContext, IProgressMonitor monitor)762 	private IProvisioningPlan createProvisioningPlan(ProfileChangeRequest request, ProvisioningContext provisioningContext, IProgressMonitor monitor) {
763 		IPlanner planner = agent.getService(IPlanner.class);
764 		return planner.getProvisioningPlan(request, provisioningContext, monitor);
765 	}
766 
767 	/*
768 	 * Call the engine to set the given property on the profile.
769 	 */
setProperty(String key, String value, ProvisioningContext provisioningContext, IProgressMonitor monitor)770 	private IStatus setProperty(String key, String value, ProvisioningContext provisioningContext, IProgressMonitor monitor) {
771 		IEngine engine = agent.getService(IEngine.class);
772 		IProvisioningPlan plan = engine.createPlan(profile, provisioningContext);
773 		plan.setProfileProperty(key, value);
774 		IPhaseSet phaseSet = PhaseSetFactory.createPhaseSetIncluding(new String[] {PhaseSetFactory.PHASE_PROPERTY});
775 		return engine.perform(plan, phaseSet, monitor);
776 	}
777 
778 	/*
779 	 * Execute the given plan.
780 	 */
executePlan(IProvisioningPlan plan, ProvisioningContext provisioningContext, IProgressMonitor monitor)781 	private IStatus executePlan(IProvisioningPlan plan, ProvisioningContext provisioningContext, IProgressMonitor monitor) {
782 		IEngine engine = agent.getService(IEngine.class);
783 		IPhaseSet phaseSet = PhaseSetFactory.createDefaultPhaseSetExcluding(new String[] {PhaseSetFactory.PHASE_COLLECT, PhaseSetFactory.PHASE_CHECK_TRUST});
784 
785 		if (plan.getInstallerPlan() != null) {
786 			IStatus installerPlanStatus = engine.perform(plan.getInstallerPlan(), phaseSet, monitor);
787 			if (!installerPlanStatus.isOK())
788 				return installerPlanStatus;
789 
790 			applyConfiguration(true);
791 		}
792 		return engine.perform(plan, phaseSet, monitor);
793 	}
794 
795 	/*
796 	 * Write out the configuration file.
797 	 */
applyConfiguration(boolean isInstaller)798 	private IStatus applyConfiguration(boolean isInstaller) {
799 		if (!isInstaller && isReconciliationApplicationRunning())
800 			return Status.OK_STATUS;
801 		BundleContext context = Activator.getContext();
802 		ServiceReference<Configurator> reference = context.getServiceReference(Configurator.class);
803 		Configurator configurator = context.getService(reference);
804 		try {
805 			configurator.applyConfiguration();
806 		} catch (IOException e) {
807 			return new Status(IStatus.ERROR, Activator.ID, "Unexpected failure applying configuration", e); //$NON-NLS-1$
808 		} finally {
809 			context.ungetService(reference);
810 		}
811 		return Status.OK_STATUS;
812 	}
813 
isReconciliationApplicationRunning()814 	static boolean isReconciliationApplicationRunning() {
815 		EnvironmentInfo info = ServiceHelper.getService(Activator.getContext(), EnvironmentInfo.class);
816 		if (info == null)
817 			return false;
818 		String[] args = info.getCommandLineArgs();
819 		if (args == null)
820 			return false;
821 		for (String arg : args) {
822 			if (arg != null && RECONCILER_APPLICATION_ID.equals(arg.trim()))
823 				return true;
824 		}
825 		return false;
826 	}
827 }
828