Merge pull request #4595 from sdedic/lsp/projectInfo

Project info exported through LSP
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/LspProjectInfo.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/LspProjectInfo.java
new file mode 100644
index 0000000..4e267b6
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/LspProjectInfo.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.java.lsp.server.project;
+
+import java.net.URI;
+
+/**
+ *
+ * @author sdedic
+ */
+public class LspProjectInfo {
+    /**
+     * Project's directory
+     */
+    public URI  projectDirectory;
+    
+    /**
+     * Project's name.
+     */
+    public String name;
+    
+    /**
+     * Project's display name as defined in project file(s)
+     */
+    public String displayName;
+    
+    /**
+     * The build system / project type. Ant, Gradle, Maven.
+     */
+    public String projectType;
+    
+    /**
+     * URIs of subprojects. Usually children of the project's own directory.
+     */
+    public URI[] subprojects;
+    
+    /**
+     * If part of a reactor or multi-project, the URI of the root project.
+     */
+    public URI rootProject;
+    
+    /**
+     * Supported project actions. Names.
+     */
+    public String[] projectActionNames;
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
index cdcf5cf..df866c4 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
@@ -749,7 +749,9 @@
                         JAVA_SUPER_IMPLEMENTATION,
                         JAVA_SOURCE_FOR,
                         JAVA_CLEAR_PROJECT_CACHES,
-                        NATIVE_IMAGE_FIND_DEBUG_PROCESS_TO_ATTACH));
+                        NATIVE_IMAGE_FIND_DEBUG_PROCESS_TO_ATTACH,
+                        JAVA_PROJECT_INFO
+                ));
                 for (CodeActionsProvider codeActionsProvider : Lookup.getDefault().lookupAll(CodeActionsProvider.class)) {
                     commands.addAll(codeActionsProvider.getCommands());
                 }
@@ -944,6 +946,12 @@
      * new project files were generated into workspace subtree.
      */
     public static final String JAVA_CLEAR_PROJECT_CACHES =  "java.clear.project.caches";
+    
+    /**
+     * For a project directory, returns basic project information and structure.
+     * Syntax: nbls.project.info(locations : String | String[], options? : { projectStructure? : boolean; actions? : boolean; recursive? : boolean }) : LspProjectInfo
+     */
+    public static final String JAVA_PROJECT_INFO = "nbls.project.info";
 
     static final String INDEXING_COMPLETED = "Indexing completed.";
     static final String NO_JAVA_SUPPORT = "Cannot initialize Java support on JDK ";
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java
index 3529067..2060a45 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java
@@ -19,6 +19,7 @@
 package org.netbeans.modules.java.lsp.server.protocol;
 
 import com.google.gson.Gson;
+import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
@@ -29,6 +30,7 @@
 import java.lang.reflect.Method;
 import java.net.MalformedURLException;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
@@ -48,6 +50,7 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
@@ -99,6 +102,7 @@
 import org.netbeans.api.java.source.ui.ElementOpen;
 import org.netbeans.api.project.FileOwnerQuery;
 import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectInformation;
 import org.netbeans.api.project.ProjectManager;
 import org.netbeans.api.project.ProjectUtils;
 import org.netbeans.api.project.SourceGroup;
@@ -111,6 +115,7 @@
 import org.netbeans.modules.java.lsp.server.Utils;
 import org.netbeans.modules.java.lsp.server.debugging.attach.AttachConfigurations;
 import org.netbeans.modules.java.lsp.server.debugging.attach.AttachNativeConfigurations;
+import org.netbeans.modules.java.lsp.server.project.LspProjectInfo;
 import org.netbeans.modules.java.source.ElementHandleAccessor;
 import org.netbeans.modules.java.source.ui.JavaSymbolProvider;
 import org.netbeans.modules.java.source.ui.JavaTypeProvider;
