blob: 3999a4057689e4a2dde3a31ec353e2232baec0d0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 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
*******************************************************************************/
package org.eclipse.pde.internal.core;
import static java.util.Collections.singletonMap;
import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.osgi.service.resolver.BaseDescription;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.service.resolver.BundleSpecification;
import org.eclipse.osgi.service.resolver.ExportPackageDescription;
import org.eclipse.osgi.service.resolver.HostSpecification;
import org.eclipse.osgi.service.resolver.ImportPackageSpecification;
import org.eclipse.osgi.service.resolver.StateHelper;
import org.eclipse.pde.core.IClasspathContributor;
import org.eclipse.pde.core.build.IBuild;
import org.eclipse.pde.core.build.IBuildEntry;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.build.IBuildPropertiesConstants;
import org.eclipse.pde.internal.core.ibundle.IBundlePluginModelBase;
public class RequiredPluginsClasspathContainer extends PDEClasspathContainer implements IClasspathContainer {
@SuppressWarnings("nls")
private static final Collection<String> JUNIT5_RUNTIME_PLUGINS = new HashSet<>(
Arrays.asList("org.junit", "org.junit.jupiter.api", "org.junit.jupiter.engine",
"org.junit.platform.commons", "org.junit.platform.engine", "org.hamcrest.core", "org.opentest4j"));
private final IPluginModelBase fModel;
private IBuild fBuild;
private IClasspathEntry[] fEntries;
private boolean addImportedPackages;
/**
* Cached list of {@link IClasspathContributor} from plug-in extensions
* @see #getClasspathContributors()
*/
private static List<IClasspathContributor> fClasspathContributors = null;
/**
* Constructor for RequiredPluginsClasspathContainer.
*/
public RequiredPluginsClasspathContainer(IPluginModelBase model) {
this(model, null);
}
public RequiredPluginsClasspathContainer(IPluginModelBase model, IBuild build) {
fModel = model;
fBuild = build;
}
@Override
public int getKind() {
return K_APPLICATION;
}
@Override
public IPath getPath() {
return PDECore.REQUIRED_PLUGINS_CONTAINER_PATH;
}
@Override
public String getDescription() {
return PDECoreMessages.RequiredPluginsClasspathContainer_description;
}
@Override
public IClasspathEntry[] getClasspathEntries() {
if (fModel == null) {
if (PDECore.DEBUG_CLASSPATH) {
System.out.println("********Returned an empty container"); //$NON-NLS-1$
}
return new IClasspathEntry[0];
}
if (fEntries == null) {
fEntries = computePluginEntries();
}
if (PDECore.DEBUG_CLASSPATH) {
System.out.println("Dependencies for plugin '" + fModel.getPluginBase().getId() + "':"); //$NON-NLS-1$ //$NON-NLS-2$
for (IClasspathEntry entry : fEntries) {
System.out.println("\t" + entry); //$NON-NLS-1$
}
}
return fEntries;
}
private IClasspathEntry[] computePluginEntries() {
ArrayList<IClasspathEntry> entries = new ArrayList<>();
try {
BundleDescription desc = fModel.getBundleDescription();
if (desc == null) {
return new IClasspathEntry[0];
}
Map<BundleDescription, ArrayList<Rule>> map = retrieveVisiblePackagesFromState(desc);
// Add any library entries contributed via classpath contributor extension (Bug 363733)
for (IClasspathContributor cc : getClasspathContributors()) {
List<IClasspathEntry> classpathEntries = cc.getInitialEntries(desc);
if (classpathEntries == null || classpathEntries.isEmpty()) {
continue;
}
entries.addAll(classpathEntries);
}
HashSet<BundleDescription> added = new HashSet<>();
// to avoid cycles, e.g. when a bundle imports a package it exports
added.add(desc);
HostSpecification host = desc.getHost();
if (host != null) {
addHostPlugin(host, added, map, entries);
} else if ("true".equals(System.getProperty("pde.allowCycles"))) { //$NON-NLS-1$ //$NON-NLS-2$
BundleDescription[] fragments = desc.getFragments();
for (BundleDescription fragment : fragments) {
if (fragment.isResolved()) {
addPlugin(fragment, false, map, entries);
}
}
}
// add dependencies
BundleSpecification[] required = desc.getRequiredBundles();
for (BundleSpecification element : required) {
addDependency((BundleDescription) element.getSupplier(), added, map, entries);
}
if (fBuild == null) {
fBuild = ClasspathUtilCore.getBuild(fModel);
}
if (fBuild != null) {
addSecondaryDependencies(desc, added, entries);
}
// add Import-Package
// sort by symbolicName_version to get a consistent order
Map<String, BundleDescription> sortedMap = new TreeMap<>();
Iterator<BundleDescription> iter = map.keySet().iterator();
while (iter.hasNext()) {
BundleDescription bundle = iter.next();
sortedMap.put(bundle.toString(), bundle);
}
iter = sortedMap.values().iterator();
while (iter.hasNext()) {
BundleDescription bundle = iter.next();
IPluginModelBase model = PluginRegistry.findModel(bundle);
if (model != null && model.isEnabled()) {
addDependencyViaImportPackage(model.getBundleDescription(), added, map, entries);
}
}
if (fBuild != null) {
addExtraClasspathEntries(added, entries);
}
addJunit5RuntimeDependencies(added, entries);
} catch (CoreException e) {
}
return entries.toArray(new IClasspathEntry[entries.size()]);
}
/**
* Return the list of {@link IClasspathContributor}s provided by the
* <code>org.eclipse.pde.core.pluginClasspathContributors</code> extension point.
* @return list of classpath contributors from the extension point
*/
synchronized private static List<IClasspathContributor> getClasspathContributors() {
if (fClasspathContributors == null) {
fClasspathContributors = new ArrayList<>();
IExtensionRegistry registry = Platform.getExtensionRegistry();
IConfigurationElement[] elements = registry.getConfigurationElementsFor("org.eclipse.pde.core.pluginClasspathContributors"); //$NON-NLS-1$
for (IConfigurationElement element : elements) {
try {
fClasspathContributors.add((IClasspathContributor) element.createExecutableExtension("class")); //$NON-NLS-1$
} catch (CoreException e) {
PDECore.log(e.getStatus());
}
}
}
return fClasspathContributors;
}
private Map<BundleDescription, ArrayList<Rule>> retrieveVisiblePackagesFromState(BundleDescription desc) {
Map<BundleDescription, ArrayList<Rule>> visiblePackages = new HashMap<>();
StateHelper helper = Platform.getPlatformAdmin().getStateHelper();
addVisiblePackagesFromState(helper, desc, visiblePackages);
if (desc.getHost() != null) {
addVisiblePackagesFromState(helper, (BundleDescription) desc.getHost().getSupplier(), visiblePackages);
}
return visiblePackages;
}
private void addVisiblePackagesFromState(StateHelper helper, BundleDescription desc, Map<BundleDescription, ArrayList<Rule>> visiblePackages) {
if (desc == null) {
return;
}
ExportPackageDescription[] exports = helper.getVisiblePackages(desc);
for (ExportPackageDescription export : exports) {
BundleDescription exporter = export.getExporter();
if (exporter == null) {
continue;
}
ArrayList<Rule> list = visiblePackages.get(exporter);
if (list == null) {
list = new ArrayList<>();
visiblePackages.put(exporter, list);
}
Rule rule = getRule(helper, desc, export);
if (!list.contains(rule)) {
list.add(rule);
}
}
}
private Rule getRule(StateHelper helper, BundleDescription desc, ExportPackageDescription export) {
Rule rule = new Rule();
rule.discouraged = helper.getAccessCode(desc, export) == StateHelper.ACCESS_DISCOURAGED;
String name = export.getName();
rule.path = (name.equals(".")) ? new Path("*") : new Path(name.replace('.', '/') + "/*"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return rule;
}
protected void addDependencyViaImportPackage(BundleDescription desc, HashSet<BundleDescription> added, Map<BundleDescription, ArrayList<Rule>> map, ArrayList<IClasspathEntry> entries) throws CoreException {
if (desc == null || !added.add(desc)) {
return;
}
addPlugin(desc, true, map, entries);
if (hasExtensibleAPI(desc) && desc.getContainingState() != null) {
BundleDescription[] fragments = desc.getFragments();
for (BundleDescription fragment : fragments) {
if (fragment.isResolved()) {
addDependencyViaImportPackage(fragment, added, map, entries);
}
}
}
}
private void addDependency(BundleDescription desc, HashSet<BundleDescription> added, Map<BundleDescription, ArrayList<Rule>> map, ArrayList<IClasspathEntry> entries) throws CoreException {
addDependency(desc, added, map, entries, true);
}
private void addDependency(BundleDescription desc, HashSet<BundleDescription> added, Map<BundleDescription, ArrayList<Rule>> map, ArrayList<IClasspathEntry> entries, boolean useInclusion) throws CoreException {
if (desc == null || !added.add(desc)) {
return;
}
BundleDescription[] fragments = hasExtensibleAPI(desc) ? desc.getFragments() : new BundleDescription[0];
// add fragment patches before host
for (BundleDescription fragment : fragments) {
if (fragment.isResolved() && ClasspathUtilCore.isPatchFragment(fragment)) {
addDependency(fragment, added, map, entries, useInclusion);
}
}
addPlugin(desc, useInclusion, map, entries);
// add fragments that are not patches after the host
for (int i = 0; i < fragments.length; i++) {
if (fragments[i].isResolved() && !ClasspathUtilCore.isPatchFragment(fragments[i])) {
addDependency(fragments[i], added, map, entries, useInclusion);
}
}
BundleSpecification[] required = desc.getRequiredBundles();
for (BundleSpecification element : required) {
if (element.isExported()) {
addDependency((BundleDescription) element.getSupplier(), added, map, entries, useInclusion);
}
}
if (addImportedPackages) {
// add Import-Package
ImportPackageSpecification[] imports = desc.getImportPackages();
for (ImportPackageSpecification importSpec : imports) {
BaseDescription supplier = importSpec.getSupplier();
if (supplier instanceof ExportPackageDescription) {
addDependencyViaImportPackage(((ExportPackageDescription) supplier).getExporter(), added, map,
entries);
}
}
}
}
private boolean addPlugin(BundleDescription desc, boolean useInclusions, Map<BundleDescription, ArrayList<Rule>> map, ArrayList<IClasspathEntry> entries) throws CoreException {
IPluginModelBase model = PluginRegistry.findModel(desc);
if (model == null || !model.isEnabled()) {
return false;
}
IResource resource = model.getUnderlyingResource();
Rule[] rules = useInclusions ? getInclusions(map, model) : null;
BundleDescription hostBundle = fModel.getBundleDescription();
if (desc == null) {
return false;
}
// Add any library entries contributed via classpath contributor extension (Bug 363733)
for (IClasspathContributor cc : getClasspathContributors()) {
List<IClasspathEntry> classpathEntries = cc.getEntriesForDependency(hostBundle, desc);
if (classpathEntries == null || classpathEntries.isEmpty()) {
continue;
}
entries.addAll(classpathEntries);
}
if (resource != null) {
addProjectEntry(resource.getProject(), rules, model.getPluginBase().exportsExternalAnnotations(), entries);
} else {
addExternalPlugin(model, rules, entries);
}
return true;
}
private Rule[] getInclusions(Map<BundleDescription, ArrayList<Rule>> map, IPluginModelBase model) {
BundleDescription desc = model.getBundleDescription();
if (desc == null || "false".equals(System.getProperty("pde.restriction")) //$NON-NLS-1$ //$NON-NLS-2$
|| !(fModel instanceof IBundlePluginModelBase) || TargetPlatformHelper.getTargetVersion() < 3.1) {
return null;
}
Rule[] rules;
if (desc.getHost() != null) {
rules = getInclusions(map, (BundleDescription) desc.getHost().getSupplier());
} else {
rules = getInclusions(map, desc);
}
return (rules.length == 0 && !ClasspathUtilCore.hasBundleStructure(model)) ? null : rules;
}
private Rule[] getInclusions(Map<BundleDescription, ArrayList<Rule>> map, BundleDescription desc) {
List<Rule> list = map.get(desc);
return list != null ? list.toArray(new Rule[list.size()]) : new Rule[0];
}
private void addHostPlugin(HostSpecification hostSpec, HashSet<BundleDescription> added, Map<BundleDescription, ArrayList<Rule>> map, ArrayList<IClasspathEntry> entries) throws CoreException {
BaseDescription desc = hostSpec.getSupplier();
if (desc instanceof BundleDescription) {
BundleDescription host = (BundleDescription) desc;
// add host plug-in
if (added.add(host) && addPlugin(host, false, map, entries)) {
BundleSpecification[] required = host.getRequiredBundles();
for (BundleSpecification bundleSpec : required) {
addDependency((BundleDescription) bundleSpec.getSupplier(), added, map, entries);
}
// add Import-Package
ImportPackageSpecification[] imports = host.getImportPackages();
for (ImportPackageSpecification importSpec : imports) {
BaseDescription supplier = importSpec.getSupplier();
if (supplier instanceof ExportPackageDescription) {
addDependencyViaImportPackage(((ExportPackageDescription) supplier).getExporter(), added, map, entries);
}
}
}
}
}
private boolean hasExtensibleAPI(BundleDescription desc) {
IPluginModelBase model = PluginRegistry.findModel(desc);
return model != null ? ClasspathUtilCore.hasExtensibleAPI(model) : false;
}
protected void addExtraClasspathEntries(HashSet<BundleDescription> added, ArrayList<IClasspathEntry> entries) {
IBuildEntry[] buildEntries = fBuild.getBuildEntries();
for (IBuildEntry entry : buildEntries) {
String name = entry.getName();
if (name.equals(IBuildPropertiesConstants.PROPERTY_JAR_EXTRA_CLASSPATH) || name.startsWith(IBuildPropertiesConstants.PROPERTY_EXTRAPATH_PREFIX)) {
addExtraClasspathEntries(added, entries, entry.getTokens());
}
}
}
protected void addExtraClasspathEntries(HashSet<BundleDescription> added, ArrayList<IClasspathEntry> entries, String[] tokens) {
for (String token : tokens) {
IPath path = Path.fromPortableString(token);
if (!path.isAbsolute()) {
File file = new File(fModel.getInstallLocation(), path.toString());
if (file.exists()) {
IFile resource = PDECore.getWorkspace().getRoot().getFileForLocation(new Path(file.getAbsolutePath()));
if (resource != null && resource.getProject().equals(fModel.getUnderlyingResource().getProject())) {
addExtraLibrary(resource.getFullPath(), null, entries);
continue;
}
}
if (path.segmentCount() >= 3 && "..".equals(path.segment(0))) { //$NON-NLS-1$
path = path.removeFirstSegments(1);
path = Path.fromPortableString("platform:/plugin/").append(path); //$NON-NLS-1$
} else {
continue;
}
}
if (!path.toPortableString().startsWith("platform:")) { //$NON-NLS-1$
addExtraLibrary(path, null, entries);
} else {
int count = path.getDevice() == null ? 4 : 3;
if (path.segmentCount() >= count) {
String pluginID = path.segment(count - 2);
IPluginModelBase model = PluginRegistry.findModel(pluginID);
if (model != null && model.isEnabled()) {
path = path.setDevice(null);
path = path.removeFirstSegments(count - 1);
IResource underlyingResource = model.getUnderlyingResource();
if (underlyingResource == null) {
IPath result = PDECore.getDefault().getModelManager().getExternalModelManager()
.getNestedLibrary(model, path.toString());
if (result != null) {
addExtraLibrary(result, model, entries);
}
} else {
IProject project = underlyingResource.getProject();
IFile file = project.getFile(path);
if (file.exists()) {
addExtraLibrary(file.getFullPath(), model, entries);
}
}
}
}
}
}
}
/**
* Adds JUnit5 dependencies that are required at runtime in eclipse, but not
* at compile-time or in tycho.
*/
private void addJunit5RuntimeDependencies(HashSet<BundleDescription> added, ArrayList<IClasspathEntry> entries)
throws CoreException {
if (!containsJunit5Dependency(added)) {
return;
}
if (JUNIT5_RUNTIME_PLUGINS.contains(fModel.getPluginBase().getId())) {
return; // never extend the classpath of a junit bundle
}
for (String pluginId : JUNIT5_RUNTIME_PLUGINS) {
IPluginModelBase model = PluginRegistry.findModel(pluginId);
if (model == null || !model.isEnabled()) {
continue;
}
BundleDescription desc = model.getBundleDescription();
if (added.contains(desc)) {
continue; // bundle has explicit dependency
}
// add dependency with exclude all rule
Map<BundleDescription, ArrayList<Rule>> rules = singletonMap(desc, new ArrayList<>());
addPlugin(desc, true, rules, entries);
}
}
private static boolean containsJunit5Dependency(Collection<BundleDescription> dependencies) {
return dependencies.stream().anyMatch(desc -> "org.junit.jupiter.api".equals(desc.getName())); //$NON-NLS-1$
}
private void addSecondaryDependencies(BundleDescription desc, HashSet<BundleDescription> added, ArrayList<IClasspathEntry> entries) {
try {
IBuildEntry entry = fBuild.getEntry(IBuildEntry.SECONDARY_DEPENDENCIES);
if (entry != null) {
String[] tokens = entry.getTokens();
for (String pluginId : tokens) {
// Get PluginModelBase first to resolve system.bundle entry if it exists
IPluginModelBase model = PluginRegistry.findModel(pluginId);
if (model != null) {
BundleDescription bundleDesc = model.getBundleDescription();
if (added.contains(bundleDesc)) {
continue;
}
Map<BundleDescription, ArrayList<Rule>> rules = new HashMap<>();
findExportedPackages(bundleDesc, desc, rules);
addDependency(bundleDesc, added, rules, entries, true);
}
}
}
} catch (CoreException e) {
return;
}
}
protected final void findExportedPackages(BundleDescription desc, BundleDescription projectDesc, Map<BundleDescription, ArrayList<Rule>> map) {
if (desc != null) {
ArrayDeque<BaseDescription> stack = new ArrayDeque<>();
stack.add(desc);
while (!stack.isEmpty()) {
BundleDescription bdesc = (BundleDescription) stack.pop();
ExportPackageDescription[] expkgs = bdesc.getExportPackages();
ArrayList<Rule> rules = new ArrayList<>();
for (ExportPackageDescription expkg : expkgs) {
Rule rule = new Rule();
rule.discouraged = restrictPackage(projectDesc, expkg);
rule.path = new Path(expkg.getName().replace('.', '/') + "/*"); //$NON-NLS-1$
rules.add(rule);
}
map.put(bdesc, rules);
// Look at re-exported Require-Bundles for any other exported packages
BundleSpecification[] requiredBundles = bdesc.getRequiredBundles();
for (BundleSpecification requiredBundle : requiredBundles) {
if (requiredBundle.isExported()) {
BaseDescription bd = requiredBundle.getSupplier();
if (bd != null && bd instanceof BundleDescription) {
stack.add(bd);
}
}
}
}
}
}
private boolean restrictPackage(BundleDescription desc, ExportPackageDescription pkg) {
String[] friends = (String[]) pkg.getDirective(ICoreConstants.FRIENDS_DIRECTIVE);
if (friends != null) {
String symbolicName = desc.getSymbolicName();
for (String friend : friends) {
if (symbolicName.equals(friend)) {
return false;
}
}
return true;
}
return (((Boolean) pkg.getDirective(ICoreConstants.INTERNAL_DIRECTIVE)).booleanValue());
}
private void addExtraLibrary(IPath path, IPluginModelBase model, ArrayList<IClasspathEntry> entries) {
if (path.segmentCount() > 1) {
IPath srcPath = null;
if (model != null) {
IPath shortPath = path.removeFirstSegments(path.matchingFirstSegments(new Path(model.getInstallLocation())));
srcPath = ClasspathUtilCore.getSourceAnnotation(model, shortPath.toString());
} else {
String filename = ClasspathUtilCore.getSourceZipName(path.lastSegment());
IPath candidate = path.removeLastSegments(1).append(filename);
if (PDECore.getWorkspace().getRoot().getFile(candidate).exists()) {
srcPath = candidate;
}
}
IClasspathEntry clsEntry = JavaCore.newLibraryEntry(path, srcPath, null);
if (!entries.contains(clsEntry)) {
entries.add(clsEntry);
}
}
}
/**
* Tries to compute a full set of bundle dependencies, including not exported
* bundle dependencies and bundles contributing packages possibly imported by
* any of bundles in the dependency graph.
*
* @return never null, but possibly empty project list which all projects in the
* workspace this container depends on, directly or indirectly.
*/
public List<IProject> getAllProjectDependencies() {
List<IProject> projects = new ArrayList<>();
IWorkspaceRoot root = PDECore.getWorkspace().getRoot();
try {
addImportedPackages = true;
IClasspathEntry[] entries = computePluginEntries();
for (IClasspathEntry cpe : entries) {
if (cpe.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
IProject project = root.getProject(cpe.getPath().lastSegment());
if (project.exists()) {
projects.add(project);
}
}
}
} finally {
addImportedPackages = false;
}
return projects;
}
}