Skip to content

Commit

Permalink
ClasspathComputer: overhaul classpath computation
Browse files Browse the repository at this point in the history
Issues fixed:
* erroneous exception "Build path contains duplicate entry"
* updating source attachment changed unrelated attributes
* failed to preserve some existing attributes
* failed to preserve entries of kind="var"
* failed to preserve existing order
* UpdateClasspathJob returned CANCEL on success

Tests now succeed, and run as part of AllPDEMinimalTests suite
  • Loading branch information
haubi authored and akurtakov committed Sep 12, 2023
1 parent 2feefeb commit 3f20167
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.core.resources.IFile;
Expand Down Expand Up @@ -58,86 +59,130 @@ public class ClasspathComputer {
private static final int SEVERITY_WARNING = 2;
private static final int SEVERITY_IGNORE = 1;

private final IProject project;
private final IPluginModelBase model;
private final Map<String, IPath> sourceLibraryMap;
private final boolean overrideCompliance;

private final IJavaProject javaProject;
private final IBuild build;
private final boolean isTestPlugin;
private final IClasspathAttribute[] defaultAttrs;
private final IClasspathEntry[] original;
private final Map<IPath, IClasspathEntry> originalByPath;
private final List<IClasspathEntry> reloaded = new ArrayList<>();

private ClasspathComputer(IProject project, IPluginModelBase model, Map<String, IPath> sourceLibraryMap,
boolean clear, boolean overrideCompliance) throws CoreException {
this.project = project;
this.model = model;
this.sourceLibraryMap = sourceLibraryMap != null ? sourceLibraryMap : new HashMap<>();
this.overrideCompliance = overrideCompliance;

this.javaProject = JavaCore.create(project);
this.build = getBuild(project);
this.isTestPlugin = hasTestPluginName(project);
this.defaultAttrs = getClasspathAttributes(project, model);
this.original = clear ? new IClasspathEntry[0] : javaProject.getRawClasspath();
// ignore duplicates eventually seen in the original classpath
this.originalByPath = mapFirstSeenByPath(Arrays.stream(this.original));
}

public static void setClasspath(IProject project, IPluginModelBase model) throws CoreException {
IClasspathEntry[] entries = getClasspath(project, model, null, false, true);
JavaCore.create(project).setRawClasspath(entries, null);
}

public static IClasspathEntry[] getClasspath(IProject project, IPluginModelBase model, Map<String, IPath> sourceLibraryMap, boolean clear, boolean overrideCompliance) throws CoreException {
IJavaProject javaProject = JavaCore.create(project);
ArrayList<IClasspathEntry> result = new ArrayList<>();
IBuild build = getBuild(project);
public static IClasspathEntry[] getClasspath(IProject project, IPluginModelBase model,
Map<String, IPath> sourceLibraryMap, boolean clear, boolean overrideCompliance) throws CoreException {
ClasspathComputer cpc = new ClasspathComputer(project, model, sourceLibraryMap, clear, overrideCompliance);
return cpc.compute();
}

private IClasspathEntry[] compute() throws CoreException {

// add JRE and set compliance options
String ee = getExecutionEnvironment(model.getBundleDescription());
result.add(createEntryUsingPreviousEntry(javaProject, ee, PDECore.JRE_CONTAINER_PATH));
setComplianceOptions(JavaCore.create(project), ee, overrideCompliance);
addContainerEntry(getEEPath(ee));
setComplianceOptions(javaProject, ee, overrideCompliance);

// add pde container
result.add(createEntryUsingPreviousEntry(javaProject, ee, PDECore.REQUIRED_PLUGINS_CONTAINER_PATH));
addContainerEntry(PDECore.REQUIRED_PLUGINS_CONTAINER_PATH);

// add own libraries/source
addSourceAndLibraries(project, model, build, clear, sourceLibraryMap, result);
addSourceAndLibraries();

IClasspathEntry[] entries = collectInOriginalOrder();

IClasspathEntry[] entries = result.toArray(new IClasspathEntry[result.size()]);
IJavaModelStatus validation = JavaConventions.validateClasspath(javaProject, entries, javaProject.getOutputLocation());
IJavaModelStatus validation = JavaConventions.validateClasspath(javaProject, entries,
javaProject.getOutputLocation());
if (!validation.isOK()) {
PDECore.logErrorMessage(validation.getMessage());
throw new CoreException(validation);
}
return entries;
}

private IClasspathEntry[] collectInOriginalOrder() {
// preserve original entries which eventually weren't reloaded
Stream<IClasspathEntry> reloadedPlusOriginal = Stream.concat(reloaded.stream(), Stream.of(original));
Map<IPath, IClasspathEntry> resultByPath = mapFirstSeenByPath(reloadedPlusOriginal);
List<IClasspathEntry> result = new ArrayList<>(resultByPath.size());
// using the original order, collect the resulting entries, and remove
// from the map to prevent from any duplicates (even original ones)
Arrays.stream(original).map(e -> resultByPath.remove(pathWithoutEE(e.getPath()))).filter(e -> e != null)
.forEachOrdered(result::add);
// using the order of reloading, append new entries (in the map still)
reloaded.stream().filter(e -> resultByPath.remove(pathWithoutEE(e.getPath())) != null)
.forEachOrdered(result::add);
return result.toArray(new IClasspathEntry[result.size()]);
}

private static void addSourceAndLibraries(IProject project, IPluginModelBase model, IBuild build, boolean clear,
Map<String, IPath> sourceLibraryMap, ArrayList<IClasspathEntry> result) throws CoreException {
boolean isTestPlugin = hasTestPluginName(project);
HashSet<IPath> paths = new HashSet<>();

// keep existing source folders
if (!clear) {
IClasspathEntry[] entries = JavaCore.create(project).getRawClasspath();
for (IClasspathEntry entry : entries) {
if (entry.getPath() != null ) {
if (PDECore.JRE_CONTAINER_PATH.isPrefixOf(entry.getPath())
|| PDECore.REQUIRED_PLUGINS_CONTAINER_PATH.equals(entry.getPath())) {
continue;
}
}
if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE
|| entry.getEntryKind() == IClasspathEntry.CPE_PROJECT
|| entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY
|| entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
if (paths.add(entry.getPath())) {
result.add(updateTestAttribute(isTestPlugin, entry));
}
}
}
private static Map<IPath, IClasspathEntry> mapFirstSeenByPath(Stream<IClasspathEntry> entryStream) {
return entryStream.collect(Collectors.toMap(e -> pathWithoutEE(e.getPath()), e -> e, (first, dupe) -> first));
}

private static IPath pathWithoutEE(IPath path) {
if (PDECore.JRE_CONTAINER_PATH.isPrefixOf(path)) {
// The path member of IClasspathEntry for JRE_CONTAINER_PATH may
// also declare an Execution Environment, which is an attribute.
return PDECore.JRE_CONTAINER_PATH;
}
return path;
}

private void addContainerEntry(IPath path) {
IClasspathEntry orig = originalByPath.get(pathWithoutEE(path));
if (orig != null) {
reloaded.add(JavaCore.newContainerEntry(path, orig.getAccessRules(), orig.getExtraAttributes(),
orig.isExported()));
} else {
reloaded.add(JavaCore.newContainerEntry(path, null, defaultAttrs, false));
}
}

IClasspathAttribute[] attrs = getClasspathAttributes(project, model);
private void addSourceAndLibraries() throws CoreException {
IPluginLibrary[] libraries = model.getPluginBase().getLibraries();
for (IPluginLibrary library : libraries) {
IBuildEntry buildEntry = build == null ? null : build.getEntry("source." + library.getName()); //$NON-NLS-1$
if (buildEntry != null) {
addSourceFolder(buildEntry, project, paths, result, isTestPlugin);
addSourceFolders(buildEntry);
continue;
}
if (library.getName().equals(".")) { //$NON-NLS-1$
addJARdPlugin("."); //$NON-NLS-1$
} else {
IPath sourceAttachment = sourceLibraryMap != null ? sourceLibraryMap.get(library.getName()) : null;
if (library.getName().equals(".")) { //$NON-NLS-1$
addJARdPlugin(project, ClasspathUtilCore.getFilename(model), sourceAttachment, attrs, result);
} else {
addLibraryEntry(project, library, sourceAttachment, attrs, result);
}
addLibraryEntry(library);
}
}
if (libraries.length == 0) {
if (build != null) {
IBuildEntry buildEntry = build.getEntry("source.."); //$NON-NLS-1$
if (buildEntry != null) {
addSourceFolder(buildEntry, project, paths, result, isTestPlugin);
addSourceFolders(buildEntry);
}
} else if (ClasspathUtilCore.hasBundleStructure(model)) {
IPath sourceAttachment = sourceLibraryMap != null ? (IPath) sourceLibraryMap.get(".") : null; //$NON-NLS-1$
addJARdPlugin(project, ClasspathUtilCore.getFilename(model), sourceAttachment, attrs, result);
addJARdPlugin("."); //$NON-NLS-1$
}
}
}
Expand Down Expand Up @@ -202,28 +247,30 @@ private static IClasspathAttribute[] getClasspathAttributes(IProject project, IP
return attributes;
}

private static void addSourceFolder(IBuildEntry buildEntry, IProject project, HashSet<IPath> paths,
ArrayList<IClasspathEntry> result, boolean isTestPlugin) throws CoreException {
private void addSourceFolders(IBuildEntry buildEntry) throws CoreException {
String[] folders = buildEntry.getTokens();
for (String folder : folders) {
IPath path = project.getFullPath().append(folder);
if (paths.add(path)) {
if (project.findMember(folder) == null) {
CoreUtility.createFolder(project.getFolder(folder));
} else {
IPackageFragmentRoot root = JavaCore.create(project).getPackageFragmentRoot(path.toString());
if (root.exists() && root.getKind() == IPackageFragmentRoot.K_BINARY) {
result.add(root.getRawClasspathEntry());
continue;
}
}
if (isTestPlugin) {
result.add(JavaCore.newSourceEntry(path, null, null, null, new IClasspathAttribute[] {
JavaCore.newClasspathAttribute(IClasspathAttribute.TEST, "true") })); //$NON-NLS-1$
} else {
result.add(JavaCore.newSourceEntry(path));
IClasspathEntry orig = originalByPath.get(pathWithoutEE(path));
if (orig != null) {
reloaded.add(orig);
continue;
}
if (project.findMember(folder) == null) {
CoreUtility.createFolder(project.getFolder(folder));
} else {
IPackageFragmentRoot root = javaProject.getPackageFragmentRoot(path.toString());
if (root.exists() && root.getKind() == IPackageFragmentRoot.K_BINARY) {
reloaded.add(root.getRawClasspathEntry());
continue;
}
}
if (isTestPlugin) {
reloaded.add(JavaCore.newSourceEntry(path, null, null, null, new IClasspathAttribute[] {
JavaCore.newClasspathAttribute(IClasspathAttribute.TEST, "true") })); //$NON-NLS-1$
} else {
reloaded.add(JavaCore.newSourceEntry(path));
}
}
}

Expand All @@ -237,45 +284,63 @@ protected static IBuild getBuild(IProject project) throws CoreException {
return (buildModel != null) ? buildModel.getBuild() : null;
}

private static void addLibraryEntry(IProject project, IPluginLibrary library, IPath sourceAttachment, IClasspathAttribute[] attrs, ArrayList<IClasspathEntry> result) throws JavaModelException {
private void addLibraryEntry(IPluginLibrary library) throws JavaModelException {
String name = ClasspathUtilCore.expandLibraryName(library.getName());
IResource jarFile = project.findMember(name);
if (jarFile == null) {
return;
}

IPath sourceAttachment = sourceLibraryMap.get(library.getName());
boolean isExported = library.isExported();

IPackageFragmentRoot root = JavaCore.create(project).getPackageFragmentRoot(jarFile);
if (root.exists() && root.getKind() == IPackageFragmentRoot.K_BINARY) {
IClasspathEntry oldEntry = root.getRawClasspathEntry();
// If we have the same binary root but new or different source, we should recreate the entry
if ((sourceAttachment == null && oldEntry.getSourceAttachmentPath() != null) || (sourceAttachment != null && sourceAttachment.equals(oldEntry.getSourceAttachmentPath()))) {
if (!result.contains(oldEntry)) {
result.add(oldEntry);
return;
}
// If we have the same binary root but new or different source,
// we should recreate the entry, preserving unrelated attributes.
if (sourceAttachment == null) {
// no override: stick to existing one, if any
sourceAttachment = oldEntry.getSourceAttachmentPath();
}
if (sourceAttachment != null && sourceAttachment.equals(oldEntry.getSourceAttachmentPath())) {
reloaded.add(oldEntry);
return;
}
// Force recreation when the source attachment:
// - is not defined: the default could be available now, or
// - is overridden with a different value.
isExported = oldEntry.isExported();
}

IClasspathEntry entry = createClasspathEntry(project, jarFile, name, sourceAttachment, attrs, library.isExported());
if (!result.contains(entry)) {
result.add(entry);
}
reloadClasspathEntry(jarFile, name, sourceAttachment, isExported);
}

private static void addJARdPlugin(IProject project, String filename, IPath sourceAttachment, IClasspathAttribute[] attrs, ArrayList<IClasspathEntry> result) {
private void addJARdPlugin(String libraryName) {
String filename = ClasspathUtilCore.getFilename(model);
String name = ClasspathUtilCore.expandLibraryName(filename);
IResource jarFile = project.findMember(name);
if (jarFile != null) {
IClasspathEntry entry = createClasspathEntry(project, jarFile, filename, sourceAttachment, attrs, true);
if (!result.contains(entry)) {
result.add(entry);
}
IPath sourceAttachment = sourceLibraryMap.get(libraryName);
reloadClasspathEntry(jarFile, filename, sourceAttachment, true);
}
}

private static IClasspathEntry createClasspathEntry(IProject project, IResource library, String fileName, IPath sourceAttachment, IClasspathAttribute[] attrs, boolean isExported) {
IResource resource = sourceAttachment != null ? project.findMember(sourceAttachment) : project.findMember(ClasspathUtilCore.getSourceZipName(fileName));
return JavaCore.newLibraryEntry(library.getFullPath(), resource == null ? null : resource.getFullPath(), null, new IAccessRule[0], attrs, isExported);
private void reloadClasspathEntry(IResource library, String fileName, IPath sourceAttachment, boolean isExported) {
IClasspathEntry orig = originalByPath.get(pathWithoutEE(library.getFullPath()));
if (orig != null && sourceAttachment == null) {
sourceAttachment = orig.getSourceAttachmentPath();
}
IResource source = sourceAttachment != null ? project.findMember(sourceAttachment)
: project.findMember(ClasspathUtilCore.getSourceZipName(fileName));
sourceAttachment = source == null ? null : source.getFullPath();
if (orig != null) {
reloaded.add(JavaCore.newLibraryEntry(library.getFullPath(), sourceAttachment, null, orig.getAccessRules(),
orig.getExtraAttributes(), orig.isExported()));
} else {
reloaded.add(JavaCore.newLibraryEntry(library.getFullPath(), sourceAttachment, null, new IAccessRule[0],
defaultAttrs, isExported));
}
}

private static String getExecutionEnvironment(BundleDescription bundleDescription) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.eclipse.pde.core.tests.internal.util.PDESchemaHelperTest;
import org.eclipse.pde.ui.tests.build.properties.AllValidatorTests;
import org.eclipse.pde.ui.tests.classpathresolver.ClasspathResolverTest;
import org.eclipse.pde.ui.tests.classpathupdater.ClasspathUpdaterTest;
import org.eclipse.pde.ui.tests.launcher.AllLauncherTests;
import org.eclipse.pde.ui.tests.model.bundle.AllBundleModelTests;
import org.eclipse.pde.ui.tests.model.xml.AllXMLModelTests;
Expand Down Expand Up @@ -49,7 +50,8 @@
AllPDERuntimeTests.class,
// ExportBundleTests.class,
AllLauncherTests.class, AllLogViewTests.class, ProjectCreationTests.class, BundleRootTests.class,
PluginRegistryTestsMinimal.class, ClasspathResolverTest.class, PDESchemaHelperTest.class,
PluginRegistryTestsMinimal.class, ClasspathResolverTest.class, ClasspathUpdaterTest.class,
PDESchemaHelperTest.class,
DynamicPluginProjectReferencesTest.class,
// ClasspathContributorTest.class
ClasspathResolutionTest.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class UpdateClasspathWorkspaceRunnable implements IWorkspaceRunnable {

@Override
public void run(IProgressMonitor monitor) throws CoreException {
fCanceled = doUpdateClasspath(monitor, fModels);
fCanceled = !doUpdateClasspath(monitor, fModels);
}

public boolean isCanceled() {
Expand Down

0 comments on commit 3f20167

Please sign in to comment.