Skip to content

Support updating whole classpath of the project #3098

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,7 @@ public Object executeCommand(String commandId, List<Object> arguments, IProgress
case "java.project.updateClassPaths": {
String projectUri = (String) arguments.get(0);
ProjectClasspathEntries entries = (JSONUtility.toModel(arguments.get(1), ProjectClasspathEntries.class));
Map<String, String> sourceAndOutput = new HashMap<>();
for (ProjectClasspathEntry entry : entries.getClasspathEntries()) {
if (entry.getKind() == IClasspathEntry.CPE_SOURCE) {
sourceAndOutput.put(entry.getPath(), entry.getOutput());
}
}
ProjectCommand.updateSourcePaths(projectUri, sourceAndOutput);
ProjectCommand.updateClasspaths(projectUri, entries.getClasspathEntries(), monitor);
return null;
}
case "java.project.isTestFile":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,11 +700,32 @@ public static void refreshDiagnostics(IProgressMonitor monitor) throws JavaModel
*/
public static IClasspathEntry[] resolveClassPathEntries(IJavaProject javaProject, Map<IPath, IPath> sourceAndOutput, List<IPath> excludingPaths, IPath outputPath) throws CoreException {
List<IClasspathEntry> newEntries = new LinkedList<>();
Map<IPath, IClasspathEntry> originalSources = new HashMap<>();
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE) {
newEntries.add(entry);
} else {
}
}

if (outputPath == null) {
outputPath = javaProject.getOutputLocation();
}

IClasspathEntry[] newSources = resolveSourceClasspathEntries(javaProject, sourceAndOutput, excludingPaths, outputPath);
newEntries.addAll(Arrays.asList(newSources));

IClasspathEntry[] rawClasspath = newEntries.toArray(IClasspathEntry[]::new);
IJavaModelStatus checkStatus = ClasspathEntry.validateClasspath(javaProject, rawClasspath, outputPath);
if (!checkStatus.isOK()) {
throw new CoreException(checkStatus);
}

return rawClasspath;
}

public static IClasspathEntry[] resolveSourceClasspathEntries(IJavaProject javaProject, Map<IPath, IPath> sourceAndOutput, List<IPath> excludingPaths, IPath outputPath) throws CoreException {
Map<IPath, IClasspathEntry> originalSources = new HashMap<>();
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
originalSources.put(entry.getPath(), entry);
}
}
Expand Down Expand Up @@ -757,15 +778,7 @@ public static IClasspathEntry[] resolveClassPathEntries(IJavaProject javaProject
}
}
}
newEntries.addAll(sourceEntries);

IClasspathEntry[] rawClasspath = newEntries.toArray(IClasspathEntry[]::new);
IJavaModelStatus checkStatus = ClasspathEntry.validateClasspath(javaProject, rawClasspath, outputPath);
if (!checkStatus.isOK()) {
throw new CoreException(checkStatus);
}

return rawClasspath;
return sourceEntries.toArray(IClasspathEntry[]::new);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
package org.eclipse.jdt.ls.core.internal.commands;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayDeque;
Expand All @@ -28,6 +30,8 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Stream;

import org.eclipse.core.resources.IContainer;
Expand All @@ -46,9 +50,11 @@
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModelStatus;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IParent;
import org.eclipse.jdt.core.IType;
Expand All @@ -71,6 +77,11 @@
import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.m2e.jdt.IClasspathManager;
import org.eclipse.m2e.jdt.MavenJdtPlugin;
import org.eclipse.m2e.jdt.internal.BuildPathManager;
import org.eclipse.m2e.jdt.internal.MavenClasspathContainer;
import org.eclipse.m2e.jdt.internal.MavenClasspathContainerSaveHelper;

