Skip to content

Commit 66f98a6

Browse files
authored
Support updating whole classpath of the project (#3098)
Signed-off-by: Sheng Chen <[email protected]>
1 parent b01ce30 commit 66f98a6

File tree

4 files changed

+259
-37
lines changed

4 files changed

+259
-37
lines changed

org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java

+1-7
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,7 @@ public Object executeCommand(String commandId, List<Object> arguments, IProgress
102102
case "java.project.updateClassPaths": {
103103
String projectUri = (String) arguments.get(0);
104104
ProjectClasspathEntries entries = (JSONUtility.toModel(arguments.get(1), ProjectClasspathEntries.class));
105-
Map<String, String> sourceAndOutput = new HashMap<>();
106-
for (ProjectClasspathEntry entry : entries.getClasspathEntries()) {
107-
if (entry.getKind() == IClasspathEntry.CPE_SOURCE) {
108-
sourceAndOutput.put(entry.getPath(), entry.getOutput());
109-
}
110-
}
111-
ProjectCommand.updateSourcePaths(projectUri, sourceAndOutput);
105+
ProjectCommand.updateClasspaths(projectUri, entries.getClasspathEntries(), monitor);
112106
return null;
113107
}
114108
case "java.project.isTestFile":

org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/ProjectUtils.java

+24-11
Original file line numberDiff line numberDiff line change
@@ -700,11 +700,32 @@ public static void refreshDiagnostics(IProgressMonitor monitor) throws JavaModel
700700
*/
701701
public static IClasspathEntry[] resolveClassPathEntries(IJavaProject javaProject, Map<IPath, IPath> sourceAndOutput, List<IPath> excludingPaths, IPath outputPath) throws CoreException {
702702
List<IClasspathEntry> newEntries = new LinkedList<>();
703-
Map<IPath, IClasspathEntry> originalSources = new HashMap<>();
704703
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
705704
if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE) {
706705
newEntries.add(entry);
707-
} else {
706+
}
707+
}
708+
709+
if (outputPath == null) {
710+
outputPath = javaProject.getOutputLocation();
711+
}
712+
713+
IClasspathEntry[] newSources = resolveSourceClasspathEntries(javaProject, sourceAndOutput, excludingPaths, outputPath);
714+
newEntries.addAll(Arrays.asList(newSources));
715+
716+
IClasspathEntry[] rawClasspath = newEntries.toArray(IClasspathEntry[]::new);
717+
IJavaModelStatus checkStatus = ClasspathEntry.validateClasspath(javaProject, rawClasspath, outputPath);
718+
if (!checkStatus.isOK()) {
719+
throw new CoreException(checkStatus);
720+
}
721+
722+
return rawClasspath;
723+
}
724+
725+
public static IClasspathEntry[] resolveSourceClasspathEntries(IJavaProject javaProject, Map<IPath, IPath> sourceAndOutput, List<IPath> excludingPaths, IPath outputPath) throws CoreException {
726+
Map<IPath, IClasspathEntry> originalSources = new HashMap<>();
727+
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
728+
if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
708729
originalSources.put(entry.getPath(), entry);
709730
}
710731
}
@@ -757,15 +778,7 @@ public static IClasspathEntry[] resolveClassPathEntries(IJavaProject javaProject
757778
}
758779
}
759780
}
760-
newEntries.addAll(sourceEntries);
761-
762-
IClasspathEntry[] rawClasspath = newEntries.toArray(IClasspathEntry[]::new);
763-
IJavaModelStatus checkStatus = ClasspathEntry.validateClasspath(javaProject, rawClasspath, outputPath);
764-
if (!checkStatus.isOK()) {
765-
throw new CoreException(checkStatus);
766-
}
767-
768-
return rawClasspath;
781+
return sourceEntries.toArray(IClasspathEntry[]::new);
769782
}
770783

771784
}

org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommand.java

+210-19
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.List;
2929
import java.util.Map;
3030
import java.util.Optional;
31+
import java.util.Map.Entry;
3132
import java.util.stream.Stream;
3233

