1 /*******************************************************************************
2  * Copyright (c) 2007, 2018 IBM Corporation and others.
3  *
4  * This
5  * program and the accompanying materials are made available under the terms of
6  * the Eclipse Public License 2.0 which accompanies this distribution, and is
7  * available at
8  * https://www.eclipse.org/legal/epl-2.0/
9  *
10  * SPDX-License-Identifier: EPL-2.0
11  *
12  * Contributors:
13  *     IBM Corporation - initial API and implementation
14  *     Ericsson AB - ongoing development
15  *     Red Hat, Inc. - fragments support added, Bug 460967
16  ******************************************************************************/
17 package org.eclipse.equinox.internal.p2.engine;
18 
19 import java.io.*;
20 import java.lang.ref.SoftReference;
21 import java.net.URI;
22 import java.util.*;
23 import java.util.Map.Entry;
24 import java.util.zip.GZIPInputStream;
25 import java.util.zip.GZIPOutputStream;
26 import javax.xml.parsers.ParserConfigurationException;
27 import org.eclipse.core.runtime.*;
28 import org.eclipse.core.runtime.jobs.Job;
29 import org.eclipse.equinox.internal.p2.core.helpers.*;
30 import org.eclipse.equinox.internal.p2.metadata.TranslationSupport;
31 import org.eclipse.equinox.internal.provisional.p2.core.eventbus.IProvisioningEventBus;
32 import org.eclipse.equinox.p2.core.*;
33 import org.eclipse.equinox.p2.core.spi.IAgentService;
34 import org.eclipse.equinox.p2.engine.*;
35 import org.eclipse.equinox.p2.metadata.*;
36 import org.eclipse.equinox.p2.query.IQueryResult;
37 import org.eclipse.equinox.p2.query.QueryUtil;
38 import org.eclipse.osgi.service.datalocation.Location;
39 import org.eclipse.osgi.util.NLS;
40 import org.osgi.framework.BundleContext;
41 import org.osgi.framework.ServiceReference;
42 import org.xml.sax.InputSource;
43 import org.xml.sax.SAXException;
44 
45 public class SimpleProfileRegistry implements IProfileRegistry, IAgentService {
46 
47 	private static final String SIMPLE_PROFILE_REGISTRY_INTERNAL = "_simpleProfileRegistry_internal_"; //$NON-NLS-1$
48 	private static final String PROFILE_REGISTRY = "profile registry"; //$NON-NLS-1$
49 	private static final String PROFILE_PROPERTIES_FILE = "state.properties"; //$NON-NLS-1$
50 
51 	private static final String PROFILE_EXT = ".profile"; //$NON-NLS-1$
52 	private static final String PROFILE_GZ_EXT = ".profile.gz"; //$NON-NLS-1$
53 	public static final String DEFAULT_STORAGE_DIR = "profileRegistry"; //$NON-NLS-1$
54 	private static final String DATA_EXT = ".data"; //$NON-NLS-1$
55 
56 	//Internal constant used to keep track of the newly created timestamp
57 	private static final String SERVICE_SHARED_INSTALL_NEW_TIMESTAMP = IProfileRegistry.class.getName() + '_' + "NEW_SELF_TIMESTAMP"; //$NON-NLS-1$
58 
59 	protected final IProvisioningAgent agent;
60 
61 	/**
62 	 * Reference to Map of String(Profile id)->Profile.
63 	 */
64 	private SoftReference<Map<String, Profile>> profiles;
65 	private Map<String, ProfileLock> profileLocks = new HashMap<>();
66 
67 	private String self;
68 
69 	//Whether the registry should update the self profile when the registry is restored
70 	private boolean updateSelfProfile;
71 
72 	private File store;
73 
74 	ISurrogateProfileHandler surrogateProfileHandler;
75 
76 	private IProvisioningEventBus eventBus;
77 	// cache of last accessed profile state properties
78 	private ProfileStateProperties lastAccessedProperties;
79 
SimpleProfileRegistry(IProvisioningAgent agent, File registryDirectory)80 	public SimpleProfileRegistry(IProvisioningAgent agent, File registryDirectory) {
81 		this(agent, registryDirectory, new SurrogateProfileHandler(agent), true);
82 	}
83 
SimpleProfileRegistry(IProvisioningAgent agent, File registryDirectory, ISurrogateProfileHandler handler, boolean updateSelfProfile)84 	public SimpleProfileRegistry(IProvisioningAgent agent, File registryDirectory, ISurrogateProfileHandler handler, boolean updateSelfProfile) {
85 		this.agent = agent;
86 		store = registryDirectory;
87 		surrogateProfileHandler = handler;
88 		Assert.isNotNull(store, "Profile registry requires a directory"); //$NON-NLS-1$
89 		findSelf();
90 		this.updateSelfProfile = updateSelfProfile;
91 	}
92 
93 	/**
94 	 * Determine the id of the "self" profile. This is only applicable for the registry
95 	 * of the currently running system.
96 	 */
findSelf()97 	private void findSelf() {
98 		//the location for the currently running system is registered as a service
99 		final BundleContext context = EngineActivator.getContext();
100 		if (context == null)
101 			return;
102 		ServiceReference<IAgentLocation> ref = context.getServiceReference(IAgentLocation.class);
103 		if (ref == null)
104 			return;
105 		IAgentLocation location = context.getService(ref);
106 		if (location == null)
107 			return;
108 		if (store.equals(getDefaultRegistryDirectory(location))) {
109 			//we are the registry for the currently running system
110 			self = context.getProperty("eclipse.p2.profile"); //$NON-NLS-1$
111 		} else if (agent.getService(IProvisioningAgent.SHARED_CURRENT_AGENT) != null) {
112 			// In shared mode, _SELF_ is the value of the current running profile for both agents current and shared
113 			if (((IProvisioningAgent) agent.getService(IProvisioningAgent.SHARED_CURRENT_AGENT)).getService(IProvisioningAgent.SHARED_BASE_AGENT) == agent) {
114 				self = context.getProperty("eclipse.p2.profile"); //$NON-NLS-1$
115 			}
116 		}
117 		if (self == null)
118 			self = (String) agent.getService("FORCED_SELF"); //$NON-NLS-1$
119 		context.ungetService(ref);
120 	}
121 
getDefaultRegistryDirectory(IAgentLocation agent)122 	public static File getDefaultRegistryDirectory(IAgentLocation agent) {
123 		File registryDirectory = null;
124 		if (agent == null)
125 			throw new IllegalStateException("Profile Registry inialization failed: Agent Location is not available"); //$NON-NLS-1$
126 		final URI engineDataArea = agent.getDataArea(EngineActivator.ID);
127 		URI registryURL = URIUtil.append(engineDataArea, DEFAULT_STORAGE_DIR);
128 		registryDirectory = new File(registryURL);
129 		registryDirectory.mkdirs();
130 		return registryDirectory;
131 	}
132 
133 	/**
134 	 * If the current profile for self is marked as a roaming profile, we need
135 	 * to update its install and bundle pool locations.
136 	 */
updateSelfProfile(Map<String, Profile> profileMap)137 	private void updateSelfProfile(Map<String, Profile> profileMap) {
138 		if (profileMap == null)
139 			return;
140 		Profile selfProfile = profileMap.get(self);
141 		if (selfProfile == null)
142 			return;
143 
144 		//register default locale provider where metadata translations are found
145 		//TODO ideally this should not be hard-coded to the current profile
146 		TranslationSupport.getInstance().setTranslationSource(selfProfile);
147 
148 		if (DebugHelper.DEBUG_PROFILE_REGISTRY)
149 			DebugHelper.debug(PROFILE_REGISTRY, "SimpleProfileRegistry.updateSelfProfile"); //$NON-NLS-1$
150 		boolean changed = false;
151 		//only update if self is a roaming profile
152 		if (Boolean.parseBoolean(selfProfile.getProperty(IProfile.PROP_ROAMING)))
153 			changed = updateRoamingProfile(selfProfile);
154 
155 		if (changed)
156 			saveProfile(selfProfile);
157 	}
158 
updateRoamingProfile(Profile selfProfile)159 	private boolean updateRoamingProfile(Profile selfProfile) {
160 		if (DebugHelper.DEBUG_PROFILE_REGISTRY)
161 			DebugHelper.debug(PROFILE_REGISTRY, "SimpleProfileRegistry.updateRoamingProfile"); //$NON-NLS-1$
162 		Location installLocation = ServiceHelper.getService(EngineActivator.getContext(), Location.class, Location.INSTALL_FILTER);
163 		File location = URLUtil.toFile(installLocation.getURL());
164 		if (location == null) {
165 			// fallback: use only path of the URL if the protocol is not 'file'
166 			location = new File(installLocation.getURL().getPath());
167 		}
168 		boolean changed = false;
169 		if (!location.equals(new File(selfProfile.getProperty(IProfile.PROP_INSTALL_FOLDER)))) {
170 			selfProfile.setProperty(IProfile.PROP_INSTALL_FOLDER, location.getAbsolutePath());
171 			changed = true;
172 		}
173 		String propCache = selfProfile.getProperty(IProfile.PROP_CACHE);
174 		if (propCache != null && !location.equals(new File(propCache))) {
175 			selfProfile.setProperty(IProfile.PROP_CACHE, location.getAbsolutePath());
176 			changed = true;
177 		}
178 		if (DebugHelper.DEBUG_PROFILE_REGISTRY)
179 			DebugHelper.debug(PROFILE_REGISTRY, "SimpleProfileRegistry.updateRoamingProfile(changed=" + changed + ')'); //$NON-NLS-1$
180 		return changed;
181 	}
182 
183 	@Override
toString()184 	public synchronized String toString() {
185 		return "Profile registry for location: " + store.getAbsolutePath() + "\n" + getProfileMap().toString(); //$NON-NLS-1$ //$NON-NLS-2$
186 	}
187 
188 	@Override
getProfile(String id)189 	public synchronized IProfile getProfile(String id) {
190 		Profile profile = internalGetProfile(id);
191 		if (profile == null)
192 			return null;
193 		return profile.snapshot();
194 	}
195 
196 	@Override
getProfile(String id, long timestamp)197 	public synchronized IProfile getProfile(String id, long timestamp) {
198 		if (SELF.equals(id))
199 			id = self;
200 
201 		if (profiles != null) {
202 			IProfile profile = getProfile(id);
203 			if (profile != null && profile.getTimestamp() == timestamp)
204 				return profile;
205 		}
206 
207 		File profileDirectory = getProfileFolder(id);
208 		if (!profileDirectory.isDirectory())
209 			return null;
210 
211 		File profileFile = new File(profileDirectory, Long.toString(timestamp) + PROFILE_GZ_EXT);
212 		if (!profileFile.exists()) {
213 			profileFile = new File(profileDirectory, Long.toString(timestamp) + PROFILE_EXT);
214 			if (!profileFile.exists())
215 				return null;
216 		}
217 
218 		Parser parser = new Parser(EngineActivator.getContext(), EngineActivator.ID);
219 		try {
220 			parser.parse(profileFile);
221 		} catch (IOException e) {
222 			LogHelper.log(new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.error_parsing_profile, profileFile), e));
223 		}
224 		return parser.getProfileMap().get(id);
225 	}
226 
227 	@Override
listProfileTimestamps(String id)228 	public synchronized long[] listProfileTimestamps(String id) {
229 		if (SELF.equals(id))
230 			id = self;
231 		//guard against null self profile
232 		if (id == null)
233 			return new long[0];
234 
235 		File profileDirectory = getProfileFolder(id);
236 		if (!profileDirectory.isDirectory())
237 			return new long[0];
238 
239 		File[] profileFiles = profileDirectory.listFiles((FileFilter) pathname -> (pathname.getName().endsWith(PROFILE_EXT) || pathname.getName().endsWith(PROFILE_GZ_EXT)) && pathname.isFile() && !pathname.getName().startsWith("._"));
240 
241 		long[] timestamps = new long[profileFiles.length];
242 		for (int i = 0; i < profileFiles.length; i++) {
243 			String filename = profileFiles[i].getName();
244 			int extensionIndex = filename.lastIndexOf(PROFILE_EXT);
245 			try {
246 				timestamps[i] = Long.parseLong(filename.substring(0, extensionIndex));
247 			} catch (NumberFormatException e) {
248 				throw new IllegalStateException("Incompatible profile file name. Expected format is {timestamp}" + PROFILE_GZ_EXT + " (or {timestamp}" + PROFILE_EXT + ") but was " + filename + "."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
249 			}
250 		}
251 		Arrays.sort(timestamps);
252 		return timestamps;
253 	}
254 
255 	/**
256 	 * Returns the profile with the given ID, or {@code null} if no such profile exists.
257 	 */
internalGetProfile(String id)258 	private Profile internalGetProfile(String id) {
259 		if (SELF.equals(id))
260 			id = self;
261 		Profile profile = getProfileMap().get(id);
262 		if (self != null && self.equals(id)) {
263 			boolean resetProfile = false;
264 			if (profile != null && ignoreExistingProfile(profile)) {
265 				internalSetProfileStateProperty(profile, profile.getTimestamp(), IProfile.STATE_PROP_SHARED_INSTALL, IProfile.STATE_SHARED_INSTALL_VALUE_BEFOREFLUSH);
266 				profile = null;
267 				resetProfile = true;
268 			}
269 			if (profile == null) {
270 				profile = createSurrogateProfile(id);
271 				if (profile == null)
272 					return null;
273 
274 				if (resetProfile) {
275 					//Now that we created a new profile. Tag it, override the property and register the timestamp in the agent registry for pickup by other
276 					internalSetProfileStateProperty(profile, profile.getTimestamp(), IProfile.STATE_PROP_SHARED_INSTALL, IProfile.STATE_SHARED_INSTALL_VALUE_NEW);
277 					internalSetProfileStateProperty(profile, profile.getTimestamp(), SIMPLE_PROFILE_REGISTRY_INTERNAL + getBaseTimestamp(profile.getProfileId()), getBaseTimestamp(id));
278 					//fragments support - remeber the property
279 					internalSetProfileStateProperty(profile, profile.getTimestamp(), SIMPLE_PROFILE_REGISTRY_INTERNAL + getExtTimeStamp(), getExtTimeStamp());
280 					agent.registerService(SERVICE_SHARED_INSTALL_NEW_TIMESTAMP, Long.toString(profile.getTimestamp()));
281 				} else {
282 					//This is the first time we create the shared profile. Tag it as such and also remember the timestamp of the base
283 					internalSetProfileStateProperty(profile, profile.getTimestamp(), IProfile.STATE_PROP_SHARED_INSTALL, IProfile.STATE_SHARED_INSTALL_VALUE_INITIAL);
284 					String baseTimestamp = getBaseTimestamp(id);
285 					if (baseTimestamp != null)
286 						internalSetProfileStateProperty(profile, profile.getTimestamp(), SIMPLE_PROFILE_REGISTRY_INTERNAL + baseTimestamp, baseTimestamp);
287 					String extTimestamp = getExtTimeStamp();
288 					internalSetProfileStateProperty(profile, profile.getTimestamp(), SIMPLE_PROFILE_REGISTRY_INTERNAL + extTimestamp, extTimestamp);
289 				}
290 			}
291 		}
292 		return profile;
293 	}
294 
295 	// get timestamp of fragments (extensions)
getExtTimeStamp()296 	private String getExtTimeStamp() {
297 		long result = -1;
298 		if (!EngineActivator.EXTENDED) {
299 			return Long.toString(result);
300 		}
301 		File[] extensions = EngineActivator.getExtensionsDirectories();
302 		for (File extension : extensions) {
303 			if (extension.lastModified() > result) {
304 				result = extension.lastModified();
305 			}
306 		}
307 		return Long.toString(result);
308 	}
309 
ignoreExistingProfile(IProfile profile)310 	private boolean ignoreExistingProfile(IProfile profile) {
311 		if (agent.getService(SERVICE_SHARED_INSTALL_NEW_TIMESTAMP) != null)
312 			return false;
313 
314 		String baseTimestamp = getBaseTimestamp(profile.getProfileId());
315 		String extTimestamp = getExtTimeStamp();
316 		if (baseTimestamp == null) {
317 			return false;
318 		}
319 
320 		boolean extensionOK = true;
321 		if (surrogateProfileHandler != null && surrogateProfileHandler.isSurrogate(profile)) {
322 			extensionOK = (internalGetProfileStateProperties(profile, SIMPLE_PROFILE_REGISTRY_INTERNAL + extTimestamp, false).size() != 0);
323 		}
324 
325 		if ((internalGetProfileStateProperties(profile, SIMPLE_PROFILE_REGISTRY_INTERNAL + baseTimestamp, false).size() != 0) && extensionOK)
326 			return false;
327 
328 		return true;
329 	}
330 
getBaseTimestamp(String id)331 	private String getBaseTimestamp(String id) {
332 		IProvisioningAgent baseAgent = (IProvisioningAgent) agent.getService(IProvisioningAgent.SHARED_BASE_AGENT);
333 		if (baseAgent == null)
334 			return null;
335 		IProfileRegistry registry = baseAgent.getService(IProfileRegistry.class);
336 		if (registry == null)
337 			return null;
338 		long[] revisions = registry.listProfileTimestamps(id);
339 		if (revisions.length >= 1) {
340 			return Long.toString(revisions[revisions.length - 1]);
341 		}
342 		return null;
343 	}
344 
createSurrogateProfile(String id)345 	private Profile createSurrogateProfile(String id) {
346 		if (surrogateProfileHandler == null)
347 			return null;
348 
349 		Profile profile = (Profile) surrogateProfileHandler.createProfile(id);
350 		if (profile == null)
351 			return null;
352 
353 		saveProfile(profile);
354 		resetProfiles();
355 		return getProfileMap().get(id);
356 	}
357 
358 	@Override
getProfiles()359 	public synchronized IProfile[] getProfiles() {
360 		Map<String, Profile> profileMap = getProfileMap();
361 		Profile[] result = new Profile[profileMap.size()];
362 		int i = 0;
363 		for (Profile profile : profileMap.values()) {
364 			result[i++] = profile.snapshot();
365 		}
366 		return result;
367 	}
368 
369 	/**
370 	 * Returns an initialized map of String(Profile id)->Profile.
371 	 */
getProfileMap()372 	protected Map<String, Profile> getProfileMap() {
373 		if (profiles != null) {
374 			Map<String, Profile> result = profiles.get();
375 			if (result != null)
376 				return result;
377 		}
378 		Map<String, Profile> result = restore();
379 		if (result == null)
380 			result = new LinkedHashMap<>(8);
381 		profiles = new SoftReference<>(result);
382 		if (updateSelfProfile) {
383 			//update self profile on first load
384 			updateSelfProfile(result);
385 		}
386 		return result;
387 	}
388 
updateProfile(Profile profile)389 	public synchronized void updateProfile(Profile profile) {
390 		String id = profile.getProfileId();
391 		Profile current = getProfileMap().get(id);
392 		if (current == null)
393 			throw new IllegalArgumentException(NLS.bind(Messages.profile_does_not_exist, id));
394 
395 		ProfileLock lock = profileLocks.get(id);
396 		lock.checkLocked();
397 
398 		current.clearLocalProperties();
399 		current.clearInstallableUnits();
400 
401 		current.addProperties(profile.getLocalProperties());
402 		IQueryResult<IInstallableUnit> queryResult = profile.query(QueryUtil.createIUAnyQuery(), null);
403 		for (IInstallableUnit iu : queryResult) {
404 			current.addInstallableUnit(iu);
405 			Map<String, String> iuProperties = profile.getInstallableUnitProperties(iu);
406 			if (iuProperties != null)
407 				current.addInstallableUnitProperties(iu, iuProperties);
408 		}
409 		saveProfile(current);
410 		profile.clearOrphanedInstallableUnitProperties();
411 		profile.setTimestamp(current.getTimestamp());
412 		broadcastChangeEvent(id, IProfileEvent.CHANGED);
413 	}
414 
415 	@Override
addProfile(String id)416 	public IProfile addProfile(String id) throws ProvisionException {
417 		return addProfile(id, null, null);
418 	}
419 
420 	@Override
addProfile(String id, Map<String, String> profileProperties)421 	public IProfile addProfile(String id, Map<String, String> profileProperties) throws ProvisionException {
422 		return addProfile(id, profileProperties, null);
423 	}
424 
addProfile(String id, Map<String, String> profileProperties, String parentId)425 	public synchronized IProfile addProfile(String id, Map<String, String> profileProperties, String parentId) throws ProvisionException {
426 		if (SELF.equals(id))
427 			id = self;
428 		Map<String, Profile> profileMap = getProfileMap();
429 		if (profileMap.get(id) != null)
430 			throw new ProvisionException(NLS.bind(Messages.Profile_Duplicate_Root_Profile_Id, id));
431 
432 		Profile parent = null;
433 		if (parentId != null) {
434 			if (SELF.equals(parentId))
435 				parentId = self;
436 			parent = profileMap.get(parentId);
437 			if (parent == null)
438 				throw new ProvisionException(NLS.bind(Messages.Profile_Parent_Not_Found, parentId));
439 		}
440 
441 		Profile profile = new Profile(agent, id, parent, profileProperties);
442 		if (surrogateProfileHandler != null && surrogateProfileHandler.isSurrogate(profile))
443 			profile.setSurrogateProfileHandler(surrogateProfileHandler);
444 		profileMap.put(id, profile);
445 		saveProfile(profile);
446 		broadcastChangeEvent(id, IProfileEvent.ADDED);
447 		return profile.snapshot();
448 	}
449 
450 	@Override
removeProfile(String profileId)451 	public synchronized void removeProfile(String profileId) {
452 		if (SELF.equals(profileId))
453 			profileId = self;
454 		//note we need to maintain a reference to the profile map until it is persisted to prevent gc
455 		Map<String, Profile> profileMap = getProfileMap();
456 		Profile profile = profileMap.get(profileId);
457 		if (profile == null)
458 			return;
459 
460 		List<String> subProfileIds = profile.getSubProfileIds();
461 		for (String subProfileId : subProfileIds) {
462 			removeProfile(subProfileId);
463 		}
464 		internalLockProfile(profile);
465 		// The above call recursively locked the parent(s). So save it away to rewind the locking process.
466 		IProfile savedParent = profile.getParentProfile();
467 		try {
468 			profile.setParent(null);
469 		} finally {
470 			internalUnlockProfile(profile);
471 			// The above call will not recurse since parent is now null. So do it explicitly.
472 			if (savedParent != null) {
473 				internalUnlockProfile(savedParent);
474 			}
475 		}
476 		profileMap.remove(profileId);
477 		profileLocks.remove(profileId);
478 		// deleting the profile removes the folder and subsequently all
479 		// the profile state properties as well since they are stored in a file in the folder.
480 		deleteProfile(profileId);
481 		broadcastChangeEvent(profileId, IProfileEvent.REMOVED);
482 	}
483 
484 	@Override
removeProfile(String id, long timestamp)485 	public synchronized void removeProfile(String id, long timestamp) throws ProvisionException {
486 		if (SELF.equals(id))
487 			id = self;
488 
489 		if (profiles != null) {
490 			IProfile profile = getProfile(id);
491 			if (profile != null && profile.getTimestamp() == timestamp)
492 				throw new ProvisionException(Messages.SimpleProfileRegistry_CannotRemoveCurrentSnapshot);
493 		}
494 
495 		File profileDirectory = getProfileFolder(id);
496 		if (!profileDirectory.isDirectory())
497 			return;
498 
499 		File profileFile = new File(profileDirectory, Long.toString(timestamp) + PROFILE_GZ_EXT);
500 		if (!profileFile.exists()) {
501 			profileFile = new File(profileDirectory, Long.toString(timestamp) + PROFILE_EXT);
502 			if (!profileFile.exists())
503 				return;
504 		}
505 		FileUtils.deleteAll(profileFile);
506 		// Ignore the return value here. If there was a problem removing the profile state
507 		// properties we don't want to fail the whole operation since the profile state itself
508 		// was removed successfully
509 		removeProfileStateProperties(id, timestamp, null);
510 	}
511 
broadcastChangeEvent(String profileId, int reason)512 	private void broadcastChangeEvent(String profileId, int reason) {
513 		if (eventBus != null)
514 			eventBus.publishEvent(new ProfileEvent(profileId, reason));
515 	}
516 
517 	/**
518 	 * Restores the profile registry from disk, and returns the loaded profile map.
519 	 * Returns <code>null</code> if unable to read the registry.
520 	 */
restore()521 	private Map<String, Profile> restore() {
522 		if (store == null || !store.isDirectory())
523 			throw new IllegalStateException(NLS.bind(Messages.reg_dir_not_available, store));
524 
525 		Parser parser = new Parser(EngineActivator.getContext(), EngineActivator.ID);
526 		File[] profileDirectories = store.listFiles((FileFilter) pathname -> pathname.getName().endsWith(PROFILE_EXT) && pathname.isDirectory());
527 		// protect against NPE
528 		if (profileDirectories == null) {
529 			parser.getProfileMap();
530 		}
531 		for (File profileDirectorie : profileDirectories) {
532 			String directoryName = profileDirectorie.getName();
533 			String profileId = unescape(directoryName.substring(0, directoryName.lastIndexOf(PROFILE_EXT)));
534 			ProfileLock lock = profileLocks.get(profileId);
535 			if (lock == null) {
536 				lock = new ProfileLock(this, profileDirectorie);
537 				profileLocks.put(profileId, lock);
538 			}
539 
540 			boolean locked = false;
541 			if (lock.processHoldsLock() || (locked = lock.lock())) {
542 				try {
543 					File profileFile = findLatestProfileFile(profileDirectorie);
544 					if (profileFile != null) {
545 						try {
546 							parser.parse(profileFile);
547 						} catch (IOException e) {
548 							LogHelper.log(new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.error_parsing_profile, profileFile), e));
549 						}
550 					}
551 				} finally {
552 					if (locked)
553 						lock.unlock();
554 				}
555 			} else {
556 				// could not lock the profile, so add a place holder
557 				parser.addProfilePlaceHolder(profileId);
558 			}
559 		}
560 		return parser.getProfileMap();
561 	}
562 
findLatestProfileFile(File profileDirectory)563 	private File findLatestProfileFile(File profileDirectory) {
564 		File latest = null;
565 		long latestTimestamp = 0;
566 		File[] profileFiles = profileDirectory.listFiles((FileFilter) pathname -> (pathname.getName().endsWith(PROFILE_GZ_EXT) || pathname.getName().endsWith(PROFILE_EXT)) && !pathname.isDirectory());
567 		// protect against NPE
568 		if (profileFiles == null)
569 			return null;
570 		for (File profileFile : profileFiles) {
571 			String fileName = profileFile.getName();
572 			try {
573 				long timestamp = Long.parseLong(fileName.substring(0, fileName.indexOf(PROFILE_EXT)));
574 				if (timestamp > latestTimestamp) {
575 					latestTimestamp = timestamp;
576 					latest = profileFile;
577 				}
578 			} catch (NumberFormatException e) {
579 				// ignore
580 			}
581 		}
582 		return latest;
583 	}
584 
saveProfile(Profile profile)585 	private void saveProfile(Profile profile) {
586 		File profileDirectory = getProfileFolder(profile.getProfileId());
587 		profileDirectory.mkdir();
588 
589 		long previousTimestamp = profile.getTimestamp();
590 		long currentTimestamp = System.currentTimeMillis();
591 		if (currentTimestamp <= previousTimestamp)
592 			currentTimestamp = previousTimestamp + 1;
593 		boolean shouldGzipFile = shouldGzipFile(profile);
594 		File profileFile = new File(profileDirectory, Long.toString(currentTimestamp) + (shouldGzipFile ? PROFILE_GZ_EXT : PROFILE_EXT));
595 
596 		// Log a stack trace to see who is writing the profile.
597 		if (DebugHelper.DEBUG_PROFILE_REGISTRY)
598 			DebugHelper.debug(PROFILE_REGISTRY, "Saving profile to: " + profileFile.getAbsolutePath()); //$NON-NLS-1$
599 
600 		profile.setTimestamp(currentTimestamp);
601 		profile.setChanged(false);
602 		OutputStream os = null;
603 		try {
604 			if (shouldGzipFile)
605 				os = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream(profileFile)));
606 			else
607 				os = new BufferedOutputStream(new FileOutputStream(profileFile));
608 			Writer writer = new Writer(os);
609 			writer.writeProfile(profile);
610 		} catch (IOException e) {
611 			profile.setTimestamp(previousTimestamp);
612 			profileFile.delete();
613 			LogHelper.log(new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.error_persisting_profile, profile.getProfileId()), e));
614 		} finally {
615 			try {
616 				if (os != null)
617 					os.close();
618 			} catch (IOException e) {
619 				// ignore
620 			}
621 		}
622 	}
623 
setEventBus(IProvisioningEventBus bus)624 	public void setEventBus(IProvisioningEventBus bus) {
625 		this.eventBus = bus;
626 	}
627 
628 	/**
629 	 * Returns whether the profile file for the given profile should be written in gzip format.
630 	 */
shouldGzipFile(Profile profile)631 	private boolean shouldGzipFile(Profile profile) {
632 		//check system property controlling compression
633 		String format = EngineActivator.getContext().getProperty(EngineActivator.PROP_PROFILE_FORMAT);
634 		if (format != null && format.equals(EngineActivator.PROFILE_FORMAT_UNCOMPRESSED))
635 			return false;
636 
637 		//check whether the profile contains the p2 engine from 3.5.0 or earlier
638 		return profile.available(QueryUtil.createIUQuery("org.eclipse.equinox.p2.engine", VersionRange.create("[0.0.0, 1.0.101)")), null).isEmpty(); //$NON-NLS-1$//$NON-NLS-2$
639 	}
640 
deleteProfile(String profileId)641 	private void deleteProfile(String profileId) {
642 		File profileDirectory = getProfileFolder(profileId);
643 		FileUtils.deleteAll(profileDirectory);
644 	}
645 
646 	/**
647 	 * Converts a profile id into a string that can be used as a file name in any file system.
648 	 */
escape(String toEscape)649 	public static String escape(String toEscape) {
650 		StringBuilder buffer = new StringBuilder();
651 		int length = toEscape.length();
652 		for (int i = 0; i < length; ++i) {
653 			char ch = toEscape.charAt(i);
654 			switch (ch) {
655 				case '\\' :
656 				case '/' :
657 				case ':' :
658 				case '*' :
659 				case '?' :
660 				case '"' :
661 				case '<' :
662 				case '>' :
663 				case '|' :
664 				case '%' :
665 					buffer.append("%" + (int) ch + ";"); //$NON-NLS-1$ //$NON-NLS-2$
666 					break;
667 				default :
668 					buffer.append(ch);
669 			}
670 		}
671 		return buffer.toString();
672 	}
673 
unescape(String text)674 	public static String unescape(String text) {
675 		if (text.indexOf('%') == -1)
676 			return text;
677 
678 		StringBuilder buffer = new StringBuilder();
679 		int length = text.length();
680 		for (int i = 0; i < length; ++i) {
681 			char ch = text.charAt(i);
682 			if (ch == '%') {
683 				int colon = text.indexOf(';', i);
684 				if (colon == -1)
685 					throw new IllegalStateException("error unescaping the sequence at character (" + i + ") for " + text + ". Expected %{int};."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
686 				ch = (char) Integer.parseInt(text.substring(i + 1, colon));
687 				i = colon;
688 			}
689 			buffer.append(ch);
690 		}
691 		return buffer.toString();
692 	}
693 
694 	static class Writer extends ProfileWriter {
695 
Writer(OutputStream output)696 		public Writer(OutputStream output) {
697 			super(output, new ProcessingInstruction[] {ProcessingInstruction.makeTargetVersionInstruction(PROFILE_TARGET, ProfileXMLConstants.CURRENT_VERSION)});
698 		}
699 	}
700 
701 	/*
702 	 * 	Parser for the contents of a SimpleProfileRegistry,
703 	 * 	as written by the Writer class.
704 	 */
705 	class Parser extends ProfileParser {
706 		private final Map<String, ProfileHandler> profileHandlers = new HashMap<>();
707 
getProfileHandlers()708 		public Map<String, ProfileHandler> getProfileHandlers() {
709 			return Collections.unmodifiableMap(profileHandlers);
710 		}
711 
Parser(BundleContext context, String bundleId)712 		public Parser(BundleContext context, String bundleId) {
713 			super(context, bundleId);
714 		}
715 
addProfilePlaceHolder(String profileId)716 		public void addProfilePlaceHolder(String profileId) {
717 			profileHandlers.put(profileId, new ProfileHandler(profileId));
718 		}
719 
parse(File file)720 		public void parse(File file) throws IOException {
721 			InputStream is;
722 			if (file.getName().endsWith(PROFILE_GZ_EXT)) {
723 				is = new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)));
724 			} else { // backward compatibility. SimpleProfileRegistry doesn't write non-gzipped profiles any more.
725 				is = new BufferedInputStream(new FileInputStream(file));
726 			}
727 			parse(is);
728 		}
729 
parse(InputStream stream)730 		public synchronized void parse(InputStream stream) throws IOException {
731 			this.status = null;
732 			try {
733 				// TODO: currently not caching the parser since we make no assumptions
734 				//		 or restrictions on concurrent parsing
735 				getParser();
736 				ProfileHandler profileHandler = new ProfileHandler();
737 				xmlReader.setContentHandler(new ProfileDocHandler(PROFILE_ELEMENT, profileHandler));
738 				xmlReader.parse(new InputSource(stream));
739 				profileHandlers.put(profileHandler.getProfileId(), profileHandler);
740 			} catch (SAXException e) {
741 				IOException ioException = new IOException(e.getMessage());
742 				ioException.initCause(e);
743 				throw ioException;
744 			} catch (ParserConfigurationException e) {
745 				IOException ioException = new IOException(e.getMessage());
746 				ioException.initCause(e);
747 				throw ioException;
748 			} finally {
749 				stream.close();
750 			}
751 		}
752 
753 		@Override
getRootObject()754 		protected Object getRootObject() {
755 			return this;
756 		}
757 
getProfileMap()758 		public Map<String, Profile> getProfileMap() {
759 			Map<String, Profile> profileMap = new HashMap<>();
760 			for (String profileId : profileHandlers.keySet()) {
761 				addProfile(profileId, profileMap);
762 			}
763 			return profileMap;
764 		}
765 
addProfile(String profileId, Map<String, Profile> profileMap)766 		private void addProfile(String profileId, Map<String, Profile> profileMap) {
767 			if (profileMap.containsKey(profileId))
768 				return;
769 
770 			ProfileHandler profileHandler = profileHandlers.get(profileId);
771 			Profile parentProfile = null;
772 
773 			String parentId = profileHandler.getParentId();
774 			if (parentId != null) {
775 				addProfile(parentId, profileMap);
776 				parentProfile = profileMap.get(parentId);
777 			}
778 
779 			Profile profile = new Profile(agent, profileId, parentProfile, profileHandler.getProperties());
780 			if (surrogateProfileHandler != null && surrogateProfileHandler.isSurrogate(profile))
781 				profile.setSurrogateProfileHandler(surrogateProfileHandler);
782 
783 			profile.setTimestamp(profileHandler.getTimestamp());
784 
785 			IInstallableUnit[] ius = profileHandler.getInstallableUnits();
786 			if (ius != null) {
787 				for (IInstallableUnit iu : ius) {
788 					profile.addInstallableUnit(iu);
789 					Map<String, String> iuProperties = profileHandler.getIUProperties(iu);
790 					if (iuProperties != null) {
791 						for (Entry<String, String> entry : iuProperties.entrySet()) {
792 							profile.setInstallableUnitProperty(iu, entry.getKey(), entry.getValue());
793 						}
794 					}
795 				}
796 			}
797 			profile.setChanged(false);
798 			profileMap.put(profileId, profile);
799 		}
800 
801 		private final class ProfileDocHandler extends DocHandler {
802 
ProfileDocHandler(String rootName, RootHandler rootHandler)803 			public ProfileDocHandler(String rootName, RootHandler rootHandler) {
804 				super(rootName, rootHandler);
805 			}
806 
807 			@Override
processingInstruction(String target, String data)808 			public void processingInstruction(String target, String data) throws SAXException {
809 				if (ProfileXMLConstants.PROFILE_TARGET.equals(target)) {
810 					Version repositoryVersion = extractPIVersion(target, data);
811 					if (!ProfileXMLConstants.XML_TOLERANCE.isIncluded(repositoryVersion)) {
812 						throw new SAXException(NLS.bind(Messages.SimpleProfileRegistry_Parser_Has_Incompatible_Version, repositoryVersion, ProfileXMLConstants.XML_TOLERANCE));
813 					}
814 				}
815 			}
816 		}
817 
818 		@Override
getErrorMessage()819 		protected String getErrorMessage() {
820 			return Messages.SimpleProfileRegistry_Parser_Error_Parsing_Registry;
821 		}
822 
823 		@Override
toString()824 		public String toString() {
825 			// TODO:
826 			return null;
827 		}
828 
829 	}
830 
831 	@Override
isCurrent(IProfile profile)832 	public synchronized boolean isCurrent(IProfile profile) {
833 		Profile internalProfile = getProfileMap().get(profile.getProfileId());
834 		if (internalProfile == null)
835 			throw new IllegalArgumentException(NLS.bind(Messages.profile_not_registered, profile.getProfileId()));
836 
837 		if (!internalLockProfile(internalProfile))
838 			throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
839 
840 		try {
841 			return (!((Profile) profile).isChanged() && checkTimestamps(profile, internalProfile));
842 		} finally {
843 			internalUnlockProfile(internalProfile);
844 		}
845 	}
846 
lockProfile(Profile profile)847 	public synchronized void lockProfile(Profile profile) {
848 		Profile internalProfile = internalGetProfile(profile.getProfileId());
849 		if (internalProfile == null)
850 			throw new IllegalArgumentException(NLS.bind(Messages.profile_not_registered, profile.getProfileId()));
851 
852 		if (!internalLockProfile(internalProfile))
853 			throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
854 
855 		boolean isCurrent = false;
856 		try {
857 			if (profile.isChanged()) {
858 				if (DebugHelper.DEBUG_PROFILE_REGISTRY)
859 					DebugHelper.debug(PROFILE_REGISTRY, "Profile is marked as changed."); //$NON-NLS-1$
860 				throw new IllegalStateException(NLS.bind(Messages.profile_changed, profile.getProfileId()));
861 			}
862 			if (!checkTimestamps(profile, internalProfile)) {
863 				if (DebugHelper.DEBUG_PROFILE_REGISTRY)
864 					DebugHelper.debug(PROFILE_REGISTRY, "Unexpected timestamp difference in profile."); //$NON-NLS-1$
865 				throw new IllegalStateException(NLS.bind(Messages.profile_not_current, new String[] {profile.getProfileId(), Long.toString(internalProfile.getTimestamp()), Long.toString(profile.getTimestamp())}));
866 			}
867 			isCurrent = true;
868 		} finally {
869 			// this check is done here to ensure we unlock even if a runtime exception is thrown
870 			if (!isCurrent)
871 				internalUnlockProfile(internalProfile);
872 		}
873 	}
874 
internalLockProfile(IProfile profile)875 	private boolean internalLockProfile(IProfile profile) {
876 		ProfileLock lock = profileLocks.get(profile.getProfileId());
877 		if (lock == null) {
878 			lock = new ProfileLock(this, getProfileFolder(profile.getProfileId()));
879 			profileLocks.put(profile.getProfileId(), lock);
880 		}
881 		return lock.lock();
882 	}
883 
checkTimestamps(IProfile profile, IProfile internalProfile)884 	private boolean checkTimestamps(IProfile profile, IProfile internalProfile) {
885 		long[] timestamps = listProfileTimestamps(profile.getProfileId());
886 		if (timestamps.length == 0) {
887 			if (DebugHelper.DEBUG_PROFILE_REGISTRY)
888 				DebugHelper.debug(PROFILE_REGISTRY, "check timestamp: expected " + profile.getTimestamp() + " but no profiles were found"); //$NON-NLS-1$ //$NON-NLS-2$
889 			resetProfiles();
890 			return false;
891 		}
892 
893 		long currentTimestamp = (timestamps.length == 0) ? -1 : timestamps[timestamps.length - 1];
894 		if (profile.getTimestamp() != currentTimestamp) {
895 			if (DebugHelper.DEBUG_PROFILE_REGISTRY)
896 				DebugHelper.debug(PROFILE_REGISTRY, "check timestamp: expected " + profile.getTimestamp() + " but was " + currentTimestamp); //$NON-NLS-1$ //$NON-NLS-2$
897 			if (internalProfile.getTimestamp() != currentTimestamp)
898 				resetProfiles();
899 			return false;
900 		}
901 
902 		return true;
903 	}
904 
905 	@Override
containsProfile(String id)906 	public synchronized boolean containsProfile(String id) {
907 		if (SELF.equals(id))
908 			id = self;
909 		//null check done after self check, because self can be null
910 		if (id == null)
911 			return false;
912 
913 		// check profiles to avoid restoring the profile registry
914 		if (profiles != null)
915 			if (getProfile(id) != null)
916 				return true;
917 
918 		File profileDirectory = getProfileFolder(id);
919 		if (!profileDirectory.isDirectory())
920 			return false;
921 		File[] profileFiles = profileDirectory.listFiles((FileFilter) pathname -> (pathname.getName().endsWith(PROFILE_GZ_EXT) || pathname.getName().endsWith(PROFILE_EXT)) && pathname.isFile());
922 		return profileFiles.length > 0;
923 	}
924 
resetProfiles()925 	public synchronized void resetProfiles() {
926 		profiles = null;
927 	}
928 
unlockProfile(IProfile profile)929 	public synchronized void unlockProfile(IProfile profile) {
930 		if (profile == null)
931 			throw new IllegalArgumentException(NLS.bind(Messages.profile_not_registered, "")); //$NON-NLS-1$
932 		internalUnlockProfile(profile);
933 	}
934 
internalUnlockProfile(IProfile profile)935 	private void internalUnlockProfile(IProfile profile) {
936 		ProfileLock lock = profileLocks.get(profile.getProfileId());
937 		lock.unlock();
938 	}
939 
validate(IProfile candidate)940 	public Profile validate(IProfile candidate) {
941 		if (candidate instanceof Profile)
942 			return (Profile) candidate;
943 
944 		throw new IllegalArgumentException("Profile incompatible: expected " + Profile.class.getName() + " but was " + ((candidate != null) ? candidate.getClass().getName() : "null") + "."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
945 	}
946 
getProfileDataDirectory(String id)947 	public synchronized File getProfileDataDirectory(String id) {
948 		if (SELF.equals(id))
949 			id = self;
950 		File profileDirectory = getProfileFolder(id);
951 		File profileDataArea = new File(profileDirectory, DATA_EXT);
952 		if (!profileDataArea.isDirectory() && !profileDataArea.mkdir())
953 			throw new IllegalStateException("Could not create profile data area " + profileDataArea.getAbsolutePath() + "for: " + id); //$NON-NLS-1$ //$NON-NLS-2$
954 		return profileDataArea;
955 	}
956 
957 	@Override
start()958 	public void start() {
959 		//nothing to do
960 	}
961 
962 	@Override
stop()963 	public void stop() {
964 		try {
965 			//ensure there are no more profile preference save jobs running
966 			Job.getJobManager().join(ProfilePreferences.PROFILE_SAVE_JOB_FAMILY, null);
967 		} catch (InterruptedException e) {
968 			//ignore
969 		}
970 	}
971 
972 	// Class representing a particular instance of a profile's state properties.
973 	// Can be used for caching.
974 	class ProfileStateProperties {
975 		private String id;
976 		private File file;
977 		private long timestamp;
978 		private Properties properties;
979 
ProfileStateProperties(String id, File file, Properties properties)980 		ProfileStateProperties(String id, File file, Properties properties) {
981 			this.id = id;
982 			this.file = file;
983 			this.properties = properties;
984 			this.timestamp = file.lastModified();
985 		}
986 
987 		// return true if the cached timestamp is the same as the one on disk
isCurrent()988 		boolean isCurrent() {
989 			if (!file.exists())
990 				return true;
991 			return file.lastModified() == timestamp;
992 		}
993 
getId()994 		String getId() {
995 			return id;
996 		}
997 
getProperties()998 		Properties getProperties() {
999 			return this.properties;
1000 		}
1001 	}
1002 
1003 	/*
1004 	 * Return the folder on disk associated with the profile with the given identifier.
1005 	 */
getProfileFolder(String id)1006 	private File getProfileFolder(String id) {
1007 		return new File(store, escape(id) + PROFILE_EXT);
1008 	}
1009 
1010 	/*
1011 	 * Read and return the state properties for the profile with the given id.
1012 	 * If one does not exist, then return an empty Properties file.
1013 	 * If there were problems reading the file then return throw an exception.
1014 	 */
readStateProperties(String id)1015 	private Properties readStateProperties(String id) throws ProvisionException {
1016 		if (SELF.equals(id))
1017 			id = self;
1018 
1019 		// if the last cached value is the one we are interested in and up-to-date
1020 		// then don't bother reading from disk
1021 		if (lastAccessedProperties != null && id.equals(lastAccessedProperties.getId()) && lastAccessedProperties.isCurrent())
1022 			return lastAccessedProperties.getProperties();
1023 
1024 		File profileDirectory = getProfileFolder(id);
1025 		if (!profileDirectory.isDirectory())
1026 			throw new ProvisionException(new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.SimpleProfileRegistry_Bad_profile_location, profileDirectory.getPath())));
1027 
1028 		File file = new File(profileDirectory, PROFILE_PROPERTIES_FILE);
1029 		Properties properties = new Properties();
1030 		if (!file.exists()) {
1031 			lastAccessedProperties = new ProfileStateProperties(id, file, properties);
1032 			return properties;
1033 		}
1034 		try (InputStream input = new BufferedInputStream(new FileInputStream(file))) {
1035 			properties.load(input);
1036 		} catch (IOException e) {
1037 			throw new ProvisionException(new Status(IStatus.ERROR, EngineActivator.ID, Messages.SimpleProfileRegistry_States_Error_Reading_File, e));
1038 		}
1039 
1040 		//cache the value before we return
1041 		lastAccessedProperties = new ProfileStateProperties(id, file, properties);
1042 		return properties;
1043 	}
1044 
1045 	/*
1046 	 * Write the given state properties to disk for the specified profile.
1047 	 */
writeStateProperties(String id, Properties properties)1048 	private IStatus writeStateProperties(String id, Properties properties) {
1049 		if (SELF.equals(id))
1050 			id = self;
1051 
1052 		File profileDirectory = getProfileFolder(id);
1053 		File file = new File(profileDirectory, PROFILE_PROPERTIES_FILE);
1054 		Properties prunedProperties = properties;
1055 		try (OutputStream output = new BufferedOutputStream(new FileOutputStream(file));) {
1056 			prunedProperties = pruneStateProperties(id, properties);
1057 			prunedProperties.store(output, null);
1058 			output.flush();
1059 		} catch (IOException e) {
1060 			return new Status(IStatus.ERROR, EngineActivator.ID, Messages.SimpleProfileRegistry_States_Error_Writing_File, e);
1061 		}
1062 		// cache the value
1063 		lastAccessedProperties = new ProfileStateProperties(id, file, prunedProperties);
1064 		return Status.OK_STATUS;
1065 	}
1066 
1067 	// Only write state properties for state timestamps that still exist
1068 	// TODO: Do we want to expose this method as API?
1069 	// TODO: Do we want to run this method on every write or just after specific elapsed times since the last Prune?
pruneStateProperties(String id, Properties properties)1070 	private Properties pruneStateProperties(String id, Properties properties) {
1071 		Properties result = new Properties();
1072 		long[] timestamps = listProfileTimestamps(id);
1073 		HashSet<String> timestampsSet = new HashSet<>(timestamps.length);
1074 		for (long timestamp : timestamps) {
1075 			timestampsSet.add(String.valueOf(timestamp));
1076 		}
1077 
1078 		Enumeration<Object> keys = properties.keys();
1079 		while (keys.hasMoreElements()) {
1080 			String key = (String) keys.nextElement();
1081 			int index = key.indexOf('.');
1082 			if (index > -1) {
1083 				String timestamp = key.substring(0, index);
1084 				if (timestampsSet.contains(timestamp)) {
1085 					result.put(key, properties.get(key));
1086 				}
1087 			}
1088 		}
1089 		return result;
1090 	}
1091 
1092 	/*
1093 	 * Ensure a profile with the given identifier has a state with the specified timestamp. Return
1094 	 * a status object indicating success or failure.
1095 	 */
validateState(String id, long timestamp)1096 	private IStatus validateState(String id, long timestamp) {
1097 		long[] states = listProfileTimestamps(id);
1098 		for (long ts : states)
1099 			if (ts == timestamp)
1100 				return Status.OK_STATUS;
1101 		return new Status(IStatus.ERROR, EngineActivator.ID, (NLS.bind(Messages.SimpleProfileRegistry_state_not_found, timestamp, id)));
1102 	}
1103 
1104 	@Override
setProfileStateProperties(String id, long timestamp, Map<String, String> propertiesToAdd)1105 	public IStatus setProfileStateProperties(String id, long timestamp, Map<String, String> propertiesToAdd) {
1106 		if (id == null || propertiesToAdd == null)
1107 			throw new NullPointerException();
1108 
1109 		Profile internalProfile = internalGetProfile(id);
1110 		if (internalProfile == null)
1111 			throw new IllegalArgumentException(id);
1112 		return internalSetProfileStateProperties(internalProfile, timestamp, propertiesToAdd);
1113 	}
1114 
internalSetProfileStateProperties(IProfile profile, long timestamp, Map<String, String> propertiesToAdd)1115 	private IStatus internalSetProfileStateProperties(IProfile profile, long timestamp, Map<String, String> propertiesToAdd) {
1116 		IStatus result = validateState(profile.getProfileId(), timestamp);
1117 		if (!result.isOK())
1118 			return result;
1119 
1120 		if (!internalLockProfile(profile))
1121 			throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
1122 
1123 		try {
1124 			Properties properties = readStateProperties(profile.getProfileId());
1125 			for (Map.Entry<String, String> entry : propertiesToAdd.entrySet()) {
1126 				// property key format is timestamp.key
1127 				properties.put(timestamp + "." + entry.getKey(), entry.getValue()); //$NON-NLS-1$
1128 			}
1129 			writeStateProperties(profile.getProfileId(), properties);
1130 		} catch (ProvisionException e) {
1131 			return e.getStatus();
1132 		} finally {
1133 			internalUnlockProfile(profile);
1134 		}
1135 		return Status.OK_STATUS;
1136 	}
1137 
1138 	@Override
setProfileStateProperty(String id, long timestamp, String key, String value)1139 	public IStatus setProfileStateProperty(String id, long timestamp, String key, String value) {
1140 		if (id == null)
1141 			throw new NullPointerException();
1142 		Profile internalProfile = internalGetProfile(id);
1143 		if (internalProfile == null)
1144 			throw new IllegalArgumentException(id);
1145 		return internalSetProfileStateProperty(internalProfile, timestamp, key, value);
1146 	}
1147 
internalSetProfileStateProperty(IProfile profile, long timestamp, String key, String value)1148 	private IStatus internalSetProfileStateProperty(IProfile profile, long timestamp, String key, String value) {
1149 		if (key == null || value == null)
1150 			throw new NullPointerException();
1151 		Map<String, String> properties = new HashMap<>();
1152 		properties.put(key, value);
1153 
1154 		return internalSetProfileStateProperties(profile, timestamp, properties);
1155 	}
1156 
1157 	@Override
getProfileStateProperties(String id, long timestamp)1158 	public Map<String, String> getProfileStateProperties(String id, long timestamp) {
1159 		if (id == null)
1160 			throw new NullPointerException();
1161 		Profile internalProfile = internalGetProfile(id);
1162 		if (internalProfile == null)
1163 			return Collections.emptyMap();
1164 		return internalGetProfileStateProperties(internalProfile, timestamp, true);
1165 	}
1166 
internalGetProfileStateProperties(IProfile profile, long timestamp, boolean lock)1167 	private Map<String, String> internalGetProfileStateProperties(IProfile profile, long timestamp, boolean lock) {
1168 		Map<String, String> result = new HashMap<>();
1169 		String timestampString = String.valueOf(timestamp);
1170 		int keyOffset = timestampString.length() + 1;
1171 		lock = lock || lastAccessedProperties == null;
1172 		if (lock)
1173 			if (!internalLockProfile(profile))
1174 				throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
1175 		try {
1176 			Properties properties = readStateProperties(profile.getProfileId());
1177 			Iterator<Object> keys = properties.keySet().iterator();
1178 			while (keys.hasNext()) {
1179 				String key = (String) keys.next();
1180 				if (key.indexOf(timestampString) == 0)
1181 					result.put(key.substring(keyOffset), properties.getProperty(key));
1182 			}
1183 		} catch (ProvisionException e) {
1184 			LogHelper.log(e);
1185 		} finally {
1186 			if (lock)
1187 				internalUnlockProfile(profile);
1188 		}
1189 		return result;
1190 	}
1191 
1192 	@Override
getProfileStateProperties(String id, String userKey)1193 	public Map<String, String> getProfileStateProperties(String id, String userKey) {
1194 		if (id == null || userKey == null)
1195 			throw new NullPointerException();
1196 
1197 		Profile internalProfile = internalGetProfile(id);
1198 		if (internalProfile == null)
1199 			return Collections.emptyMap();
1200 		return internalGetProfileStateProperties(internalProfile, userKey, true);
1201 	}
1202 
internalGetProfileStateProperties(IProfile profile, String userKey, boolean lock)1203 	private Map<String, String> internalGetProfileStateProperties(IProfile profile, String userKey, boolean lock) {
1204 		Map<String, String> result = new HashMap<>();
1205 		lock = lock || lastAccessedProperties == null;
1206 		if (lock)
1207 			if (!internalLockProfile(profile))
1208 				throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
1209 		try {
1210 			Properties properties = readStateProperties(profile.getProfileId());
1211 			Iterator<Object> keys = properties.keySet().iterator();
1212 			while (keys.hasNext()) {
1213 				// property key format is timestamp.key
1214 				String key = (String) keys.next();
1215 				int index = key.indexOf('.');
1216 				if (index != -1 && index + 1 != key.length() && key.substring(index + 1).equals(userKey)) {
1217 					result.put(key.substring(0, index), properties.getProperty(key));
1218 				}
1219 			}
1220 		} catch (ProvisionException e) {
1221 			LogHelper.log(e);
1222 		} finally {
1223 			if (lock)
1224 				internalUnlockProfile(profile);
1225 		}
1226 		return result;
1227 	}
1228 
1229 	@Override
removeProfileStateProperties(String id, long timestamp, Collection<String> keys)1230 	public IStatus removeProfileStateProperties(String id, long timestamp, Collection<String> keys) {
1231 		if (id == null)
1232 			throw new NullPointerException();
1233 		// return if there is no work to do
1234 		if (keys != null && keys.size() == 0)
1235 			return Status.OK_STATUS;
1236 
1237 		Profile internalProfile = internalGetProfile(id);
1238 		if (internalProfile == null)
1239 			return Status.OK_STATUS;
1240 
1241 		if (!internalLockProfile(internalProfile))
1242 			throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
1243 
1244 		try {
1245 			Properties properties = readStateProperties(id);
1246 			String timestampString = String.valueOf(timestamp);
1247 			if (keys == null) {
1248 				// remove all keys
1249 				for (Iterator<Object> already = properties.keySet().iterator(); already.hasNext();) {
1250 					String key = (String) already.next();
1251 					// property key is timestamp.key
1252 					if (key.startsWith(timestampString))
1253 						already.remove();
1254 				}
1255 			} else {
1256 				for (String key : keys) {
1257 					// property key format is timestamp.key
1258 					if (key != null)
1259 						properties.remove(timestampString + "." + key); //$NON-NLS-1$
1260 				}
1261 			}
1262 			writeStateProperties(id, properties);
1263 		} catch (ProvisionException e) {
1264 			return e.getStatus();
1265 		} finally {
1266 			internalUnlockProfile(internalProfile);
1267 		}
1268 		return Status.OK_STATUS;
1269 	}
1270 }
1271