From 7328c67c02ae65af4297d909938e0f9be1245d41 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Tue, 28 Aug 2018 11:16:09 +0800 Subject: [PATCH] Validate mainClass and projectName configs (#205) * Validate mainClass and projectName configs Signed-off-by: Jinbo Wang * Update ValidationResult scheme * Revert 'errorMessage' to a neutral name 'message' for ValidationResult * Only report not unique validation error when projectName is not specified * Ignore mainClass validation when the user specified the classpaths manually * Use Object.equals to compare two strings --- com.microsoft.java.debug.plugin/plugin.xml | 3 + .../JavaDebugDelegateCommandHandler.java | 4 + .../internal/ResolveClasspathsHandler.java | 2 +- .../internal/ResolveMainClassHandler.java | 165 +++++++++++++++++- 4 files changed, 166 insertions(+), 8 deletions(-) diff --git a/com.microsoft.java.debug.plugin/plugin.xml b/com.microsoft.java.debug.plugin/plugin.xml index dea0d451..bb4407ea 100644 --- a/com.microsoft.java.debug.plugin/plugin.xml +++ b/com.microsoft.java.debug.plugin/plugin.xml @@ -9,6 +9,9 @@ + + diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java index 54daddc9..e42d9640 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java @@ -32,6 +32,8 @@ public class JavaDebugDelegateCommandHandler implements IDelegateCommandHandler public static String UPDATE_DEBUG_SETTINGS = "vscode.java.updateDebugSettings"; + public static String VALIDATE_LAUNCHCONFIG = "vscode.java.validateLaunchConfig"; + @Override public Object executeCommand(String commandId, List arguments, IProgressMonitor progress) throws Exception { if (DEBUG_STARTSESSION.equals(commandId)) { @@ -50,6 +52,8 @@ public class JavaDebugDelegateCommandHandler implements IDelegateCommandHandler return UsageDataStore.getInstance().fetchAll(); } else if (UPDATE_DEBUG_SETTINGS.equals(commandId)) { return DebugSettingUtils.configDebugSettings(arguments); + } else if (VALIDATE_LAUNCHCONFIG.equals(commandId)) { + return new ResolveMainClassHandler().validateLaunchConfig(arguments); } throw new UnsupportedOperationException(String.format("Java debug plugin doesn't support the command '%s'.", commandId)); diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveClasspathsHandler.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveClasspathsHandler.java index 10baa336..04fca2ec 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveClasspathsHandler.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveClasspathsHandler.java @@ -79,7 +79,7 @@ public class ResolveClasspathsHandler { * @throws CoreException * CoreException */ - private static List getJavaProjectFromType(String fullyQualifiedTypeName) throws CoreException { + public static List getJavaProjectFromType(String fullyQualifiedTypeName) throws CoreException { String[] splitItems = fullyQualifiedTypeName.split("/"); // If the main class name contains the module name, should trim the module info. if (splitItems.length == 2) { diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainClassHandler.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainClassHandler.java index 7582fa32..9ef18aef 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainClassHandler.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainClassHandler.java @@ -12,12 +12,16 @@ package com.microsoft.java.debug.plugin.internal; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; @@ -40,19 +44,35 @@ import com.microsoft.java.debug.core.Configuration; public class ResolveMainClassHandler { private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); + private static final String CLASSNAME_REGX = "([$\\w]+\\.)*[$\\w]+"; /** * resolve main class and project name. * @return an array of main class and project name - * @throws CoreException when there are errors when resolving main class. */ - public Object resolveMainClass(List arguments) throws CoreException { + public Object resolveMainClass(List arguments) { return resolveMainClassCore(arguments); } - private List resolveMainClassCore(List arguments) throws CoreException { + /** + * Validate whether the mainClass and projectName is correctly configured or not. If not, report the validation error and provide the quick fix proposal. + * + * @param arguments the mainClass and projectName configs. + * @return the validation response. + * @throws Exception when there are any errors during validating the mainClass and projectName. + */ + public Object validateLaunchConfig(List arguments) throws Exception { + try { + return validateLaunchConfigCore(arguments); + } catch (CoreException ex) { + logger.log(Level.SEVERE, "Failed to validate launch config: " + ex.getMessage(), ex); + throw new Exception("Failed to validate launch config: " + ex.getMessage(), ex); + } + } + + private List resolveMainClassCore(List arguments) { IPath rootPath = null; - if (arguments != null && arguments.size() > 0) { + if (arguments != null && arguments.size() > 0 && arguments.get(0) != null) { rootPath = ResourceUtils.filePathFromURI((String) arguments.get(0)); } final ArrayList targetProjectPath = new ArrayList<>(); @@ -62,7 +82,7 @@ public class ResolveMainClassHandler { IJavaSearchScope searchScope = SearchEngine.createWorkspaceScope(); SearchPattern pattern = SearchPattern.createPattern("main(String[]) void", IJavaSearchConstants.METHOD, IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH); - ArrayList res = new ArrayList<>(); + final List res = new ArrayList<>(); SearchRequestor requestor = new SearchRequestor() { @Override public void acceptSearchMatch(SearchMatch match) { @@ -114,10 +134,93 @@ public class ResolveMainClassHandler { } catch (Exception e) { logger.log(Level.SEVERE, String.format("Searching the main class failure: %s", e.toString()), e); } - return res.stream().distinct().collect(Collectors.toList()); + + List resolutions = res.stream().distinct().collect(Collectors.toList()); + Collections.sort(resolutions); + return resolutions; } - private class ResolutionItem { + private ValidationResponse validateLaunchConfigCore(List arguments) throws CoreException { + ValidationResponse response = new ValidationResponse(); + + String mainClass = null; + String projectName = null; + boolean containsExternalClasspaths = false; + if (arguments != null) { + if (arguments.size() > 1) { + mainClass = (String) arguments.get(1); + } + if (arguments.size() > 2) { + projectName = (String) arguments.get(2); + } + if (arguments.size() > 3) { + containsExternalClasspaths = (boolean) arguments.get(3); + } + } + + response.mainClass = validateMainClass(mainClass, projectName, containsExternalClasspaths); + response.projectName = validateProjectName(mainClass, projectName); + + if (!response.mainClass.isValid || !response.projectName.isValid) { + response.proposals = computeProposals(arguments, mainClass, projectName); + } + + return response; + } + + private ValidationResult validateMainClass(final String mainClass, final String projectName, boolean containsExternalClasspaths) throws CoreException { + if (StringUtils.isEmpty(mainClass)) { + return new ValidationResult(true); + } else if (!mainClass.matches(CLASSNAME_REGX)) { + return new ValidationResult(false, String.format("ConfigError: '%s' is not a valid class name.", mainClass)); + } + + if (!containsExternalClasspaths && StringUtils.isEmpty(projectName)) { + List javaProjects = searchClassInProjectClasspaths(mainClass); + if (javaProjects.size() == 0) { + return new ValidationResult(false, String.format("ConfigError: Main class '%s' doesn't exist in the workspace.", mainClass)); + } + if (javaProjects.size() > 1) { + return new ValidationResult(false, String.format("ConfigError: Main class '%s' isn't unique in the workspace.", mainClass)); + } + } + + return new ValidationResult(true); + } + + private List searchClassInProjectClasspaths(String fullyQualifiedClassName) throws CoreException { + return ResolveClasspathsHandler.getJavaProjectFromType(fullyQualifiedClassName); + } + + private ValidationResult validateProjectName(final String mainClass, final String projectName) { + if (StringUtils.isEmpty(projectName)) { + return new ValidationResult(true); + } + + if (JdtUtils.getJavaProject(projectName) == null) { + return new ValidationResult(false, String.format("ConfigError: The project '%s' is not a valid java project.", projectName)); + } + + return new ValidationResult(true); + } + + private List computeProposals(List arguments, final String mainClass, final String projectName) { + List proposals = resolveMainClassCore(arguments); + + Collections.sort(proposals, new ProposalItemComparator((ResolutionItem item) -> { + if (Objects.equals(item.mainClass, mainClass)) { + return 1; + } else if (Objects.equals(item.projectName, projectName)) { + return 2; + } + + return 999; + })); + + return proposals; + } + + private class ResolutionItem implements Comparable { private String mainClass; private String projectName; private String filePath; @@ -146,5 +249,53 @@ public class ResolveMainClassHandler { public int hashCode() { return Objects.hash(mainClass, projectName, filePath); } + + @Override + public int compareTo(ResolutionItem o) { + if (isDefaultProject(this.projectName) && !isDefaultProject(o.projectName)) { + return 1; + } else if (!isDefaultProject(this.projectName) && isDefaultProject(o.projectName)) { + return -1; + } + + return (this.projectName + "|" + this.mainClass).compareTo(o.projectName + "|" + o.mainClass); + } + + private boolean isDefaultProject(String projectName) { + return StringUtils.isEmpty(projectName); + } + } + + class ProposalItemComparator implements Comparator { + Function getRank; + + ProposalItemComparator(Function getRank) { + this.getRank = getRank; + } + + @Override + public int compare(ResolutionItem o1, ResolutionItem o2) { + return getRank.apply(o1) - getRank.apply(o2); + } + } + + class ValidationResponse { + ValidationResult mainClass; + ValidationResult projectName; + List proposals; + } + + class ValidationResult { + boolean isValid; + String message; + + ValidationResult(boolean isValid) { + this.isValid = isValid; + } + + ValidationResult(boolean isValid, String message) { + this.isValid = isValid; + this.message = message; + } } }