All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.java.debug.plugin.internal.ResolveMainClassHandler Maven / Gradle / Ivy

There is a newer version: 0.53.1
Show newest version
/*******************************************************************************
 * Copyright (c) 2017-2022 Microsoft Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Microsoft Corporation - initial API and implementation
 *******************************************************************************/

package com.microsoft.java.debug.plugin.internal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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 java.util.stream.Stream;

import javax.lang.model.SourceVersion;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.core.SourceMethod;
import org.eclipse.jdt.ls.core.internal.ProjectUtils;
import org.eclipse.jdt.ls.core.internal.ResourceUtils;
import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager;

import com.microsoft.java.debug.core.Configuration;

public class ResolveMainClassHandler {
    private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME);
    private static final int CONFIGERROR_INVALID_CLASS_NAME = 1;
    private static final int CONFIGERROR_MAIN_CLASS_NOT_EXIST = 2;
    private static final int CONFIGERROR_MAIN_CLASS_NOT_UNIQUE = 3;
    private static final int CONFIGERROR_INVALID_JAVA_PROJECT = 4;

    /**
     * resolve main class and project name.
     * @return an array of main class and project name
     */
    public Object resolveMainClass(List arguments) {
        return resolveMainClassCore(arguments);
    }

    /**
     * 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) {
        if (arguments != null && arguments.size() > 0 && arguments.get(0) != null) {
            String argument = (String) arguments.get(0);
            IProject[] projects = ProjectUtils.getAllProjects();
            if (Stream.of(projects).anyMatch(project -> Objects.equals(project.getName(), argument))) {
                return resolveMainClassUnderProject(argument);
            }

            IPath rootPath = ResourceUtils.filePathFromURI(argument);
            if (rootPath != null) {
                return resolveMainClassUnderPaths(Arrays.asList(rootPath));
            }
        }

        return resolveMainClassUnderPaths(Collections.emptyList());
    }

    private List resolveMainClassUnderPaths(List parentPaths) {
        // Limit to search main method from source code only.
        IJavaSearchScope searchScope = SearchEngine.createJavaSearchScope(ProjectUtils.getJavaProjects(),
            IJavaSearchScope.REFERENCED_PROJECTS | IJavaSearchScope.SOURCES);
        SearchPattern pattern = createMainMethodSearchPattern();
        final List res = new ArrayList<>();
        SearchRequestor requestor = new SearchRequestor() {
            @Override
            public void acceptSearchMatch(SearchMatch match) {
                Object element = match.getElement();
                if (element instanceof IMethod) {
                    IMethod method = (IMethod) element;
                    if (isMainMethod(method)) {
                        IResource resource = method.getResource();
                        if (resource != null) {
                            IProject project = resource.getProject();
                            if (project != null) {
                                String mainClass = method.getDeclaringType().getFullyQualifiedName();
                                IJavaProject javaProject = JdtUtils.getJavaProject(project);
                                if (javaProject != null) {
                                    String moduleName = JdtUtils.getModuleName(javaProject);
                                    if (moduleName != null) {
                                        mainClass = moduleName + "/" + mainClass;
                                    }
                                }
                                String projectName = ProjectsManager.DEFAULT_PROJECT_NAME.equals(project.getName()) ? null : project.getName();
                                if (parentPaths.isEmpty()
                                    || ResourceUtils.isContainedIn(project.getLocation(), parentPaths)
                                    || isContainedInInvisibleProject(project, parentPaths)) {
                                    String filePath = null;

                                    if (match.getResource() instanceof IFile) {
                                        try {
                                            filePath = match.getResource().getLocation().toOSString();
                                        } catch (Exception ex) {
                                            // ignore
                                        }
                                    }
                                    res.add(new ResolutionItem(mainClass, projectName, filePath));
                                }
                            }
                        }
                    }
                }
            }
        };
        SearchEngine searchEngine = new SearchEngine();
        try {
            searchEngine.search(pattern, new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()},
                    searchScope, requestor, null /* progress monitor */);
        } catch (Exception e) {
            logger.log(Level.SEVERE, String.format("Searching the main class failure: %s", e.toString()), e);
        }

        List resolutions = res.stream().distinct().collect(Collectors.toList());
        Collections.sort(resolutions);
        return resolutions;
    }

    private List resolveMainClassUnderProject(final String projectName) {
        // Limit to search main method from source code only.
        IJavaProject javaProject = ProjectUtils.getJavaProject(projectName);
        IJavaSearchScope searchScope = SearchEngine.createJavaSearchScope(javaProject == null ? new IJavaProject[0] : new IJavaProject[] {javaProject},
            IJavaSearchScope.REFERENCED_PROJECTS | IJavaSearchScope.SOURCES);
        SearchPattern pattern = createMainMethodSearchPattern();
        final List res = new ArrayList<>();
        SearchRequestor requestor = new SearchRequestor() {
            @Override
            public void acceptSearchMatch(SearchMatch match) {
                Object element = match.getElement();
                if (element instanceof IMethod) {
                    IMethod method = (IMethod) element;
                    if (isMainMethod(method)) {
                        IResource resource = method.getResource();
                        if (resource != null) {
                            IProject project = resource.getProject();
                            if (project != null) {
                                String mainClass = method.getDeclaringType().getFullyQualifiedName();
                                IJavaProject javaProject = JdtUtils.getJavaProject(project);
                                if (javaProject != null) {
                                    String moduleName = JdtUtils.getModuleName(javaProject);
                                    if (moduleName != null) {
                                        mainClass = moduleName + "/" + mainClass;
                                    }
                                }

                                String filePath = null;
                                if (match.getResource() instanceof IFile) {
                                    try {
                                        filePath = match.getResource().getLocation().toOSString();
                                    } catch (Exception ex) {
                                        // ignore
                                    }
                                }
                                res.add(new ResolutionItem(mainClass, projectName, filePath));
                            }
                        }
                    }
                }
            }
        };
        SearchEngine searchEngine = new SearchEngine();
        try {
            searchEngine.search(pattern, new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()},
                    searchScope, requestor, null /* progress monitor */);
        } catch (Exception e) {
            logger.log(Level.SEVERE, String.format("Searching the main class failure: %s", e.toString()), e);
        }

        List resolutions = res.stream().distinct().collect(Collectors.toList());
        Collections.sort(resolutions);
        return resolutions;
    }

    private SearchPattern createMainMethodSearchPattern() {
        SearchPattern pattern1 = SearchPattern.createPattern("main(String[]) void", IJavaSearchConstants.METHOD,
                IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH);
        SearchPattern pattern2 = SearchPattern.createPattern("main() void", IJavaSearchConstants.METHOD,
                IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH);
        return SearchPattern.createOrPattern(pattern1, pattern2);
    }

    private boolean isMainMethod(IMethod method) {
        try {
            if (method instanceof SourceMethod
                    && ((SourceMethod) method).isMainMethodCandidate()) {
                return true;
            }

            return method.isMainMethod();
        } catch (JavaModelException e) {
            // do nothing
        }

        return false;
    }

    private boolean isContainedInInvisibleProject(IProject project, Collection rootPaths) {
        if (project == null) {
            return false;
        }

        IFolder workspaceLinkFolder = project.getFolder(ProjectUtils.WORKSPACE_LINK);
        return workspaceLinkFolder.exists() && ResourceUtils.isContainedIn(workspaceLinkFolder.getLocation(), rootPaths);
    }

    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 (!isValidMainClassName(mainClass)) {
            return new ValidationResult(false, String.format("ConfigError: '%s' is not a valid class name.", mainClass),
                CONFIGERROR_INVALID_CLASS_NAME);
        }

        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),
                    CONFIGERROR_MAIN_CLASS_NOT_EXIST);
            }
            if (javaProjects.size() > 1) {
                return new ValidationResult(false, String.format("ConfigError: Main class '%s' isn't unique in the workspace.", mainClass),
                    CONFIGERROR_MAIN_CLASS_NOT_UNIQUE);
            }
        }

        return new ValidationResult(true);
    }

    // Java command line supports two kinds of main class format:  and [/]
    private boolean isValidMainClassName(String mainClass) {
        if (StringUtils.isEmpty(mainClass)) {
            return true;
        }

        int index = mainClass.indexOf('/');
        if (index == -1) {
            return SourceVersion.isName(mainClass);
        }

        return SourceVersion.isName(mainClass.substring(0, index))
            && SourceVersion.isName(mainClass.substring(index + 1));
    }

    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),
                CONFIGERROR_INVALID_JAVA_PROJECT);
        }

        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;

        public ResolutionItem(String mainClass, String projectName, String filePath) {
            this.mainClass = mainClass;
            this.projectName = projectName;
            this.filePath = filePath;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o instanceof ResolutionItem) {
                ResolutionItem item = (ResolutionItem) o;
                return Objects.equals(mainClass, item.mainClass)
                        && Objects.equals(projectName, item.projectName)
                        && Objects.equals(filePath, item.filePath);
            }
            return false;
        }

        @Override
        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;
        int kind;

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

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