blob: 9084845683b256b9c0c7e6a89c33d727e2710fec [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2020 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Christoph Läubrich - Bug 568865 - [target] add advanced editing capabilities for custom target platforms
*******************************************************************************/
package org.eclipse.pde.internal.core.target;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.stream.Stream.concat;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.equinox.frameworkadmin.BundleInfo;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.core.target.ITargetDefinition;
import org.eclipse.pde.core.target.TargetBundle;
import org.eclipse.pde.core.target.TargetFeature;
import org.eclipse.pde.internal.core.P2Utils;
import org.eclipse.pde.internal.core.PDECore;
/**
* A bundle container representing an installed profile.
*
* @since 3.5
*/
public class ProfileBundleContainer extends AbstractBundleContainer {
// The following constants are duplicated from org.eclipse.equinox.internal.p2.core.Activator
private static final String CONFIG_INI = "config.ini"; //$NON-NLS-1$
private static final String PROP_AGENT_DATA_AREA = "eclipse.p2.data.area"; //$NON-NLS-1$
private static final String PROP_PROFILE = "eclipse.p2.profile"; //$NON-NLS-1$
private static final String PROP_CONFIG_DIR = "osgi.configuration.area"; //$NON-NLS-1$
private static final String PROP_USER_DIR = "user.dir"; //$NON-NLS-1$
private static final String PROP_USER_HOME = "user.home"; //$NON-NLS-1$
private static final String VAR_CONFIG_DIR = "@config.dir"; //$NON-NLS-1$
private static final String VAR_USER_DIR = "@user.dir"; //$NON-NLS-1$
private static final String VAR_USER_HOME = "@user.home"; //$NON-NLS-1$
/**
* Constant describing the type of bundle container
*/
public static final String TYPE = "Profile"; //$NON-NLS-1$
/**
* Path to home/root install location. May contain string variables.
*/
private final String fHome;
/**
* Alternate configuration location or <code>null</code> if default.
* May contain string variables.
*/
private final String fConfiguration;
/**
* Creates a new bundle container for the profile at the specified location.
*
* @param home path in local file system, may contain string variables
* @param configurationLocation alternate configuration location or <code>null</code> for default,
* may contain string variables
*/
public ProfileBundleContainer(String home, String configurationLocation) {
fHome = home;
fConfiguration = configurationLocation;
}
@Override
public String getLocation(boolean resolve) throws CoreException {
if (resolve) {
return resolveHomeLocation().toOSString();
}
return fHome;
}
@Override
public String getType() {
return TYPE;
}
/**
* Returns the configuration area for this container if one was specified during creation.
*
* @return string path to configuration location or <code>null</code>
*/
public String getConfigurationLocation() {
return fConfiguration;
}
@Override
protected TargetBundle[] resolveBundles(ITargetDefinition definition, IProgressMonitor monitor) throws CoreException {
String home = resolveHomeLocation().toOSString();
if (!new File(home).isDirectory()) {
throw new CoreException(Status.error(NLS.bind(Messages.ProfileBundleContainer_0, home)));
}
File configurationArea = getConfigurationArea();
if (configurationArea != null) {
if (!configurationArea.isDirectory()) {
throw new CoreException(Status.error(NLS.bind(Messages.ProfileBundleContainer_2, home)));
}
}
BundleInfo[] infos = P2Utils.readBundles(home, configurationArea);
if (infos == null) {
if (configurationArea != null) {
Collection<TargetBundle> osgiBundles = readBundleInfosFromConfigIni(configurationArea, new File(home));
if (!osgiBundles.isEmpty()) {
return osgiBundles.toArray(new TargetBundle[0]);
}
}
DirectoryBundleContainer directoryBundleContainer = new DirectoryBundleContainer(home);
directoryBundleContainer.resolve(definition, monitor);
TargetBundle[] platformBundles = directoryBundleContainer.getBundles();
if (platformBundles != null) {
return platformBundles;
}
infos = new BundleInfo[0];
}
if (monitor.isCanceled()) {
return new TargetBundle[0];
}
BundleInfo[] source = P2Utils.readSourceBundles(home, configurationArea);
if (source == null) {
source = new BundleInfo[0];
}
SubMonitor localMonitor = SubMonitor.convert(monitor, Messages.DirectoryBundleContainer_0, infos.length + source.length);
return concat(stream(infos), stream(source)).parallel().map(info -> {
URI location = info.getLocation();
try {
if (monitor.isCanceled()) {
return null;
}
return new TargetBundle(URIUtil.toFile(location));
} catch (CoreException e) {
return new InvalidTargetBundle(new BundleInfo(location), e.getStatus());
} finally {
localMonitor.split(1);
}
}).filter(Objects::nonNull).toArray(TargetBundle[]::new);
}
private Collection<TargetBundle> readBundleInfosFromConfigIni(File configArea, File home) {
File configIni = new File(configArea, CONFIG_INI);
if (!configIni.isFile()) {
return emptyList();
}
Properties configProps = new Properties();
try (FileInputStream fis = new FileInputStream(configIni)) {
configProps.load(fis);
} catch (IOException e) {
PDECore.log(e);
return emptyList();
}
List<File> bundleFiles = parseBundlesFromConfigIni(configProps, home);
ArrayList<TargetBundle> bundles = new ArrayList<>();
for (File file : bundleFiles) {
if (!file.exists()) {
continue;
}
TargetBundle bundle;
try {
bundle = new TargetBundle(file);
} catch (CoreException e) {
bundle = new InvalidTargetBundle(new BundleInfo(file.toURI()), e.getStatus());
}
bundles.add(bundle);
}
return bundles;
}
public static List<File> parseBundlesFromConfigIni(Properties configProps, File home) {
String osgiBundles = configProps.getProperty("osgi.bundles"); //$NON-NLS-1$
if (osgiBundles == null || osgiBundles.isEmpty()) {
return emptyList();
}
ArrayList<File> bundles = new ArrayList<>();
File baseDir = null;
String osgiFramework = configProps.getProperty("osgi.framework"); //$NON-NLS-1$
if (osgiFramework != null) {
File frameworkBundle = parseBundleLocation(osgiFramework);
if (!frameworkBundle.isAbsolute()) {
frameworkBundle = new File(home, frameworkBundle.getPath());
}
bundles.add(frameworkBundle);
baseDir = frameworkBundle.getParentFile();
}
for (String spec : osgiBundles.split(",")) { //$NON-NLS-1$
File location = parseBundleLocation(spec);
if (baseDir == null || location.isAbsolute()) {
bundles.add(location);
} else {
bundles.add(new File(baseDir, location.getPath()));
}
}
return bundles;
}
private static File parseBundleLocation(String spec) {
String path = spec.split("@", 2)[0]; //$NON-NLS-1$
path = trimPrefix(path, "reference:"); //$NON-NLS-1$
path = trimPrefix(path, "file:"); //$NON-NLS-1$
return new File(path);
}
private static String trimPrefix(String string, String prefix) {
if (string.startsWith(prefix)) {
return string.substring(prefix.length());
}
return string;
}
@Override
protected TargetFeature[] resolveFeatures(ITargetDefinition definition, IProgressMonitor monitor) throws CoreException {
if (definition instanceof TargetDefinition) {
return ((TargetDefinition) definition).resolveFeatures(getLocation(false), monitor);
}
return new TargetFeature[0];
}
/**
* Returns the home location with all variables resolved as a path.
*
* @return resolved home location
* @throws CoreException
*/
private IPath resolveHomeLocation() throws CoreException {
return new Path(resolveVariables(fHome));
}
/**
* Returns a URL to the configuration area associated with this profile or <code>null</code>
* if none.
*
* @return configuration area URL or <code>null</code>
* @throws CoreException if unable to generate a URL or the user specified location does not exist
*/
private File getConfigurationArea() throws CoreException {
IPath home = resolveHomeLocation();
IPath configuration = null;
if (fConfiguration == null) {
configuration = home.append("configuration"); //$NON-NLS-1$
} else {
configuration = new Path(resolveVariables(fConfiguration));
}
File file = configuration.toFile();
if (file.exists()) {
return file;
} else if (fConfiguration != null) {
// If the user specified config area does not exist throw an error
throw new CoreException(
Status.error(NLS.bind(Messages.ProfileBundleContainer_2, configuration.toOSString())));
}
return null;
}
@Override
public boolean equals(Object o) {
if (o instanceof ProfileBundleContainer) {
ProfileBundleContainer pbc = (ProfileBundleContainer) o;
return fHome.equals(pbc.fHome) && isNullOrEqual(pbc.fConfiguration, fConfiguration);
}
return false;
}
@Override
public int hashCode() {
int hash = fHome.hashCode();
if (fConfiguration != null) {
hash += fConfiguration.hashCode();
}
return hash;
}
/**
* Returns the location of the profile file that describes the installation this container represents or <code>null</code>
* if no profile file could be determined. This method checks the configuration file for a p2 data area entry and profile name
* to determine where the profile is located.
* <p>
* Note that when self hosting, the returned profile location will not have all running plug-ins installed unless the launch has generated
* a complete profile.
* </p>
*
* @return the profile file or <code>null</code>
*/
public File getProfileFileLocation() throws CoreException {
// Get the configuration location
String home = resolveHomeLocation().toOSString();
if (!new File(home).isDirectory()) {
throw new CoreException(Status.error(NLS.bind(Messages.ProfileBundleContainer_0, home)));
}
File configArea = getConfigurationArea();
if (configArea == null) {
configArea = new File(home);
}
if (!configArea.isDirectory()) {
throw new CoreException(Status.error(NLS.bind(Messages.ProfileBundleContainer_2, configArea)));
}
// Location of the profile
File p2DataArea = null;
String profileName = null;
// Load the config.ini to try and find the backing profile
File configIni = new File(configArea, CONFIG_INI);
if (configIni.isFile()) {
// Read config.ini
Properties configProps = new Properties();
try (FileInputStream fis = new FileInputStream(configIni)) {
configProps.load(fis);
} catch (IOException e) {
PDECore.log(e);
}
String p2Area = configProps.getProperty(PROP_AGENT_DATA_AREA);
if (p2Area != null) {
if (p2Area.startsWith(VAR_USER_HOME)) {
String base = substituteVar(configProps, p2Area, VAR_USER_HOME, PROP_USER_HOME, configArea);
p2Area = new Path(base).toFile().getAbsolutePath();
} else if (p2Area.startsWith(VAR_USER_DIR)) {
String base = substituteVar(configProps, p2Area, VAR_USER_DIR, PROP_USER_DIR, configArea);
p2Area = new Path(base).toFile().getAbsolutePath();
} else if (p2Area.startsWith(VAR_CONFIG_DIR)) {
String base = substituteVar(configProps, p2Area, VAR_CONFIG_DIR, PROP_CONFIG_DIR, configArea);
p2Area = new Path(base).toFile().getAbsolutePath();
}
p2DataArea = new File(p2Area);
}
profileName = configProps.getProperty(PROP_PROFILE);
}
if (p2DataArea == null || !p2DataArea.isDirectory()) {
p2DataArea = new File(configArea, "p2"); //$NON-NLS-1$
}
if (profileName == null || profileName.length() == 0) {
profileName = "SDKProfile"; //$NON-NLS-1$
}
@SuppressWarnings("restriction")
File profile = new Path(p2DataArea.getAbsolutePath())
.append(org.eclipse.equinox.internal.p2.engine.EngineActivator.ID).append("profileRegistry") //$NON-NLS-1$
.append(profileName + ".profile").toFile(); //$NON-NLS-1$
if (profile.exists()) {
return profile;
}
return null;
}
/**
* Replaces a variable in config.ini
* @param props properties containing entries from the
* @param source the string to replace the var in
* @param var the variable to replace
* @param prop the property to lookup for a replacement value
* @param defaultValue value to use if the property can't be found
* @return source string with the variable replaced with the proper value
*/
private String substituteVar(Properties props, String source, String var, String prop, File defaultValue) {
String value = props.getProperty(prop);
if (value == null) {
value = defaultValue.getAbsolutePath();
}
return value + source.substring(var.length());
}
private boolean isNullOrEqual(Object o1, Object o2) {
if (o1 == null) {
return o2 == null;
}
if (o2 == null) {
return false;
}
return o1.equals(o2);
}
@Override
public String toString() {
return new StringBuilder("Installation ").append(fHome).append(' ') //$NON-NLS-1$
.append(fConfiguration == null ? "Default Configuration" : fConfiguration).toString(); //$NON-NLS-1$
}
public void reload() {
clearResolutionStatus();
}
}