blob: 2efe605c4e1abb095e99f5574574f56801ba2cd2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2015 IBM Corporation and others. All rights reserved. This
* program and the accompanying materials are made available under the terms of
* the Eclipse Public License v1.0 which accompanies this distribution, and is
* available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Ericsson AB - ongoing development
* Red Hat, Inc. - fragments support added, Bug 460967
******************************************************************************/
package org.eclipse.equinox.internal.p2.engine;
import java.io.*;
import java.lang.ref.SoftReference;
import java.net.URI;
import java.util.*;
import java.util.Map.Entry;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.equinox.internal.p2.core.helpers.*;
import org.eclipse.equinox.internal.p2.metadata.TranslationSupport;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.IProvisioningEventBus;
import org.eclipse.equinox.p2.core.*;
import org.eclipse.equinox.p2.core.spi.IAgentService;
import org.eclipse.equinox.p2.engine.*;
import org.eclipse.equinox.p2.metadata.*;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.osgi.service.datalocation.Location;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
public class SimpleProfileRegistry implements IProfileRegistry, IAgentService {
private static final String SIMPLE_PROFILE_REGISTRY_INTERNAL = "_simpleProfileRegistry_internal_"; //$NON-NLS-1$
private static final String PROFILE_REGISTRY = "profile registry"; //$NON-NLS-1$
private static final String PROFILE_PROPERTIES_FILE = "state.properties"; //$NON-NLS-1$
private static final String PROFILE_EXT = ".profile"; //$NON-NLS-1$
private static final String PROFILE_GZ_EXT = ".profile.gz"; //$NON-NLS-1$
public static final String DEFAULT_STORAGE_DIR = "profileRegistry"; //$NON-NLS-1$
private static final String DATA_EXT = ".data"; //$NON-NLS-1$
//Internal constant used to keep track of the newly created timestamp
private static final String SERVICE_SHARED_INSTALL_NEW_TIMESTAMP = IProfileRegistry.class.getName() + '_' + "NEW_SELF_TIMESTAMP"; //$NON-NLS-1$
protected final IProvisioningAgent agent;
/**
* Reference to Map of String(Profile id)->Profile.
*/
private SoftReference<Map<String, Profile>> profiles;
private Map<String, ProfileLock> profileLocks = new HashMap<String, ProfileLock>();
private String self;
//Whether the registry should update the self profile when the registry is restored
private boolean updateSelfProfile;
private File store;
ISurrogateProfileHandler surrogateProfileHandler;
private IProvisioningEventBus eventBus;
// cache of last accessed profile state properties
private ProfileStateProperties lastAccessedProperties;
public SimpleProfileRegistry(IProvisioningAgent agent, File registryDirectory) {
this(agent, registryDirectory, new SurrogateProfileHandler(agent), true);
}
public SimpleProfileRegistry(IProvisioningAgent agent, File registryDirectory, ISurrogateProfileHandler handler, boolean updateSelfProfile) {
this.agent = agent;
store = registryDirectory;
surrogateProfileHandler = handler;
Assert.isNotNull(store, "Profile registry requires a directory"); //$NON-NLS-1$
findSelf();
this.updateSelfProfile = updateSelfProfile;
}
/**
* Determine the id of the "self" profile. This is only applicable for the registry
* of the currently running system.
*/
private void findSelf() {
//the location for the currently running system is registered as a service
final BundleContext context = EngineActivator.getContext();
if (context == null)
return;
ServiceReference<IAgentLocation> ref = context.getServiceReference(IAgentLocation.class);
if (ref == null)
return;
IAgentLocation location = context.getService(ref);
if (location == null)
return;
if (store.equals(getDefaultRegistryDirectory(location))) {
//we are the registry for the currently running system
self = context.getProperty("eclipse.p2.profile"); //$NON-NLS-1$
} else if (agent.getService(IProvisioningAgent.SHARED_CURRENT_AGENT) != null) {
// In shared mode, _SELF_ is the value of the current running profile for both agents current and shared
if (((IProvisioningAgent) agent.getService(IProvisioningAgent.SHARED_CURRENT_AGENT)).getService(IProvisioningAgent.SHARED_BASE_AGENT) == agent) {
self = context.getProperty("eclipse.p2.profile"); //$NON-NLS-1$
}
}
if (self == null)
self = (String) agent.getService("FORCED_SELF");
context.ungetService(ref);
}
public static File getDefaultRegistryDirectory(IAgentLocation agent) {
File registryDirectory = null;
if (agent == null)
throw new IllegalStateException("Profile Registry inialization failed: Agent Location is not available"); //$NON-NLS-1$
final URI engineDataArea = agent.getDataArea(EngineActivator.ID);
URI registryURL = URIUtil.append(engineDataArea, DEFAULT_STORAGE_DIR);
registryDirectory = new File(registryURL);
registryDirectory.mkdirs();
return registryDirectory;
}
/**
* If the current profile for self is marked as a roaming profile, we need
* to update its install and bundle pool locations.
*/
private void updateSelfProfile(Map<String, Profile> profileMap) {
if (profileMap == null)
return;
Profile selfProfile = profileMap.get(self);
if (selfProfile == null)
return;
//register default locale provider where metadata translations are found
//TODO ideally this should not be hard-coded to the current profile
TranslationSupport.getInstance().setTranslationSource(selfProfile);
if (DebugHelper.DEBUG_PROFILE_REGISTRY)
DebugHelper.debug(PROFILE_REGISTRY, "SimpleProfileRegistry.updateSelfProfile"); //$NON-NLS-1$
boolean changed = false;
//only update if self is a roaming profile
if (Boolean.parseBoolean(selfProfile.getProperty(IProfile.PROP_ROAMING)))
changed = updateRoamingProfile(selfProfile);
if (changed)
saveProfile(selfProfile);
}
private boolean updateRoamingProfile(Profile selfProfile) {
if (DebugHelper.DEBUG_PROFILE_REGISTRY)
DebugHelper.debug(PROFILE_REGISTRY, "SimpleProfileRegistry.updateRoamingProfile"); //$NON-NLS-1$
Location installLocation = ServiceHelper.getService(EngineActivator.getContext(), Location.class, Location.INSTALL_FILTER);
File location = new File(installLocation.getURL().getPath());
boolean changed = false;
if (!location.equals(new File(selfProfile.getProperty(IProfile.PROP_INSTALL_FOLDER)))) {
selfProfile.setProperty(IProfile.PROP_INSTALL_FOLDER, location.getAbsolutePath());
changed = true;
}
String propCache = selfProfile.getProperty(IProfile.PROP_CACHE);
if (propCache != null && !location.equals(new File(propCache))) {
selfProfile.setProperty(IProfile.PROP_CACHE, location.getAbsolutePath());
changed = true;
}
if (DebugHelper.DEBUG_PROFILE_REGISTRY)
DebugHelper.debug(PROFILE_REGISTRY, "SimpleProfileRegistry.updateRoamingProfile(changed=" + changed + ')'); //$NON-NLS-1$
return changed;
}
public synchronized String toString() {
return "Profile registry for location: " + store.getAbsolutePath() + "\n" + getProfileMap().toString(); //$NON-NLS-1$ //$NON-NLS-2$
}
public synchronized IProfile getProfile(String id) {
Profile profile = internalGetProfile(id);
if (profile == null)
return null;
return profile.snapshot();
}
public synchronized IProfile getProfile(String id, long timestamp) {
if (SELF.equals(id))
id = self;
if (profiles != null) {
IProfile profile = getProfile(id);
if (profile != null && profile.getTimestamp() == timestamp)
return profile;
}
File profileDirectory = getProfileFolder(id);
if (!profileDirectory.isDirectory())
return null;
File profileFile = new File(profileDirectory, Long.toString(timestamp) + PROFILE_GZ_EXT);
if (!profileFile.exists()) {
profileFile = new File(profileDirectory, Long.toString(timestamp) + PROFILE_EXT);
if (!profileFile.exists())
return null;
}
Parser parser = new Parser(EngineActivator.getContext(), EngineActivator.ID);
try {
parser.parse(profileFile);
} catch (IOException e) {
LogHelper.log(new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.error_parsing_profile, profileFile), e));
}
return parser.getProfileMap().get(id);
}
public synchronized long[] listProfileTimestamps(String id) {
if (SELF.equals(id))
id = self;
//guard against null self profile
if (id == null)
return new long[0];
File profileDirectory = getProfileFolder(id);
if (!profileDirectory.isDirectory())
return new long[0];
File[] profileFiles = profileDirectory.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return (pathname.getName().endsWith(PROFILE_EXT) || pathname.getName().endsWith(PROFILE_GZ_EXT)) && pathname.isFile() && !pathname.getName().startsWith("._"); //$NON-NLS-1$
}
});
long[] timestamps = new long[profileFiles.length];
for (int i = 0; i < profileFiles.length; i++) {
String filename = profileFiles[i].getName();
int extensionIndex = filename.lastIndexOf(PROFILE_EXT);
try {
timestamps[i] = Long.parseLong(filename.substring(0, extensionIndex));
} catch (NumberFormatException e) {
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$
}
}
Arrays.sort(timestamps);
return timestamps;
}
private Profile internalGetProfile(String id) {
if (SELF.equals(id))
id = self;
Profile profile = getProfileMap().get(id);
if (self != null && self.equals(id)) {
boolean resetProfile = false;
if (profile != null && ignoreExistingProfile(profile)) {
internalSetProfileStateProperty(profile, profile.getTimestamp(), IProfile.STATE_PROP_SHARED_INSTALL, IProfile.STATE_SHARED_INSTALL_VALUE_BEFOREFLUSH);
profile = null;
resetProfile = true;
}
if (profile == null) {
profile = createSurrogateProfile(id);
if (profile == null)
return null;
if (resetProfile) {
//Now that we created a new profile. Tag it, override the property and register the timestamp in the agent registry for pickup by other
internalSetProfileStateProperty(profile, profile.getTimestamp(), IProfile.STATE_PROP_SHARED_INSTALL, IProfile.STATE_SHARED_INSTALL_VALUE_NEW);
internalSetProfileStateProperty(profile, profile.getTimestamp(), SIMPLE_PROFILE_REGISTRY_INTERNAL + getBaseTimestamp(profile.getProfileId()), getBaseTimestamp(id));
//fragments support - remeber the property
internalSetProfileStateProperty(profile, profile.getTimestamp(), SIMPLE_PROFILE_REGISTRY_INTERNAL + getExtTimeStamp(), getExtTimeStamp());
agent.registerService(SERVICE_SHARED_INSTALL_NEW_TIMESTAMP, Long.toString(profile.getTimestamp()));
} else {
//This is the first time we create the shared profile. Tag it as such and also remember the timestamp of the base
internalSetProfileStateProperty(profile, profile.getTimestamp(), IProfile.STATE_PROP_SHARED_INSTALL, IProfile.STATE_SHARED_INSTALL_VALUE_INITIAL);
String baseTimestamp = getBaseTimestamp(id);
if (baseTimestamp != null)
internalSetProfileStateProperty(profile, profile.getTimestamp(), SIMPLE_PROFILE_REGISTRY_INTERNAL + baseTimestamp, baseTimestamp);
String extTimestamp = getExtTimeStamp();
internalSetProfileStateProperty(profile, profile.getTimestamp(), SIMPLE_PROFILE_REGISTRY_INTERNAL + extTimestamp, extTimestamp);
}
}
}
return profile;
}
// get timestamp of fragments (extensions)
private String getExtTimeStamp() {
long result = -1;
if (!EngineActivator.EXTENDED) {
return Long.toString(result);
}
File[] extensions = EngineActivator.getExtensionsDirectories();
for (File extension : extensions) {
if (extension.lastModified() > result) {
result = extension.lastModified();
}
}
return Long.toString(result);
}
private boolean ignoreExistingProfile(IProfile profile) {
if (agent.getService(SERVICE_SHARED_INSTALL_NEW_TIMESTAMP) != null)
return false;
String baseTimestamp = getBaseTimestamp(profile.getProfileId());
String extTimestamp = getExtTimeStamp();
if (baseTimestamp == null) {
return false;
}
boolean extensionOK = true;
if (surrogateProfileHandler != null && surrogateProfileHandler.isSurrogate(profile)) {
extensionOK = (internalGetProfileStateProperties(profile, SIMPLE_PROFILE_REGISTRY_INTERNAL + extTimestamp, false).size() != 0);
}
if ((internalGetProfileStateProperties(profile, SIMPLE_PROFILE_REGISTRY_INTERNAL + baseTimestamp, false).size() != 0) && extensionOK)
return false;
return true;
}
private String getBaseTimestamp(String id) {
IProvisioningAgent baseAgent = (IProvisioningAgent) agent.getService(IProvisioningAgent.SHARED_BASE_AGENT);
if (baseAgent == null)
return null;
IProfileRegistry registry = (IProfileRegistry) baseAgent.getService(IProfileRegistry.SERVICE_NAME);
if (registry == null)
return null;
long[] revisions = registry.listProfileTimestamps(id);
if (revisions.length >= 1) {
return Long.toString(revisions[revisions.length - 1]);
}
return null;
}
private Profile createSurrogateProfile(String id) {
if (surrogateProfileHandler == null)
return null;
Profile profile = (Profile) surrogateProfileHandler.createProfile(id);
if (profile == null)
return null;
saveProfile(profile);
resetProfiles();
return getProfileMap().get(id);
}
public synchronized IProfile[] getProfiles() {
Map<String, Profile> profileMap = getProfileMap();
Profile[] result = new Profile[profileMap.size()];
int i = 0;
for (Profile profile : profileMap.values()) {
result[i++] = profile.snapshot();
}
return result;
}
/**
* Returns an initialized map of String(Profile id)->Profile.
*/
protected Map<String, Profile> getProfileMap() {
if (profiles != null) {
Map<String, Profile> result = profiles.get();
if (result != null)
return result;
}
Map<String, Profile> result = restore();
if (result == null)
result = new LinkedHashMap<String, Profile>(8);
profiles = new SoftReference<Map<String, Profile>>(result);
if (updateSelfProfile) {
//update self profile on first load
updateSelfProfile(result);
}
return result;
}
public synchronized void updateProfile(Profile profile) {
String id = profile.getProfileId();
Profile current = getProfileMap().get(id);
if (current == null)
throw new IllegalArgumentException(NLS.bind(Messages.profile_does_not_exist, id));
ProfileLock lock = profileLocks.get(id);
lock.checkLocked();
current.clearLocalProperties();
current.clearInstallableUnits();
current.addProperties(profile.getLocalProperties());
IQueryResult<IInstallableUnit> queryResult = profile.query(QueryUtil.createIUAnyQuery(), null);
for (Iterator<IInstallableUnit> queryResultIt = queryResult.iterator(); queryResultIt.hasNext();) {
IInstallableUnit iu = queryResultIt.next();
current.addInstallableUnit(iu);
Map<String, String> iuProperties = profile.getInstallableUnitProperties(iu);
if (iuProperties != null)
current.addInstallableUnitProperties(iu, iuProperties);
}
saveProfile(current);
profile.clearOrphanedInstallableUnitProperties();
profile.setTimestamp(current.getTimestamp());
broadcastChangeEvent(id, IProfileEvent.CHANGED);
}
public IProfile addProfile(String id) throws ProvisionException {
return addProfile(id, null, null);
}
public IProfile addProfile(String id, Map<String, String> profileProperties) throws ProvisionException {
return addProfile(id, profileProperties, null);
}
public synchronized IProfile addProfile(String id, Map<String, String> profileProperties, String parentId) throws ProvisionException {
if (SELF.equals(id))
id = self;
Map<String, Profile> profileMap = getProfileMap();
if (profileMap.get(id) != null)
throw new ProvisionException(NLS.bind(Messages.Profile_Duplicate_Root_Profile_Id, id));
Profile parent = null;
if (parentId != null) {
if (SELF.equals(parentId))
parentId = self;
parent = profileMap.get(parentId);
if (parent == null)
throw new ProvisionException(NLS.bind(Messages.Profile_Parent_Not_Found, parentId));
}
Profile profile = new Profile(agent, id, parent, profileProperties);
if (surrogateProfileHandler != null && surrogateProfileHandler.isSurrogate(profile))
profile.setSurrogateProfileHandler(surrogateProfileHandler);
profileMap.put(id, profile);
saveProfile(profile);
broadcastChangeEvent(id, IProfileEvent.ADDED);
return profile.snapshot();
}
public synchronized void removeProfile(String profileId) {
if (SELF.equals(profileId))
profileId = self;
//note we need to maintain a reference to the profile map until it is persisted to prevent gc
Map<String, Profile> profileMap = getProfileMap();
Profile profile = profileMap.get(profileId);
if (profile == null)
return;
List<String> subProfileIds = profile.getSubProfileIds();
for (int i = 0; i < subProfileIds.size(); i++) {
removeProfile(subProfileIds.get(i));
}
internalLockProfile(profile);
// The above call recursively locked the parent(s). So save it away to rewind the locking process.
IProfile savedParent = profile.getParentProfile();
try {
profile.setParent(null);
} finally {
internalUnlockProfile(profile);
// The above call will not recurse since parent is now null. So do it explicitly.
if (savedParent != null) {
internalUnlockProfile(savedParent);
}
}
profileMap.remove(profileId);
profileLocks.remove(profileId);
// deleting the profile removes the folder and subsequently all
// the profile state properties as well since they are stored in a file in the folder.
deleteProfile(profileId);
broadcastChangeEvent(profileId, IProfileEvent.REMOVED);
}
public synchronized void removeProfile(String id, long timestamp) throws ProvisionException {
if (SELF.equals(id))
id = self;
if (profiles != null) {
IProfile profile = getProfile(id);
if (profile != null && profile.getTimestamp() == timestamp)
throw new ProvisionException(Messages.SimpleProfileRegistry_CannotRemoveCurrentSnapshot);
}
File profileDirectory = getProfileFolder(id);
if (!profileDirectory.isDirectory())
return;
File profileFile = new File(profileDirectory, Long.toString(timestamp) + PROFILE_GZ_EXT);
if (!profileFile.exists()) {
profileFile = new File(profileDirectory, Long.toString(timestamp) + PROFILE_EXT);
if (!profileFile.exists())
return;
}
FileUtils.deleteAll(profileFile);
// Ignore the return value here. If there was a problem removing the profile state
// properties we don't want to fail the whole operation since the profile state itself
// was removed successfully
removeProfileStateProperties(id, timestamp, null);
}
private void broadcastChangeEvent(String profileId, int reason) {
if (eventBus != null)
eventBus.publishEvent(new ProfileEvent(profileId, reason));
}
/**
* Restores the profile registry from disk, and returns the loaded profile map.
* Returns <code>null</code> if unable to read the registry.
*/
private Map<String, Profile> restore() {
if (store == null || !store.isDirectory())
throw new IllegalStateException(NLS.bind(Messages.reg_dir_not_available, store));
Parser parser = new Parser(EngineActivator.getContext(), EngineActivator.ID);
File[] profileDirectories = store.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.getName().endsWith(PROFILE_EXT) && pathname.isDirectory();
}
});
// protect against NPE
if (profileDirectories == null) {
parser.getProfileMap();
}
for (int i = 0; i < profileDirectories.length; i++) {
String directoryName = profileDirectories[i].getName();
String profileId = unescape(directoryName.substring(0, directoryName.lastIndexOf(PROFILE_EXT)));
ProfileLock lock = profileLocks.get(profileId);
if (lock == null) {
lock = new ProfileLock(this, profileDirectories[i]);
profileLocks.put(profileId, lock);
}
boolean locked = false;
if (lock.processHoldsLock() || (locked = lock.lock())) {
try {
File profileFile = findLatestProfileFile(profileDirectories[i]);
if (profileFile != null) {
try {
parser.parse(profileFile);
} catch (IOException e) {
LogHelper.log(new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.error_parsing_profile, profileFile), e));
}
}
} finally {
if (locked)
lock.unlock();
}
} else {
// could not lock the profile, so add a place holder
parser.addProfilePlaceHolder(profileId);
}
}
return parser.getProfileMap();
}
private File findLatestProfileFile(File profileDirectory) {
File latest = null;
long latestTimestamp = 0;
File[] profileFiles = profileDirectory.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return (pathname.getName().endsWith(PROFILE_GZ_EXT) || pathname.getName().endsWith(PROFILE_EXT)) && !pathname.isDirectory();
}
});
// protect against NPE
if (profileFiles == null)
return null;
for (int i = 0; i < profileFiles.length; i++) {
File profileFile = profileFiles[i];
String fileName = profileFile.getName();
try {
long timestamp = Long.parseLong(fileName.substring(0, fileName.indexOf(PROFILE_EXT)));
if (timestamp > latestTimestamp) {
latestTimestamp = timestamp;
latest = profileFile;
}
} catch (NumberFormatException e) {
// ignore
}
}
return latest;
}
private void saveProfile(Profile profile) {
File profileDirectory = getProfileFolder(profile.getProfileId());
profileDirectory.mkdir();
long previousTimestamp = profile.getTimestamp();
long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp <= previousTimestamp)
currentTimestamp = previousTimestamp + 1;
boolean shouldGzipFile = shouldGzipFile(profile);
File profileFile = new File(profileDirectory, Long.toString(currentTimestamp) + (shouldGzipFile ? PROFILE_GZ_EXT : PROFILE_EXT));
// Log a stack trace to see who is writing the profile.
if (DebugHelper.DEBUG_PROFILE_REGISTRY)
DebugHelper.debug(PROFILE_REGISTRY, "Saving profile to: " + profileFile.getAbsolutePath()); //$NON-NLS-1$
profile.setTimestamp(currentTimestamp);
profile.setChanged(false);
OutputStream os = null;
try {
if (shouldGzipFile)
os = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream(profileFile)));
else
os = new BufferedOutputStream(new FileOutputStream(profileFile));
Writer writer = new Writer(os);
writer.writeProfile(profile);
} catch (IOException e) {
profile.setTimestamp(previousTimestamp);
profileFile.delete();
LogHelper.log(new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.error_persisting_profile, profile.getProfileId()), e));
} finally {
try {
if (os != null)
os.close();
} catch (IOException e) {
// ignore
}
}
}
public void setEventBus(IProvisioningEventBus bus) {
this.eventBus = bus;
}
/**
* Returns whether the profile file for the given profile should be written in gzip format.
*/
private boolean shouldGzipFile(Profile profile) {
//check system property controlling compression
String format = EngineActivator.getContext().getProperty(EngineActivator.PROP_PROFILE_FORMAT);
if (format != null && format.equals(EngineActivator.PROFILE_FORMAT_UNCOMPRESSED))
return false;
//check whether the profile contains the p2 engine from 3.5.0 or earlier
return profile.available(QueryUtil.createIUQuery("org.eclipse.equinox.p2.engine", new VersionRange("[0.0.0, 1.0.101)")), null).isEmpty(); //$NON-NLS-1$//$NON-NLS-2$
}
private void deleteProfile(String profileId) {
File profileDirectory = getProfileFolder(profileId);
FileUtils.deleteAll(profileDirectory);
}
/**
* Converts a profile id into a string that can be used as a file name in any file system.
*/
public static String escape(String toEscape) {
StringBuffer buffer = new StringBuffer();
int length = toEscape.length();
for (int i = 0; i < length; ++i) {
char ch = toEscape.charAt(i);
switch (ch) {
case '\\' :
case '/' :
case ':' :
case '*' :
case '?' :
case '"' :
case '<' :
case '>' :
case '|' :
case '%' :
buffer.append("%" + (int) ch + ";"); //$NON-NLS-1$ //$NON-NLS-2$
break;
default :
buffer.append(ch);
}
}
return buffer.toString();
}
public static String unescape(String text) {
if (text.indexOf('%') == -1)
return text;
StringBuffer buffer = new StringBuffer();
int length = text.length();
for (int i = 0; i < length; ++i) {
char ch = text.charAt(i);
if (ch == '%') {
int colon = text.indexOf(';', i);
if (colon == -1)
throw new IllegalStateException("error unescaping the sequence at character (" + i + ") for " + text + ". Expected %{int};."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
ch = (char) Integer.parseInt(text.substring(i + 1, colon));
i = colon;
}
buffer.append(ch);
}
return buffer.toString();
}
static class Writer extends ProfileWriter {
public Writer(OutputStream output) throws IOException {
super(output, new ProcessingInstruction[] {ProcessingInstruction.makeTargetVersionInstruction(PROFILE_TARGET, ProfileXMLConstants.CURRENT_VERSION)});
}
}
/*
* Parser for the contents of a SimpleProfileRegistry,
* as written by the Writer class.
*/
class Parser extends ProfileParser {
private final Map<String, ProfileHandler> profileHandlers = new HashMap<String, ProfileHandler>();
public Map<String, ProfileHandler> getProfileHandlers() {
return Collections.unmodifiableMap(profileHandlers);
}
public Parser(BundleContext context, String bundleId) {
super(context, bundleId);
}
public void addProfilePlaceHolder(String profileId) {
profileHandlers.put(profileId, new ProfileHandler(profileId));
}
public void parse(File file) throws IOException {
InputStream is;
if (file.getName().endsWith(PROFILE_GZ_EXT)) {
is = new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)));
} else { // backward compatibility. SimpleProfileRegistry doesn't write non-gzipped profiles any more.
is = new BufferedInputStream(new FileInputStream(file));
}
parse(is);
}
public synchronized void parse(InputStream stream) throws IOException {
this.status = null;
try {
// TODO: currently not caching the parser since we make no assumptions
// or restrictions on concurrent parsing
getParser();
ProfileHandler profileHandler = new ProfileHandler();
xmlReader.setContentHandler(new ProfileDocHandler(PROFILE_ELEMENT, profileHandler));
xmlReader.parse(new InputSource(stream));
profileHandlers.put(profileHandler.getProfileId(), profileHandler);
} catch (SAXException e) {
throw new IOException(e.getMessage());
} catch (ParserConfigurationException e) {
throw new IOException(e.getMessage());
} finally {
stream.close();
}
}
protected Object getRootObject() {
return this;
}
public Map<String, Profile> getProfileMap() {
Map<String, Profile> profileMap = new HashMap<String, Profile>();
for (String profileId : profileHandlers.keySet()) {
addProfile(profileId, profileMap);
}
return profileMap;
}
private void addProfile(String profileId, Map<String, Profile> profileMap) {
if (profileMap.containsKey(profileId))
return;
ProfileHandler profileHandler = profileHandlers.get(profileId);
Profile parentProfile = null;
String parentId = profileHandler.getParentId();
if (parentId != null) {
addProfile(parentId, profileMap);
parentProfile = profileMap.get(parentId);
}
Profile profile = new Profile(agent, profileId, parentProfile, profileHandler.getProperties());
if (surrogateProfileHandler != null && surrogateProfileHandler.isSurrogate(profile))
profile.setSurrogateProfileHandler(surrogateProfileHandler);
profile.setTimestamp(profileHandler.getTimestamp());
IInstallableUnit[] ius = profileHandler.getInstallableUnits();
if (ius != null) {
for (int i = 0; i < ius.length; i++) {
IInstallableUnit iu = ius[i];
profile.addInstallableUnit(iu);
Map<String, String> iuProperties = profileHandler.getIUProperties(iu);
if (iuProperties != null) {
for (Entry<String, String> entry : iuProperties.entrySet()) {
profile.setInstallableUnitProperty(iu, entry.getKey(), entry.getValue());
}
}
}
}
profile.setChanged(false);
profileMap.put(profileId, profile);
}
private final class ProfileDocHandler extends DocHandler {
public ProfileDocHandler(String rootName, RootHandler rootHandler) {
super(rootName, rootHandler);
}
public void processingInstruction(String target, String data) throws SAXException {
if (ProfileXMLConstants.PROFILE_TARGET.equals(target)) {
Version repositoryVersion = extractPIVersion(target, data);
if (!ProfileXMLConstants.XML_TOLERANCE.isIncluded(repositoryVersion)) {
throw new SAXException(NLS.bind(Messages.SimpleProfileRegistry_Parser_Has_Incompatible_Version, repositoryVersion, ProfileXMLConstants.XML_TOLERANCE));
}
}
}
}
protected String getErrorMessage() {
return Messages.SimpleProfileRegistry_Parser_Error_Parsing_Registry;
}
public String toString() {
// TODO:
return null;
}
}
public synchronized boolean isCurrent(IProfile profile) {
Profile internalProfile = getProfileMap().get(profile.getProfileId());
if (internalProfile == null)
throw new IllegalArgumentException(NLS.bind(Messages.profile_not_registered, profile.getProfileId()));
if (!internalLockProfile(internalProfile))
throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
try {
return (!((Profile) profile).isChanged() && checkTimestamps(profile, internalProfile));
} finally {
internalUnlockProfile(internalProfile);
}
}
public synchronized void lockProfile(Profile profile) {
Profile internalProfile = internalGetProfile(profile.getProfileId());
if (internalProfile == null)
throw new IllegalArgumentException(NLS.bind(Messages.profile_not_registered, profile.getProfileId()));
if (!internalLockProfile(internalProfile))
throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
boolean isCurrent = false;
try {
if (profile.isChanged()) {
if (DebugHelper.DEBUG_PROFILE_REGISTRY)
DebugHelper.debug(PROFILE_REGISTRY, "Profile is marked as changed."); //$NON-NLS-1$
throw new IllegalStateException(NLS.bind(Messages.profile_changed, profile.getProfileId()));
}
if (!checkTimestamps(profile, internalProfile)) {
if (DebugHelper.DEBUG_PROFILE_REGISTRY)
DebugHelper.debug(PROFILE_REGISTRY, "Unexpected timestamp difference in profile."); //$NON-NLS-1$
throw new IllegalStateException(NLS.bind(Messages.profile_not_current, new String[] {profile.getProfileId(), Long.toString(internalProfile.getTimestamp()), Long.toString(profile.getTimestamp())}));
}
isCurrent = true;
} finally {
// this check is done here to ensure we unlock even if a runtime exception is thrown
if (!isCurrent)
internalUnlockProfile(internalProfile);
}
}
private boolean internalLockProfile(IProfile profile) {
ProfileLock lock = profileLocks.get(profile.getProfileId());
if (lock == null) {
lock = new ProfileLock(this, getProfileFolder(profile.getProfileId()));
profileLocks.put(profile.getProfileId(), lock);
}
return lock.lock();
}
private boolean checkTimestamps(IProfile profile, IProfile internalProfile) {
long[] timestamps = listProfileTimestamps(profile.getProfileId());
if (timestamps.length == 0) {
if (DebugHelper.DEBUG_PROFILE_REGISTRY)
DebugHelper.debug(PROFILE_REGISTRY, "check timestamp: expected " + profile.getTimestamp() + " but no profiles were found"); //$NON-NLS-1$ //$NON-NLS-2$
resetProfiles();
return false;
}
long currentTimestamp = (timestamps.length == 0) ? -1 : timestamps[timestamps.length - 1];
if (profile.getTimestamp() != currentTimestamp) {
if (DebugHelper.DEBUG_PROFILE_REGISTRY)
DebugHelper.debug(PROFILE_REGISTRY, "check timestamp: expected " + profile.getTimestamp() + " but was " + currentTimestamp); //$NON-NLS-1$ //$NON-NLS-2$
if (internalProfile.getTimestamp() != currentTimestamp)
resetProfiles();
return false;
}
return true;
}
/* (non-Javadoc)
* @see org.eclipse.equinox.internal.provisional.p2.engine.IProfileRegistry#containsProfile(java.lang.String)
*/
public synchronized boolean containsProfile(String id) {
if (SELF.equals(id))
id = self;
//null check done after self check, because self can be null
if (id == null)
return false;
// check profiles to avoid restoring the profile registry
if (profiles != null)
if (getProfile(id) != null)
return true;
File profileDirectory = getProfileFolder(id);
if (!profileDirectory.isDirectory())
return false;
File[] profileFiles = profileDirectory.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return (pathname.getName().endsWith(PROFILE_GZ_EXT) || pathname.getName().endsWith(PROFILE_EXT)) && pathname.isFile();
}
});
return profileFiles.length > 0;
}
public synchronized void resetProfiles() {
profiles = null;
}
public synchronized void unlockProfile(IProfile profile) {
if (profile == null)
throw new IllegalArgumentException(NLS.bind(Messages.profile_not_registered, "")); //$NON-NLS-1$
internalUnlockProfile(profile);
}
private void internalUnlockProfile(IProfile profile) {
ProfileLock lock = profileLocks.get(profile.getProfileId());
lock.unlock();
}
public Profile validate(IProfile candidate) {
if (candidate instanceof Profile)
return (Profile) candidate;
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$
}
public synchronized File getProfileDataDirectory(String id) {
if (SELF.equals(id))
id = self;
File profileDirectory = getProfileFolder(id);
File profileDataArea = new File(profileDirectory, DATA_EXT);
if (!profileDataArea.isDirectory() && !profileDataArea.mkdir())
throw new IllegalStateException("Could not create profile data area " + profileDataArea.getAbsolutePath() + "for: " + id); //$NON-NLS-1$ //$NON-NLS-2$
return profileDataArea;
}
/*(non-Javadoc)
* @see org.eclipse.equinox.p2.core.spi.IAgentService#start()
*/
public void start() {
//nothing to do
}
/*(non-Javadoc)
* @see org.eclipse.equinox.p2.core.spi.IAgentService#stop()
*/
public void stop() {
try {
//ensure there are no more profile preference save jobs running
Job.getJobManager().join(ProfilePreferences.PROFILE_SAVE_JOB_FAMILY, null);
} catch (InterruptedException e) {
//ignore
}
}
// Class representing a particular instance of a profile's state properties.
// Can be used for caching.
class ProfileStateProperties {
private String id;
private File file;
private long timestamp;
private Properties properties;
ProfileStateProperties(String id, File file, Properties properties) {
this.id = id;
this.file = file;
this.properties = properties;
this.timestamp = file.lastModified();
}
// return true if the cached timestamp is the same as the one on disk
boolean isCurrent() {
if (!file.exists())
return true;
return file.lastModified() == timestamp;
}
String getId() {
return id;
}
Properties getProperties() {
return this.properties;
}
}
/*
* Return the folder on disk associated with the profile with the given identifier.
*/
private File getProfileFolder(String id) {
return new File(store, escape(id) + PROFILE_EXT);
}
/*
* Read and return the state properties for the profile with the given id.
* If one does not exist, then return an empty Properties file.
* If there were problems reading the file then return throw an exception.
*/
private Properties readStateProperties(String id) throws ProvisionException {
if (SELF.equals(id))
id = self;
// if the last cached value is the one we are interested in and up-to-date
// then don't bother reading from disk
if (lastAccessedProperties != null && id.equals(lastAccessedProperties.getId()) && lastAccessedProperties.isCurrent())
return lastAccessedProperties.getProperties();
File profileDirectory = getProfileFolder(id);
if (!profileDirectory.isDirectory())
throw new ProvisionException(new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.SimpleProfileRegistry_Bad_profile_location, profileDirectory.getPath())));
File file = new File(profileDirectory, PROFILE_PROPERTIES_FILE);
Properties properties = new Properties();
if (!file.exists()) {
lastAccessedProperties = new ProfileStateProperties(id, file, properties);
return properties;
}
InputStream input = null;
try {
input = new BufferedInputStream(new FileInputStream(file));
properties.load(input);
} catch (IOException e) {
throw new ProvisionException(new Status(IStatus.ERROR, EngineActivator.ID, Messages.SimpleProfileRegistry_States_Error_Reading_File, e));
} finally {
if (input != null)
try {
input.close();
} catch (IOException e) {
// Ignore
}
}
//cache the value before we return
lastAccessedProperties = new ProfileStateProperties(id, file, properties);
return properties;
}
/*
* Write the given state properties to disk for the specified profile.
*/
private IStatus writeStateProperties(String id, Properties properties) {
if (SELF.equals(id))
id = self;
File profileDirectory = getProfileFolder(id);
File file = new File(profileDirectory, PROFILE_PROPERTIES_FILE);
OutputStream output = null;
Properties prunedProperties = properties;
try {
output = new BufferedOutputStream(new FileOutputStream(file));
prunedProperties = pruneStateProperties(id, properties);
prunedProperties.store(output, null);
output.flush();
} catch (IOException e) {
return new Status(IStatus.ERROR, EngineActivator.ID, Messages.SimpleProfileRegistry_States_Error_Writing_File, e);
} finally {
try {
if (output != null)
output.close();
} catch (IOException e) {
// ignore
}
}
// cache the value
lastAccessedProperties = new ProfileStateProperties(id, file, prunedProperties);
return Status.OK_STATUS;
}
// Only write state properties for state timestamps that still exist
// TODO: Do we want to expose this method as API?
// TODO: Do we want to run this method on every write or just after specific elapsed times since the last Prune?
private Properties pruneStateProperties(String id, Properties properties) {
Properties result = new Properties();
long[] timestamps = listProfileTimestamps(id);
HashSet<String> timestampsSet = new HashSet<String>(timestamps.length);
for (int i = 0; i < timestamps.length; i++) {
timestampsSet.add(String.valueOf(timestamps[i]));
}
Enumeration<Object> keys = properties.keys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
int index = key.indexOf('.');
if (index > -1) {
String timestamp = key.substring(0, index);
if (timestampsSet.contains(timestamp)) {
result.put(key, properties.get(key));
}
}
}
return result;
}
/*
* Ensure a profile with the given identifier has a state with the specified timestamp. Return
* a status object indicating success or failure.
*/
private IStatus validateState(String id, long timestamp) {
long[] states = listProfileTimestamps(id);
for (long ts : states)
if (ts == timestamp)
return Status.OK_STATUS;
return new Status(IStatus.ERROR, EngineActivator.ID, (NLS.bind(Messages.SimpleProfileRegistry_state_not_found, timestamp, id)));
}
/* (non-Javadoc)
* @see org.eclipse.equinox.p2.engine.IProfileRegistry#setProfileStateProperties(java.lang.String, long, java.util.Map)
*/
public IStatus setProfileStateProperties(String id, long timestamp, Map<String, String> propertiesToAdd) {
if (id == null || propertiesToAdd == null)
throw new NullPointerException();
Profile internalProfile = internalGetProfile(id);
return internalSetProfileStateProperties(internalProfile, timestamp, propertiesToAdd);
}
private IStatus internalSetProfileStateProperties(IProfile profile, long timestamp, Map<String, String> propertiesToAdd) {
IStatus result = validateState(profile.getProfileId(), timestamp);
if (!result.isOK())
return result;
if (!internalLockProfile(profile))
throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
try {
Properties properties = readStateProperties(profile.getProfileId());
for (Map.Entry<String, String> entry : propertiesToAdd.entrySet()) {
// property key format is timestamp.key
properties.put(timestamp + "." + entry.getKey(), entry.getValue()); //$NON-NLS-1$
}
writeStateProperties(profile.getProfileId(), properties);
} catch (ProvisionException e) {
return e.getStatus();
} finally {
internalUnlockProfile(profile);
}
return Status.OK_STATUS;
}
/* (non-Javadoc)
* @see org.eclipse.equinox.p2.engine.IProfileRegistry#setProfileStateProperty(java.lang.String, long, java.lang.String, java.lang.String)
*/
public IStatus setProfileStateProperty(String id, long timestamp, String key, String value) {
if (id == null)
throw new NullPointerException();
return internalSetProfileStateProperty(internalGetProfile(id), timestamp, key, value);
}
private IStatus internalSetProfileStateProperty(IProfile profile, long timestamp, String key, String value) {
if (key == null || value == null)
throw new NullPointerException();
Map<String, String> properties = new HashMap<String, String>();
properties.put(key, value);
return internalSetProfileStateProperties(profile, timestamp, properties);
}
public Map<String, String> getProfileStateProperties(String id, long timestamp) {
if (id == null)
throw new NullPointerException();
return internalGetProfileStateProperties(internalGetProfile(id), timestamp, true);
}
private Map<String, String> internalGetProfileStateProperties(IProfile profile, long timestamp, boolean lock) {
Map<String, String> result = new HashMap<String, String>();
String timestampString = String.valueOf(timestamp);
int keyOffset = timestampString.length() + 1;
lock = lock || lastAccessedProperties == null;
if (lock)
if (!internalLockProfile(profile))
throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
try {
Properties properties = readStateProperties(profile.getProfileId());
Iterator<Object> keys = properties.keySet().iterator();
while (keys.hasNext()) {
String key = (String) keys.next();
if (key.indexOf(timestampString) == 0)
result.put(key.substring(keyOffset), properties.getProperty(key));
}
} catch (ProvisionException e) {
LogHelper.log(e);
} finally {
if (lock)
internalUnlockProfile(profile);
}
return result;
}
/* (non-Javadoc)
* @see org.eclipse.equinox.p2.engine.IProfileRegistry#getProfileStateProperties(java.lang.String, java.lang.String)
*/
public Map<String, String> getProfileStateProperties(String id, String userKey) {
if (id == null || userKey == null)
throw new NullPointerException();
Profile internalProfile = internalGetProfile(id);
return internalGetProfileStateProperties(internalProfile, userKey, true);
}
private Map<String, String> internalGetProfileStateProperties(IProfile profile, String userKey, boolean lock) {
Map<String, String> result = new HashMap<String, String>();
lock = lock || lastAccessedProperties == null;
if (lock)
if (!internalLockProfile(profile))
throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
try {
Properties properties = readStateProperties(profile.getProfileId());
Iterator<Object> keys = properties.keySet().iterator();
while (keys.hasNext()) {
// property key format is timestamp.key
String key = (String) keys.next();
int index = key.indexOf('.');
if (index != -1 && index + 1 != key.length() && key.substring(index + 1).equals(userKey)) {
result.put(key.substring(0, index), properties.getProperty(key));
}
}
} catch (ProvisionException e) {
LogHelper.log(e);
} finally {
if (lock)
internalUnlockProfile(profile);
}
return result;
}
/* (non-Javadoc)
* @see org.eclipse.equinox.p2.engine.IProfileRegistry#removeProfileStateProperties(java.lang.String, long, java.util.Collection)
*/
public IStatus removeProfileStateProperties(String id, long timestamp, Collection<String> keys) {
if (id == null)
throw new NullPointerException();
// return if there is no work to do
if (keys != null && keys.size() == 0)
return Status.OK_STATUS;
Profile internalProfile = internalGetProfile(id);
if (!internalLockProfile(internalProfile))
throw new IllegalStateException(Messages.SimpleProfileRegistry_Profile_in_use);
try {
Properties properties = readStateProperties(id);
String timestampString = String.valueOf(timestamp);
if (keys == null) {
// remove all keys
for (Iterator<Object> already = properties.keySet().iterator(); already.hasNext();) {
String key = (String) already.next();
// property key is timestamp.key
if (key.indexOf(timestampString) == 0)
already.remove();
}
} else {
for (String key : keys) {
// property key format is timestamp.key
if (key != null)
properties.remove(timestampString + "." + key); //$NON-NLS-1$
}
}
writeStateProperties(id, properties);
} catch (ProvisionException e) {
return e.getStatus();
} finally {
internalUnlockProfile(internalProfile);
}
return Status.OK_STATUS;
}
}