3334
import org.eclipse.core.resources.IContainer;
@@ -46,9 +47,11 @@
4647
import org.eclipse.core.runtime.jobs.Job;
4748
import org.eclipse.debug.core.ILaunchConfiguration;
4849
import org.eclipse.jdt.core.IClasspathAttribute;
50+
import org.eclipse.jdt.core.IClasspathContainer;
4951
import org.eclipse.jdt.core.IClasspathEntry;
5052
import org.eclipse.jdt.core.ICompilationUnit;
5153
import org.eclipse.jdt.core.IJavaElement;
54+
import org.eclipse.jdt.core.IJavaModelStatus;
5255
import org.eclipse.jdt.core.IJavaProject;
5356
import org.eclipse.jdt.core.IParent;
5457
import org.eclipse.jdt.core.IType;
@@ -147,22 +150,43 @@ public static Map<String, Object> getProjectSettings(String uri, List<String> se
147150
settings.putIfAbsent(key, referencedLibraries);
148151
break;
149152
case CLASSPATH_ENTRIES:
150-
IClasspathEntry[] entries = javaProject.getRawClasspath();
153+
List<IClasspathEntry> entriesToBeScan = new LinkedList<>();
154+
Collections.addAll(entriesToBeScan, javaProject.getRawClasspath());
151155
List<ProjectClasspathEntry> classpathEntries = new LinkedList<>();
152-
for (IClasspathEntry entry : entries) {
156+
for (int i = 0; i < entriesToBeScan.size(); i++) {
157+
IClasspathEntry entry = entriesToBeScan.get(i);
153158
IPath path = entry.getPath();
154159
IPath output = entry.getOutputLocation();
155-
if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
160+
int entryKind = entry.getEntryKind();
161+
if (entryKind == IClasspathEntry.CPE_SOURCE) {
162+
IPath relativePath = path.makeRelativeTo(project.getFullPath());
163+
if (relativePath.isEmpty()) {
164+
continue; // A valid relative source path should not be empty.
165+
}
156166
path = project.getFolder(path.makeRelativeTo(project.getFullPath())).getLocation();
157167
if (output != null) {
158168
output = project.getFolder(output.makeRelativeTo(project.getFullPath())).getLocation();
159169
}
170+
} else if (entryKind == IClasspathEntry.CPE_CONTAINER) {
171+
// skip JRE container, since it's already handled in VM_LOCATION.
172+
if (!path.toString().startsWith(JavaRuntime.JRE_CONTAINER)) {
173+
entriesToBeScan.addAll(expandContainerEntry(javaProject, entry));
174+
}
175+
continue;
176+
} else if (entryKind == IClasspathEntry.CPE_LIBRARY) {
177+
if (!path.toFile().exists()) {
178+
// the case when lib path is relative
179+
path = project.getFile(path.makeRelativeTo(project.getFullPath())).getLocation();
180+
}
181+
if (!path.toFile().exists()) {
182+
continue;
183+
}
160184
}
161185
Map<String, String> attributes = new HashMap<>();
162186
for (IClasspathAttribute attribute : entry.getExtraAttributes()) {
163187
attributes.put(attribute.getName(), attribute.getValue());
164188
}
165-
classpathEntries.add(new ProjectClasspathEntry(entry.getEntryKind(), path.toOSString(),
189+
classpathEntries.add(new ProjectClasspathEntry(entryKind, path.toOSString(),
166190
output == null ? null : output.toOSString(), attributes));
167191
}
168192
settings.putIfAbsent(key, classpathEntries);
@@ -175,6 +199,164 @@ public static Map<String, Object> getProjectSettings(String uri, List<String> se
175199
return settings;
176200
}
177201

202+
/**
203+
* Update the project classpath by the given classpath entries.
204+
*/
205+
public static void updateClasspaths(String uri, List<ProjectClasspathEntry> entries, IProgressMonitor monitor) throws CoreException, URISyntaxException {
206+
IJavaProject javaProject = getJavaProjectFromUri(uri);
207+
IProject project = javaProject.getProject();
208+
Map<IPath, IPath> sourceAndOutput = new HashMap<>();
209+
List<IClasspathEntry> newEntries = new LinkedList<>();
210+
List<IClasspathEntry> newDependencyEntries = new LinkedList<>();
211+
for (ProjectClasspathEntry entry : entries) {
212+
if (entry.getKind() == IClasspathEntry.CPE_SOURCE) {
213+
IPath path = project.getFolder(entry.getPath()).getFullPath();
214+
IPath outputLocation = null;
215+
String output = entry.getOutput();
216+
if (output != null) {
217+
if (".".equals(output)) {
218+
outputLocation = project.getFullPath();
219+
} else {
220+
outputLocation = project.getFolder(output).getFullPath();
221+
}
222+
}
223+
sourceAndOutput.put(path, outputLocation);
224+
} else if (entry.getKind() == IClasspathEntry.CPE_CONTAINER) {
225+
if (entry.getPath().startsWith(JavaRuntime.JRE_CONTAINER)) {
226+
String jdkPath = entry.getPath().substring(JavaRuntime.JRE_CONTAINER.length());
227+
newEntries.add(getNewJdkEntry(javaProject, jdkPath));
228+
} else {
229+
JavaLanguageServerPlugin.logInfo("The container entry " + entry.getPath() + " is not supported to be updated.");
230+
}
231+
} else {
232+
newDependencyEntries.add(convertClasspathEntry(entry));
233+
}
234+
}
235+
IClasspathEntry[] sources = ProjectUtils.resolveSourceClasspathEntries(javaProject, sourceAndOutput, Collections.emptyList(), javaProject.getOutputLocation());
236+
newEntries.addAll(Arrays.asList(sources));
237+
newEntries.addAll(resolveDependencyEntries(javaProject, newDependencyEntries));
238+
239+
IClasspathEntry[] rawClasspath = newEntries.toArray(IClasspathEntry[]::new);
240+
IJavaModelStatus checkStatus = ClasspathEntry.validateClasspath(javaProject, rawClasspath, javaProject.getOutputLocation());
241+
if (!checkStatus.isOK()) {
242+
throw new CoreException(checkStatus);
243+
}
244+
javaProject.setRawClasspath(rawClasspath, monitor);
245+
}
246+
247+
/**
248+
* Check the new dependency entries are different from the current ones or not.
249+
* If they are equal, return the current dependency entries, otherwise return the new ones.
250+
*/
251+
private static List<IClasspathEntry> resolveDependencyEntries(IJavaProject javaProject, List<IClasspathEntry> newEntries) throws JavaModelException {
252+
List<IClasspathEntry> currentDependencyEntries = new LinkedList<>();
253+
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
254+
if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE &&
255+
!entry.getPath().toString().startsWith(JavaRuntime.JRE_CONTAINER)) {
256+
currentDependencyEntries.add(entry);
257+
}
258+
}
259+
260+
Map<IPath, IClasspathEntry> currentEntryMapping = new HashMap<>();
261+
for (IClasspathEntry entry : currentDependencyEntries) {
262+
if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
263+
List<IClasspathEntry> expandedContainerEntry = expandContainerEntry(javaProject, entry);
264+
for (IClasspathEntry containerEntry : expandedContainerEntry) {
265+
currentEntryMapping.put(containerEntry.getPath(), containerEntry);
266+
}
267+
} else {
268+
currentEntryMapping.put(entry.getPath(), entry);
269+
}
270+
}
271+
272+
// Use new dependency entries if the size is different
273+
if (newEntries.size() != currentEntryMapping.size()) {
274+
return newEntries;
275+
}
276+
277+
for (IClasspathEntry entry : newEntries) {
278+
IClasspathEntry currentEntry = currentEntryMapping.get(entry.getPath());
279+
if (currentEntry == null) {
280+
return newEntries;
281+
}
282+
}
283+
284+
return currentDependencyEntries;
285+
}
286+
287+
/**
288+
* Expand the container entry, the returned list is guaranteed to contain no container entry.
289+
*/
290+
private static List<IClasspathEntry> expandContainerEntry(IJavaProject javaProject, IClasspathEntry entry) throws JavaModelException {
291+
if (entry.getEntryKind() != IClasspathEntry.CPE_CONTAINER) {
292+
return Collections.singletonList(entry);
293+
}
294+
295+
List<IClasspathEntry> resolvedEntries = new LinkedList<>();
296+
List<IClasspathEntry> entriesToScan = new LinkedList<>();
297+
entriesToScan.add(entry);
298+
for (int i = 0; i < entriesToScan.size(); i++) {
299+
IClasspathEntry currentEntry = entriesToScan.get(i);
300+
if (currentEntry.getEntryKind() != IClasspathEntry.CPE_CONTAINER) {
301+
resolvedEntries.add(currentEntry);
302+
continue;
303+
}
304+
IClasspathContainer container = JavaCore.getClasspathContainer(currentEntry.getPath(), javaProject);
305+
if (container == null) {
306+
continue;
307+
}
308+
IClasspathEntry[] containerEntries = container.getClasspathEntries();
309+
for (IClasspathEntry containerEntry : containerEntries) {
310+
if (containerEntry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
311+
entriesToScan.add(containerEntry);
312+
} else {
313+
resolvedEntries.add(containerEntry);
314+
}
315+
}
316+
}
317+
return resolvedEntries;
318+
}
319+
320+
/**
321+
* Convert ProjectClasspathEntry to IClasspathEntry.
322+
*/
323+
private static IClasspathEntry convertClasspathEntry(ProjectClasspathEntry entry) {
324+
List<IClasspathAttribute> attributes = new LinkedList<>();
325+
if (entry.getAttributes() != null) {
326+
for (Entry<String, String> attributeEntry : entry.getAttributes().entrySet()) {
327+
attributes.add(JavaCore.newClasspathAttribute(attributeEntry.getKey(), attributeEntry.getValue()));
328+
}
329+
}
330+
switch (entry.getKind()) {
331+
case IClasspathEntry.CPE_CONTAINER:
332+
return JavaCore.newContainerEntry(
333+
IPath.fromOSString(entry.getPath()),
334+
ClasspathEntry.NO_ACCESS_RULES,
335+
attributes.toArray(IClasspathAttribute[]::new),
336+
false
337+
);
338+
case IClasspathEntry.CPE_LIBRARY:
339+
return JavaCore.newLibraryEntry(
340+
IPath.fromOSString(entry.getPath()),
341+
null,
342+
null,
343+
ClasspathEntry.NO_ACCESS_RULES,
344+
attributes.toArray(IClasspathAttribute[]::new),
345+
false
346+
);
347+
case IClasspathEntry.CPE_PROJECT:
348+
return JavaCore.newProjectEntry(
349+
IPath.fromOSString(entry.getPath()),
350+
ClasspathEntry.NO_ACCESS_RULES,
351+
false,
352+
attributes.toArray(IClasspathAttribute[]::new),
353+
false
354+
);
355+
default:
356+
return null;
357+
}
358+
}
359+
178360
/**
179361
* Updates the project source paths.
180362
* @param uri Uri of the project.
@@ -432,31 +614,40 @@ public static SymbolInformation resolveWorkspaceSymbol(SymbolInformation request
432614

433615
public static JdkUpdateResult updateProjectJdk(String projectUri, String jdkPath, IProgressMonitor monitor) throws CoreException, URISyntaxException {
434616
IJavaProject javaProject = ProjectCommand.getJavaProjectFromUri(projectUri);
435-
IClasspathEntry[] originalClasspathEntries = javaProject.getRawClasspath();
436-
IClasspathAttribute[] extraAttributes = null;
437617
List<IClasspathEntry> newClasspathEntries = new ArrayList<>();
438-
for (IClasspathEntry entry : originalClasspathEntries) {
439-
if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER &&
440-
entry.getPath().toString().startsWith("org.eclipse.jdt.launching.JRE_CONTAINER")) {
441-
extraAttributes = entry.getExtraAttributes();
442-
} else {
443-
newClasspathEntries.add(entry);
618+
try {
619+
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
620+
if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER &&
621+
entry.getPath().toString().startsWith(JavaRuntime.JRE_CONTAINER)) {
622+
newClasspathEntries.add(getNewJdkEntry(javaProject, jdkPath));
623+
} else {
624+
newClasspathEntries.add(entry);
625+
}
444626
}
627+
javaProject.setRawClasspath(newClasspathEntries.toArray(IClasspathEntry[]::new), monitor);
628+
} catch (CoreException e) {
629+
JavaLanguageServerPlugin.log(e);
630+
return new JdkUpdateResult(false, e.getMessage());
445631
}
632+
return new JdkUpdateResult(true, jdkPath);
633+
}
446634

635+
private static IClasspathEntry getNewJdkEntry(IJavaProject javaProject, String jdkPath) throws CoreException {
447636
IVMInstall vmInstall = getVmInstallByPath(jdkPath);
637+
List<IClasspathAttribute> extraAttributes = new ArrayList<>();
448638
if (vmInstall == null) {
449-
JavaLanguageServerPlugin.log(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "The select JDK path is not valid."));
450-
return new JdkUpdateResult(false, "The selected JDK path is not valid.");
639+
throw new CoreException(new Status(IStatus.ERROR, IConstants.PLUGIN_ID, "The select JDK path is not valid."));
640+
}
641+
if (javaProject.getOwnModuleDescription() != null) {
642+
extraAttributes.add(JavaCore.newClasspathAttribute(IClasspathAttribute.MODULE, "true"));
451643
}
452-
newClasspathEntries.add(JavaCore.newContainerEntry(
644+
645+
return JavaCore.newContainerEntry(
453646
JavaRuntime.newJREContainerPath(vmInstall),
454647
ClasspathEntry.NO_ACCESS_RULES,
455-
extraAttributes,
648+
extraAttributes.toArray(IClasspathAttribute[]::new),
456649
false /*isExported*/
457-
));
458-
javaProject.setRawClasspath(newClasspathEntries.toArray(IClasspathEntry[]::new), monitor);
459-
return new JdkUpdateResult(true, vmInstall.getInstallLocation().getAbsolutePath());
650+
);
460651
}
461652