public class ProjectCommand {

Expand Down Expand Up @@ -147,22 +158,47 @@ public static Map<String, Object> getProjectSettings(String uri, List<String> se
settings.putIfAbsent(key, referencedLibraries);
break;
case CLASSPATH_ENTRIES:
IClasspathEntry[] entries = javaProject.getRawClasspath();
List<IClasspathEntry> entriesToBeScan = new LinkedList<>();
Collections.addAll(entriesToBeScan, javaProject.getRawClasspath());
List<ProjectClasspathEntry> classpathEntries = new LinkedList<>();
for (IClasspathEntry entry : entries) {
for (int i = 0; i < entriesToBeScan.size(); i++) {
IClasspathEntry entry = entriesToBeScan.get(i);
IPath path = entry.getPath();
IPath output = entry.getOutputLocation();
if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
int entryKind = entry.getEntryKind();
if (entryKind == IClasspathEntry.CPE_SOURCE) {
IPath relativePath = path.makeRelativeTo(project.getFullPath());
if (relativePath.isEmpty()) {
continue; // A valid relative source path should not be empty.
}
path = project.getFolder(path.makeRelativeTo(project.getFullPath())).getLocation();
if (output != null) {
output = project.getFolder(output.makeRelativeTo(project.getFullPath())).getLocation();
}
} else if (entryKind == IClasspathEntry.CPE_CONTAINER) {
IPath containerPath = entry.getPath();
// skip JRE container, since it's already handled in VM_LOCATION.
if (!containerPath.toString().startsWith(JavaRuntime.JRE_CONTAINER)) {
IClasspathContainer container = JavaCore.getClasspathContainer(containerPath, javaProject);
if (container != null) {
entriesToBeScan.addAll(Arrays.asList(container.getClasspathEntries()));
}
}
continue;
} else if (entryKind == IClasspathEntry.CPE_LIBRARY) {
if (!path.toFile().exists()) {
// the case when lib path is relative
path = project.getFile(path.makeRelativeTo(project.getFullPath())).getLocation();
}
if (!path.toFile().exists()) {
continue;
}
}
Map<String, String> attributes = new HashMap<>();
for (IClasspathAttribute attribute : entry.getExtraAttributes()) {
attributes.put(attribute.getName(), attribute.getValue());
}
classpathEntries.add(new ProjectClasspathEntry(entry.getEntryKind(), path.toOSString(),
classpathEntries.add(new ProjectClasspathEntry(entryKind, path.toOSString(),
output == null ? null : output.toOSString(), attributes));
}
settings.putIfAbsent(key, classpathEntries);
Expand All @@ -175,6 +211,160 @@ public static Map<String, Object> getProjectSettings(String uri, List<String> se
return settings;
}

/**
* Update the project classpath by the given classpath entries.
*/
public static void updateClasspaths(String uri, List<ProjectClasspathEntry> entries, IProgressMonitor monitor) throws CoreException, URISyntaxException {
IJavaProject javaProject = getJavaProjectFromUri(uri);
IProject project = javaProject.getProject();
Map<IPath, IPath> sourceAndOutput = new HashMap<>();
List<IClasspathEntry> newEntries = new LinkedList<>();
List<IClasspathEntry> dependencyEntries = new LinkedList<>();
for (ProjectClasspathEntry entry : entries) {
if (entry.getKind() == IClasspathEntry.CPE_SOURCE) {
IPath path = project.getFolder(entry.getPath()).getFullPath();
IPath outputLocation = null;
String output = entry.getOutput();
if (output != null) {
if (".".equals(output)) {
outputLocation = project.getFullPath();
} else {
outputLocation = project.getFolder(output).getFullPath();
}
}
sourceAndOutput.put(path, outputLocation);
} else if (entry.getKind() == IClasspathEntry.CPE_CONTAINER) {
if (entry.getPath().startsWith(JavaRuntime.JRE_CONTAINER)) {
String jdkPath = entry.getPath().substring(JavaRuntime.JRE_CONTAINER.length());
newEntries.add(getNewJdkEntry(javaProject, jdkPath));
} else {
JavaLanguageServerPlugin.logInfo("The container entry " + entry.getPath() + " is not supported to be updated.");
}
} else {
dependencyEntries.add(convertClasspathEntry(entry));
}
}
IClasspathEntry[] sources = ProjectUtils.resolveSourceClasspathEntries(javaProject, sourceAndOutput, Collections.emptyList(), javaProject.getOutputLocation());
newEntries.addAll(Arrays.asList(sources));

// update maven classpath container since it's exposed from m2e.
// May consider extracting this operation to an API of IBuildSupport.
if (ProjectUtils.isMavenProject(project)) {
dependencyEntries = resolveMavenDependencyEntries(javaProject, dependencyEntries.toArray(IClasspathEntry[]::new), monitor);
}
newEntries.addAll(dependencyEntries);

IClasspathEntry[] rawClasspath = newEntries.toArray(IClasspathEntry[]::new);
IJavaModelStatus checkStatus = ClasspathEntry.validateClasspath(javaProject, rawClasspath, javaProject.getOutputLocation());
if (!checkStatus.isOK()) {
throw new CoreException(checkStatus);
}
javaProject.setRawClasspath(rawClasspath, monitor);
}

/**
* Convert ProjectClasspathEntry to IClasspathEntry.
*/
private static IClasspathEntry convertClasspathEntry(ProjectClasspathEntry entry) {
List<IClasspathAttribute> attributes = new LinkedList<>();
if (entry.getAttributes() != null) {
for (Entry<String, String> attributEntry : entry.getAttributes().entrySet()) {
attributes.add(JavaCore.newClasspathAttribute(attributEntry.getKey(), attributEntry.getValue()));
}
}
switch (entry.getKind()) {
case IClasspathEntry.CPE_CONTAINER:
return JavaCore.newContainerEntry(
IPath.fromOSString(entry.getPath()),
ClasspathEntry.NO_ACCESS_RULES,
attributes.toArray(IClasspathAttribute[]::new),
false
);
case IClasspathEntry.CPE_LIBRARY:
return JavaCore.newLibraryEntry(
IPath.fromOSString(entry.getPath()),
null,
null,
ClasspathEntry.NO_ACCESS_RULES,
attributes.toArray(IClasspathAttribute[]::new),
false
);
case IClasspathEntry.CPE_PROJECT:
return JavaCore.newProjectEntry(
IPath.fromOSString(entry.getPath()),
ClasspathEntry.NO_ACCESS_RULES,
false,
attributes.toArray(IClasspathAttribute[]::new),
false
);
default:
return null;
}
}

/**
* See {@link org.eclipse.m2e.jdt.internal.BuildPathManager#updateClasspath()}
*/
private static List<IClasspathEntry> resolveMavenDependencyEntries(IJavaProject javaProject, IClasspathEntry[] entries, IProgressMonitor monitor) {
IProject project = javaProject.getProject();
if (!ProjectUtils.isMavenProject(project)) {
JavaLanguageServerPlugin.logError("Project '" + javaProject.getElementName() + "' is not a Maven project.");
return Collections.emptyList();
}

List<IClasspathEntry> mavenEntries = new LinkedList<>();
List<IClasspathEntry> resolvedEntries = new LinkedList<>();
for (IClasspathEntry entry : entries) {
if (isMavenEntry(entry)) {
mavenEntries.add(entry);
} else {
resolvedEntries.add(entry);
}
}
try {
IClasspathEntry containerEntry = BuildPathManager.getMavenContainerEntry(javaProject);
IPath path = containerEntry != null ? containerEntry.getPath() :
IPath.fromOSString(BuildPathManager.CONTAINER_ID);
IClasspathContainer container = new MavenClasspathContainer(path, entries);
JavaCore.setClasspathContainer(container.getPath(), new IJavaProject[] {javaProject},
new IClasspathContainer[] {container}, monitor);
saveContainerState(project, container);
resolvedEntries.add(JavaCore.newContainerEntry(path));
} catch (JavaModelException e) {
JavaLanguageServerPlugin.log(e);
}
return resolvedEntries;
}

private static boolean isMavenEntry(IClasspathEntry entry) {
for (IClasspathAttribute attribute : entry.getExtraAttributes()) {
if (Objects.equals(IClasspathManager.POMDERIVED_ATTRIBUTE, attribute.getName()) && Boolean.parseBoolean(attribute.getValue())) {
return true;
}
}
return false;
}

/**
* See {@link org.eclipse.m2e.jdt.internal.BuildPathManager#saveContainerState()}
*/
private static void saveContainerState(IProject project, IClasspathContainer container) {
File containerStateFile = getContainerStateFile(project);
try (FileOutputStream is = new FileOutputStream(containerStateFile)) {
new MavenClasspathContainerSaveHelper().writeContainer(container, is);
} catch(IOException ex) {
JavaLanguageServerPlugin.logException("Can't save classpath container state for " + project.getName(), ex); //$NON-NLS-1$
}
}

/**
* See {@link org.eclipse.m2e.jdt.internal.BuildPathManager#getContainerStateFile()}
*/
private static File getContainerStateFile(IProject project) {
File stateLocationDir = MavenJdtPlugin.getDefault().getStateLocation().toFile();
return new File(stateLocationDir, project.getName() + ".container"); //$NON-NLS-1$
}

/**
* Updates the project source paths.
* @param uri Uri of the project.
Expand Down Expand Up @@ -432,31 +622,40 @@ public static SymbolInformation resolveWorkspaceSymbol(SymbolInformation request

public static JdkUpdateResult updateProjectJdk(String projectUri, String jdkPath, IProgressMonitor monitor) throws CoreException, URISyntaxException {
IJavaProject javaProject = ProjectCommand.getJavaProjectFromUri(projectUri);
IClasspathEntry[] originalClasspathEntries = javaProject.getRawClasspath();
IClasspathAttribute[] extraAttributes = null;
List<IClasspathEntry> newClasspathEntries = new ArrayList<>();
for (IClasspathEntry entry : originalClasspathEntries) {
if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER &&
entry.getPath().toString().startsWith("org.eclipse.jdt.launching.JRE_CONTAINER")) {
extraAttributes = entry.getExtraAttributes();
} else {
newClasspathEntries.add(entry);
try {
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER &&
entry.getPath().toString().startsWith(JavaRuntime.JRE_CONTAINER)) {
newClasspathEntries.add(getNewJdkEntry(javaProject, jdkPath));
} else {
newClasspathEntries.add(entry);
}
}
javaProject.setRawClasspath(newClasspathEntries.toArray(IClasspathEntry[]::new), monitor);
} catch (CoreException e) {
JavaLanguageServerPlugin.log(e);
return new JdkUpdateResult(false, e.getMessage());
}
return new JdkUpdateResult(true, jdkPath);
}

private static IClasspathEntry getNewJdkEntry(IJavaProject javaProject, String jdkPath) throws CoreException {
IVMInstall vmInstall = getVmInstallByPath(jdkPath);
List<IClasspathAttribute> extraAttributes = new ArrayList<>();
if (vmInstall == null) {
JavaLanguageServerPlugin.log(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "The select JDK path is not valid."));
return new JdkUpdateResult(false, "The selected JDK path is not valid.");
throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "The select JDK path is not valid."));
}
newClasspathEntries.add(JavaCore.newContainerEntry(
if (javaProject.getOwnModuleDescription() != null) {
extraAttributes.add(JavaCore.newClasspathAttribute(IClasspathAttribute.MODULE, "true"));
}

return JavaCore.newContainerEntry(
JavaRuntime.newJREContainerPath(vmInstall),
ClasspathEntry.NO_ACCESS_RULES,
extraAttributes,
extraAttributes.toArray(IClasspathAttribute[]::new),
false /*isExported*/
));
javaProject.setRawClasspath(newClasspathEntries.toArray(IClasspathEntry[]::new), monitor);
return new JdkUpdateResult(true, vmInstall.getInstallLocation().getAbsolutePath());
);
}

private static IVMInstall getVmInstallByPath(String path) {
Expand Down
Loading