@@ -139,8 +144,11 @@
  * @author lahvac
  */
 public final class WorkspaceServiceImpl implements WorkspaceService, LanguageClientAware {
+    
+    private static final Logger LOG = Logger.getLogger(WorkspaceServiceImpl.class.getName());
 
     private static final RequestProcessor WORKER = new RequestProcessor(WorkspaceServiceImpl.class.getName(), 1, false, false);
+    private static final RequestProcessor PROJECT_WORKER = new RequestProcessor(WorkspaceServiceImpl.class.getName(), 5, false, false);
 
     private final Gson gson = new Gson();
     private final LspServerState server;
@@ -578,6 +586,56 @@
                 }
                 return (CompletableFuture<Object>) (CompletableFuture<?>)result;
             }
+            
+            case Server.JAVA_PROJECT_INFO: {
+                final CompletableFuture<Object> result = new CompletableFuture<>();
+                List<Object> arguments = params.getArguments();
+                if (arguments.size() < 1) {
+                    result.completeExceptionally(new IllegalArgumentException("Expecting URL or URL[] as an argument to " + command));
+                    return result;
+                }
+                Object o = arguments.get(0);
+                URL[] locations = null;
+                if (o instanceof JsonArray) {
+                    List<URL> locs = new ArrayList<>();
+                    JsonArray a = (JsonArray)o;
+                    a.forEach((e) -> {
+                        if (e instanceof JsonPrimitive) {
+                            String s = ((JsonPrimitive)e).getAsString();
+                            try {
+                                locs.add(new URL(s));
+                            } catch (MalformedURLException ex) {
+                                throw new IllegalArgumentException("Illegal location: " + s);
+                            }
+                        }
+                    });
+                } else if (o instanceof JsonPrimitive) {
+                    String s = ((JsonPrimitive)o).getAsString();
+                    try {
+                        locations = new URL[] { new URL(s) };
+                    } catch (MalformedURLException ex) {
+                        throw new IllegalArgumentException("Illegal location: " + s);
+                    }
+                }
+                if (locations == null || locations.length == 0) {
+                    result.completeExceptionally(new IllegalArgumentException("Expecting URL or URL[] as an argument to " + command));
+                    return result;
+                }
+                boolean projectStructure = false;
+                boolean actions = false;
+                boolean recursive = false;
+                
+                if (arguments.size() > 1) {
+                    Object a2 = arguments.get(1);
+                    if (a2 instanceof JsonObject) {
+                        JsonObject options = (JsonObject)a2;
+                        projectStructure = getOption(options, "projectStructure", false); // NOI18N
+                        actions = getOption(options, "actions", false); // NOI18N
+                        recursive = getOption(options, "recursive", false); // NOI18N
+                    }
+                }
+                return (CompletableFuture<Object>)(CompletableFuture<?>)new ProjectInfoWorker(locations, projectStructure, recursive, actions).process();
+            }
             default:
                 for (CodeActionsProvider codeActionsProvider : Lookup.getDefault().lookupAll(CodeActionsProvider.class)) {
                     if (codeActionsProvider.getCommands().contains(command)) {
@@ -588,6 +646,133 @@
         throw new UnsupportedOperationException("Command not supported: " + params.getCommand());
     }
     
+    private class ProjectInfoWorker {
+        final URL[] locations;
+        final boolean projectStructure;
+        final boolean recursive;
+        final boolean actions;
+        
+        Map<FileObject, LspProjectInfo> infos = new HashMap<>();
+        Set<Project> toOpen = new HashSet<>();
+
+        public ProjectInfoWorker(URL[] locations, boolean projectStructure, boolean recursive, boolean actions) {
+            this.locations = locations;
+            this.projectStructure = projectStructure;
+            this.recursive = recursive;
+            this.actions = actions;
+        }
+
+        public CompletableFuture<LspProjectInfo[]> process() {
+            List<FileObject> files = new ArrayList();
+            for (URL u : locations) {
+                FileObject f = URLMapper.findFileObject(u);
+                if (f != null) {
+                    files.add(f);
+                }
+            }
+            return server.asyncOpenSelectedProjects(files, false).thenCompose(this::processProjects);
+        }
+        
+        LspProjectInfo fillProjectInfo(Project p) {
+            LspProjectInfo info = infos.get(p.getProjectDirectory());
+            if (info != null) {
+                return info;
+            }
+            info = new LspProjectInfo();
+            
+            ProjectInformation pi = ProjectUtils.getInformation(p);
+            URL projectURL = URLMapper.findURL(p.getProjectDirectory(), URLMapper.EXTERNAL);
+            if (projectURL != null) {
+                try {
+                    info.projectDirectory = projectURL.toURI();
+                } catch (URISyntaxException ex) {
+                    // should not happen
+                }
+            }
+            info.name = pi.getName();
+            info.displayName = pi.getDisplayName();
+            
+            // attempt to determine the project type
+            ProjectManager.Result r = ProjectManager.getDefault().isProject2(p.getProjectDirectory());
+            info.projectType = r.getProjectType();
+            
+            if (actions) {
+                ActionProvider ap = p.getLookup().lookup(ActionProvider.class);
+                if (ap != null) {
+                    info.projectActionNames = ap.getSupportedActions();
+                }
+            }
+
+            if (projectStructure) {
+                Set<Project> children = ProjectUtils.getContainedProjects(p, false);
+                List<URI> subprojectDirs = new ArrayList<>();
+                for (Project c : children) {
+                    try {
+                        subprojectDirs.add(URLMapper.findURL(c.getProjectDirectory(), URLMapper.EXTERNAL).toURI());
+                    } catch (URISyntaxException ex) {
+                        // should not happen
+                    }
+                }
+                info.subprojects = subprojectDirs.toArray(new URI[subprojectDirs.size()]);
+                Project root = ProjectUtils.rootOf(p);
+                if (root != null) {
+                    try {
+                        info.rootProject = URLMapper.findURL(root.getProjectDirectory(), URLMapper.EXTERNAL).toURI();
+                    } catch (URISyntaxException ex) {
+                        // should not happen
+                    }
+                }
+                if (recursive) {
+                    toOpen.addAll(children);
+                }
+            }
+            infos.put(p.getProjectDirectory(), info);
+            return info;
+        }
+        
+        CompletableFuture<LspProjectInfo[]> processProjects(Project[] prjs) {
+            for (Project p : prjs) {
+                fillProjectInfo(p);
+            }
+            if (toOpen.isEmpty()) {
+                return finalizeInfos();
+            }
+            List<FileObject> dirs = new ArrayList<>(toOpen.size());
+            for (Project p : toOpen) {
+                dirs.add(p.getProjectDirectory());
+            }
+            toOpen.clear();
+            return server.asyncOpenSelectedProjects(dirs).thenCompose(this::processProjects);
+        }
+        
+        CompletableFuture<LspProjectInfo[]> finalizeInfos() {
+            List<LspProjectInfo> list = new ArrayList();
+            for (URL u : locations) {
+                FileObject f = URLMapper.findFileObject(u);
+                Project owner = FileOwnerQuery.getOwner(f);
+                if (owner != null) {
+                    list.add(infos.remove(owner.getProjectDirectory()));
+                } else {
+                    list.add(null);
+                }
+            }
+            list.addAll(infos.values());
+            LspProjectInfo[] toArray = list.toArray(new LspProjectInfo[list.size()]);
+            return CompletableFuture.completedFuture(toArray);
+        }
+    }
+    
+    private static boolean getOption(JsonObject opts, String member, boolean def) {
+        if (!opts.has(member)) {
+            return def;
+        }
+        Object o = opts.get(member);
+        if (!(o instanceof JsonPrimitive)) {
+            return false;
+        }
+        return ((JsonPrimitive)o).getAsBoolean();
+    }
+    
     private final AtomicReference<BiConsumer<FileObject, Collection<TestMethodController.TestMethod>>> testMethodsListener = new AtomicReference<>();
     private final AtomicReference<PropertyChangeListener> openProjectsListener = new AtomicReference<>();