Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions com.microsoft.java.debug.plugin/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<command id="vscode.java.buildWorkspace"/>
<command id="vscode.java.fetchUsageData"/>
<command id="vscode.java.updateDebugSettings"/>
<command
id="vscode.java.validateLaunchConfig">
</command>
</delegateCommandHandler>
</extension>
</plugin>
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object> arguments, IProgressMonitor progress) throws Exception {
if (DEBUG_STARTSESSION.equals(commandId)) {
Expand All @@ -50,6 +52,8 @@ public Object executeCommand(String commandId, List<Object> arguments, IProgress
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about make class ResolveMainClassHandler a utility class(with a changed name) and not new ResolveMainClassHandler every time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess the original habit is inherited from jdt.ls handler implementation. If we just refactor this one, it will be a little weird, because ResolveClasspathsHandler has similar issue. So i prefer to keep the current convention.

}

throw new UnsupportedOperationException(String.format("Java debug plugin doesn't support the command '%s'.", commandId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private static IJavaProject getJavaProjectFromName(String projectName) throws Co
* @throws CoreException
* CoreException
*/
private static List<IJavaProject> getJavaProjectFromType(String fullyQualifiedTypeName) throws CoreException {
public static List<IJavaProject> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,19 +44,35 @@

public class ResolveMainClassHandler {
private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME);
private static final String CLASSNAME_REGX = "([$\\w]+\\.)*[$\\w]+";

@andxu Andy Xu(devdiv) (andxu) Aug 17, 2018 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a better way is to validate the main Class in js side, the java side only exposure ways of querying java projects and main classes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to separate the validation logic to two parts, that's hard for reading. JS side is responsible for UI related logic, and JAVA side is for computing the result.


/**
* 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<Object> arguments) throws CoreException {
public Object resolveMainClass(List<Object> arguments) {
return resolveMainClassCore(arguments);
}

private List<ResolutionItem> resolveMainClassCore(List<Object> 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<Object> 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<ResolutionItem> resolveMainClassCore(List<Object> 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<IPath> targetProjectPath = new ArrayList<>();
Expand All @@ -62,7 +82,7 @@ private List<ResolutionItem> resolveMainClassCore(List<Object> arguments) throws
IJavaSearchScope searchScope = SearchEngine.createWorkspaceScope();
SearchPattern pattern = SearchPattern.createPattern("main(String[]) void", IJavaSearchConstants.METHOD,
IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH);
ArrayList<ResolutionItem> res = new ArrayList<>();
final List<ResolutionItem> res = new ArrayList<>();
SearchRequestor requestor = new SearchRequestor() {
@Override
public void acceptSearchMatch(SearchMatch match) {
Expand Down Expand Up @@ -114,10 +134,93 @@ public void acceptSearchMatch(SearchMatch match) {
} 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<ResolutionItem> resolutions = res.stream().distinct().collect(Collectors.toList());
Collections.sort(resolutions);
return resolutions;
}

private ValidationResponse validateLaunchConfigCore(List<Object> 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<IJavaProject> 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<IJavaProject> 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<ResolutionItem> computeProposals(List<Object> arguments, final String mainClass, final String projectName) {
List<ResolutionItem> 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 {
private class ResolutionItem implements Comparable<ResolutionItem> {
private String mainClass;
private String projectName;
private String filePath;
Expand Down Expand Up @@ -146,5 +249,53 @@ public boolean equals(Object o) {
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<ResolutionItem> {
Function<ResolutionItem, Integer> getRank;

ProposalItemComparator(Function<ResolutionItem, Integer> 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<ResolutionItem> proposals;
}

class ValidationResult {
boolean isValid;
String message;

ValidationResult(boolean isValid) {
this.isValid = isValid;
}

ValidationResult(boolean isValid, String message) {
this.isValid = isValid;
this.message = message;
}
}
}