462653
private static IVMInstall getVmInstallByPath(String path) {

org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommandTest.java

+24
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,30 @@ public void testGetClasspathEntries() throws Exception {
173173
List<ProjectClasspathEntry> entries = (List) options.get(ProjectCommand.CLASSPATH_ENTRIES);
174174
assertNotNull(options.get(ProjectCommand.CLASSPATH_ENTRIES));
175175
assertTrue(entries.size() > 0);
176+
assertTrue(entries.stream().anyMatch(entry -> {
177+
return entry.getKind() == IClasspathEntry.CPE_SOURCE;
178+
}));
179+
assertTrue(entries.stream().anyMatch(entry -> {
180+
return entry.getKind() == IClasspathEntry.CPE_LIBRARY;
181+
}));
182+
}
183+
184+
@Test
185+
public void testUpdateClasspathEntries() throws Exception {
186+
importProjects("maven/salut2");
187+
IProject project = WorkspaceHelper.getProject("salut2");
188+
String uriString = project.getFile("src/main/java/foo/Bar.java").getLocationURI().toString();
189+
List<String> settingKeys = Arrays.asList(ProjectCommand.CLASSPATH_ENTRIES);
190+
Map<String, Object> options = ProjectCommand.getProjectSettings(uriString, settingKeys);
191+
List<ProjectClasspathEntry> entries = (List) options.get(ProjectCommand.CLASSPATH_ENTRIES);
192+
193+
int size = entries.size();
194+
entries.remove(size - 1);
195+
ProjectCommand.updateClasspaths(uriString, entries, new NullProgressMonitor());
196+
197+
options = ProjectCommand.getProjectSettings(uriString, settingKeys);
198+
List<ProjectClasspathEntry> newEntries = (List) options.get(ProjectCommand.CLASSPATH_ENTRIES);
199+
assertEquals(size - 1, newEntries.size());
176200
}
177201

178202
@Test

0 commit comments

Comments
 (0)