diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java index 8c838fe720..653225dd2a 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTDelegateCommandHandler.java @@ -102,13 +102,7 @@ public Object executeCommand(String commandId, List arguments, IProgress case "java.project.updateClassPaths": { String projectUri = (String) arguments.get(0); ProjectClasspathEntries entries = (JSONUtility.toModel(arguments.get(1), ProjectClasspathEntries.class)); - Map 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": diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/ProjectUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/ProjectUtils.java index 3a2de147d8..97d88efcc7 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/ProjectUtils.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/ProjectUtils.java @@ -700,11 +700,32 @@ public static void refreshDiagnostics(IProgressMonitor monitor) throws JavaModel */ public static IClasspathEntry[] resolveClassPathEntries(IJavaProject javaProject, Map sourceAndOutput, List excludingPaths, IPath outputPath) throws CoreException { List newEntries = new LinkedList<>(); - Map 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 sourceAndOutput, List excludingPaths, IPath outputPath) throws CoreException { + Map originalSources = new HashMap<>(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { originalSources.put(entry.getPath(), entry); } } @@ -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); } } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommand.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommand.java index c82723316e..9448543699 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommand.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommand.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Map.Entry; import java.util.stream.Stream; import org.eclipse.core.resources.IContainer; @@ -46,9 +47,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; @@ -147,22 +150,43 @@ public static Map getProjectSettings(String uri, List se settings.putIfAbsent(key, referencedLibraries); break; case CLASSPATH_ENTRIES: - IClasspathEntry[] entries = javaProject.getRawClasspath(); + List entriesToBeScan = new LinkedList<>(); + Collections.addAll(entriesToBeScan, javaProject.getRawClasspath()); List 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) { + // skip JRE container, since it's already handled in VM_LOCATION. + if (!path.toString().startsWith(JavaRuntime.JRE_CONTAINER)) { + entriesToBeScan.addAll(expandContainerEntry(javaProject, entry)); + } + 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 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); @@ -175,6 +199,164 @@ public static Map getProjectSettings(String uri, List se return settings; } + /** + * Update the project classpath by the given classpath entries. + */ + public static void updateClasspaths(String uri, List entries, IProgressMonitor monitor) throws CoreException, URISyntaxException { + IJavaProject javaProject = getJavaProjectFromUri(uri); + IProject project = javaProject.getProject(); + Map sourceAndOutput = new HashMap<>(); + List newEntries = new LinkedList<>(); + List newDependencyEntries = 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 { + newDependencyEntries.add(convertClasspathEntry(entry)); + } + } + IClasspathEntry[] sources = ProjectUtils.resolveSourceClasspathEntries(javaProject, sourceAndOutput, Collections.emptyList(), javaProject.getOutputLocation()); + newEntries.addAll(Arrays.asList(sources)); + newEntries.addAll(resolveDependencyEntries(javaProject, newDependencyEntries)); + + 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); + } + + /** + * Check the new dependency entries are different from the current ones or not. + * If they are equal, return the current dependency entries, otherwise return the new ones. + */ + private static List resolveDependencyEntries(IJavaProject javaProject, List newEntries) throws JavaModelException { + List currentDependencyEntries = new LinkedList<>(); + for (IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE && + !entry.getPath().toString().startsWith(JavaRuntime.JRE_CONTAINER)) { + currentDependencyEntries.add(entry); + } + } + + Map currentEntryMapping = new HashMap<>(); + for (IClasspathEntry entry : currentDependencyEntries) { + if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { + List expandedContainerEntry = expandContainerEntry(javaProject, entry); + for (IClasspathEntry containerEntry : expandedContainerEntry) { + currentEntryMapping.put(containerEntry.getPath(), containerEntry); + } + } else { + currentEntryMapping.put(entry.getPath(), entry); + } + } + + // Use new dependency entries if the size is different + if (newEntries.size() != currentEntryMapping.size()) { + return newEntries; + } + + for (IClasspathEntry entry : newEntries) { + IClasspathEntry currentEntry = currentEntryMapping.get(entry.getPath()); + if (currentEntry == null) { + return newEntries; + } + } + + return currentDependencyEntries; + } + + /** + * Expand the container entry, the returned list is guaranteed to contain no container entry. + */ + private static List expandContainerEntry(IJavaProject javaProject, IClasspathEntry entry) throws JavaModelException { + if (entry.getEntryKind() != IClasspathEntry.CPE_CONTAINER) { + return Collections.singletonList(entry); + } + + List resolvedEntries = new LinkedList<>(); + List entriesToScan = new LinkedList<>(); + entriesToScan.add(entry); + for (int i = 0; i < entriesToScan.size(); i++) { + IClasspathEntry currentEntry = entriesToScan.get(i); + if (currentEntry.getEntryKind() != IClasspathEntry.CPE_CONTAINER) { + resolvedEntries.add(currentEntry); + continue; + } + IClasspathContainer container = JavaCore.getClasspathContainer(currentEntry.getPath(), javaProject); + if (container == null) { + continue; + } + IClasspathEntry[] containerEntries = container.getClasspathEntries(); + for (IClasspathEntry containerEntry : containerEntries) { + if (containerEntry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { + entriesToScan.add(containerEntry); + } else { + resolvedEntries.add(containerEntry); + } + } + } + return resolvedEntries; + } + + /** + * Convert ProjectClasspathEntry to IClasspathEntry. + */ + private static IClasspathEntry convertClasspathEntry(ProjectClasspathEntry entry) { + List attributes = new LinkedList<>(); + if (entry.getAttributes() != null) { + for (Entry attributeEntry : entry.getAttributes().entrySet()) { + attributes.add(JavaCore.newClasspathAttribute(attributeEntry.getKey(), attributeEntry.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; + } + } + /** * Updates the project source paths. * @param uri Uri of the project. @@ -432,31 +614,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 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 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.")); + } + if (javaProject.getOwnModuleDescription() != null) { + extraAttributes.add(JavaCore.newClasspathAttribute(IClasspathAttribute.MODULE, "true")); } - newClasspathEntries.add(JavaCore.newContainerEntry( + + 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) { diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommandTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommandTest.java index a986cdbcc5..187ecaf6f4 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommandTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/ProjectCommandTest.java @@ -173,6 +173,30 @@ public void testGetClasspathEntries() throws Exception { List entries = (List) options.get(ProjectCommand.CLASSPATH_ENTRIES); assertNotNull(options.get(ProjectCommand.CLASSPATH_ENTRIES)); assertTrue(entries.size() > 0); + assertTrue(entries.stream().anyMatch(entry -> { + return entry.getKind() == IClasspathEntry.CPE_SOURCE; + })); + assertTrue(entries.stream().anyMatch(entry -> { + return entry.getKind() == IClasspathEntry.CPE_LIBRARY; + })); + } + + @Test + public void testUpdateClasspathEntries() throws Exception { + importProjects("maven/salut2"); + IProject project = WorkspaceHelper.getProject("salut2"); + String uriString = project.getFile("src/main/java/foo/Bar.java").getLocationURI().toString(); + List settingKeys = Arrays.asList(ProjectCommand.CLASSPATH_ENTRIES); + Map options = ProjectCommand.getProjectSettings(uriString, settingKeys); + List entries = (List) options.get(ProjectCommand.CLASSPATH_ENTRIES); + + int size = entries.size(); + entries.remove(size - 1); + ProjectCommand.updateClasspaths(uriString, entries, new NullProgressMonitor()); + + options = ProjectCommand.getProjectSettings(uriString, settingKeys); + List newEntries = (List) options.get(ProjectCommand.CLASSPATH_ENTRIES); + assertEquals(size - 1, newEntries.size()); } @Test