You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@netbeans.apache.org by GitBox <gi...@apache.org> on 2020/12/08 09:25:19 UTC

[GitHub] [netbeans] sdedic commented on a change in pull request #2575: Open GraalVM sources out of the box

sdedic commented on a change in pull request #2575:
URL: https://github.com/apache/netbeans/pull/2575#discussion_r538171530



##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteClassPathProvider.java
##########
@@ -0,0 +1,100 @@
+/*
+ * 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.mx.project;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.classpath.GlobalPathRegistry;
+import org.netbeans.api.java.classpath.JavaClassPathConstants;
+import org.netbeans.api.java.platform.JavaPlatformManager;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery.Result;
+import org.netbeans.spi.java.classpath.ClassPathProvider;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.AnnotationProcessingQueryImplementation;
+import org.netbeans.spi.project.ui.ProjectOpenedHook;
+import org.openide.filesystems.FileObject;
+
+final class SuiteClassPathProvider extends ProjectOpenedHook implements ClassPathProvider, AnnotationProcessingQueryImplementation {
+    private final SuiteProject project;
+    private final ClassPath bootCP;
+
+    public SuiteClassPathProvider(SuiteProject project) {
+        this.project = project;
+        List<ClassPath.Entry> entries = JavaPlatformManager.getDefault().getDefaultPlatform().getBootstrapLibraries().entries();
+        List<URL> roots = new ArrayList<>();
+        for (ClassPath.Entry entry : entries) {
+            URL root = entry.getURL();
+            if (root.getPath().contains("/graal-sdk.jar")) {

Review comment:
       Maybe this exclusion is worth a comment in source; why the exclusion is needed.

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteActionProvider.java
##########
@@ -0,0 +1,245 @@
+/*
+ * 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.mx.project;
+
+import java.awt.Toolkit;
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.Future;
+import org.netbeans.api.actions.Openable;
+import org.netbeans.api.debugger.*;
+import org.netbeans.api.debugger.jpda.ListeningDICookie;
+import org.netbeans.api.extexecution.ExecutionDescriptor;
+import org.netbeans.api.extexecution.ExecutionService;
+import org.netbeans.api.extexecution.base.ProcessBuilder;
+import org.netbeans.api.extexecution.print.ConvertedLine;
+import org.netbeans.spi.project.ActionProvider;
+import org.netbeans.spi.project.SingleMethod;
+import org.openide.LifecycleManager;
+import org.openide.cookies.LineCookie;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.text.Line;
+import org.openide.util.Lookup;
+import org.openide.util.NbBundle;
+import org.openide.util.RequestProcessor;
+import org.openide.windows.OutputEvent;
+import org.openide.windows.OutputListener;
+
+final class SuiteActionProvider implements ActionProvider {
+    private final SuiteProject prj;
+
+    SuiteActionProvider(SuiteProject prj) {
+        this.prj = prj;
+    }
+
+    @Override
+    public String[] getSupportedActions() {
+        return new String[] {
+            ActionProvider.COMMAND_CLEAN,
+            ActionProvider.COMMAND_BUILD,
+            ActionProvider.COMMAND_COMPILE_SINGLE,
+            ActionProvider.COMMAND_REBUILD,
+            ActionProvider.COMMAND_TEST_SINGLE,
+            ActionProvider.COMMAND_RUN_SINGLE,
+            ActionProvider.COMMAND_DEBUG_TEST_SINGLE,
+            ActionProvider.COMMAND_DEBUG_SINGLE,
+            SingleMethod.COMMAND_DEBUG_SINGLE_METHOD,
+            SingleMethod.COMMAND_RUN_SINGLE_METHOD,
+        };
+    }
+
+    @NbBundle.Messages({
+        "MSG_Clean=mx clean {0}",
+        "MSG_Build=mx build {0}",
+        "MSG_BuildOnly=mx build {0} --only {1}",
+        "MSG_Rebuild=mx rebuild {0}",
+        "MSG_Unittest=mx unittest {0}",
+    })
+    @Override
+    public void invokeAction(String action, Lookup context) throws IllegalArgumentException {
+        FileObject fo = context.lookup(FileObject.class);
+        String testSuffix = "";
+        switch (action) {
+            case ActionProvider.COMMAND_CLEAN:
+                runMx(Bundle.MSG_Clean(prj.getName()), "clean"); // NOI18N
+                break;
+            case ActionProvider.COMMAND_BUILD:
+                runMx(Bundle.MSG_Build(prj.getName()), "build"); // NOI18N
+                break;
+            case ActionProvider.COMMAND_REBUILD:
+                runMx(Bundle.MSG_Rebuild(prj.getName()), "build"); // NOI18N
+                break;
+            case ActionProvider.COMMAND_COMPILE_SINGLE: {
+                SuiteSources.Group grp = prj.getSources().findGroup(fo);
+                if (grp == null) {
+                    Toolkit.getDefaultToolkit().beep();
+                    return;
+                }
+                final String name = grp.getDisplayName();
+                runMx(Bundle.MSG_BuildOnly(prj.getName(), name), "build", "--only", name); // NOI18N
+                break;
+            }
+            case SingleMethod.COMMAND_RUN_SINGLE_METHOD: {
+                SingleMethod m = context.lookup(SingleMethod.class);
+                if (m != null && fo == null) {
+                    fo = m.getFile();
+                    testSuffix = "#" + m.getMethodName();
+                }
+                // fallthrough
+            }
+            case ActionProvider.COMMAND_TEST_SINGLE:
+            case ActionProvider.COMMAND_RUN_SINGLE:
+                if (fo == null) {
+                    Toolkit.getDefaultToolkit().beep();
+                    return;
+                }
+                runMx(Bundle.MSG_Unittest(fo.getName()), "unittest", fo.getName() + testSuffix); // NOI18N
+                break;
+            case SingleMethod.COMMAND_DEBUG_SINGLE_METHOD: {
+                SingleMethod m = context.lookup(SingleMethod.class);
+                if (m != null && fo == null) {
+                    fo = m.getFile();
+                    testSuffix = "#" + m.getMethodName();
+                }
+                // fallthrough
+            }
+            case ActionProvider.COMMAND_DEBUG_TEST_SINGLE:
+            case ActionProvider.COMMAND_DEBUG_SINGLE:
+                if (fo == null) {
+                    Toolkit.getDefaultToolkit().beep();
+                    return;
+                }
+                ListeningDICookie ldic = ListeningDICookie.create(-1);
+                Object obj = ldic.getArgs().get("port"); // NOI18N
+                DebuggerInfo di = DebuggerInfo.create(ListeningDICookie.ID, ldic);
+                DebuggerEngine[] engines = { null };
+                RequestProcessor.getDefault().post(() -> {
+                    DebuggerEngine[] engs = DebuggerManager.getDebuggerManager().startDebugging(di);
+                    engines[0] = engs[0];
+                });
+                int port = ldic.getPortNumber();
+                runMx(Bundle.MSG_Unittest(fo.getName()), "--attach", "" + port, "unittest", fo.getName() + testSuffix); // NOI18N
+                break;
+            default:
+                throw new UnsupportedOperationException(action);
+        }
+    }
+
+    private boolean runMx(String taskName, String... args) {
+        final File suiteDir = FileUtil.toFile(prj.getProjectDirectory());
+        if (!suiteDir.isDirectory()) {
+            Toolkit.getDefaultToolkit().beep();
+            return true;
+        }
+        LifecycleManager.getDefault().saveAll();
+        ExecutionDescriptor descriptor = new ExecutionDescriptor()
+                .frontWindow(true).controllable(true)
+                .errConvertorFactory(() -> {
+                    return (String line) -> {
+                        String[] segments = line.split(":");
+                        if (segments.length > 2) {
+                            File src = new File(segments[0]);
+                            if (src.exists()) {
+                                int lineNumber = parseLineNumber(segments) - 1;
+                                return Collections.singletonList(ConvertedLine.forText(line, new OutputListener() {
+                                    @Override
+                                    public void outputLineSelected(OutputEvent ev) {
+                                        openLine(Line.ShowOpenType.NONE, Line.ShowVisibilityType.FRONT);
+                                    }
+
+                                    @Override
+                                    public void outputLineAction(OutputEvent ev) {
+                                        openLine(Line.ShowOpenType.OPEN, Line.ShowVisibilityType.FOCUS);
+                                    }
+
+                                    private boolean openLine(final Line.ShowOpenType openType, final Line.ShowVisibilityType visibilityType) throws IndexOutOfBoundsException {
+                                        FileObject fo = FileUtil.toFileObject(src);
+                                        if (fo != null) {
+                                            Lookup lkp = fo.getLookup();
+                                            final LineCookie lines = lkp.lookup(LineCookie.class);
+                                            if (lines != null) {
+                                                Line open = lines.getLineSet().getOriginal(lineNumber);
+                                                if (open != null) {
+                                                    open.show(openType, visibilityType);
+                                                    return true;
+                                                }
+                                            }
+                                            Openable open = lkp.lookup(Openable.class);
+                                            if (open != null) {
+                                                open.open();
+                                            } else {
+                                                Toolkit.getDefaultToolkit().beep();
+                                            }
+                                        }
+                                        return false;
+                                    }
+
+                                    @Override
+                                    public void outputLineCleared(OutputEvent ev) {
+                                    }
+                                }));
+                            }
+                        }
+                        return null;
+                    };
+                });
+        ProcessBuilder processBuilder = ProcessBuilder.getLocal();
+        processBuilder.setWorkingDirectory(suiteDir.getPath());
+        processBuilder.setExecutable("mx"); // NOI18N

Review comment:
       This seems to assume `mx` is on the IDE process'  `PATH`. Shouldn't it be configurable in Tools | Options, similar to maven/ant, especially when NB does not distribute `mx` ? If reasonable, I recommend to track it as JIRA enhancement.

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteClassPathProvider.java
##########
@@ -0,0 +1,100 @@
+/*
+ * 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.mx.project;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.classpath.GlobalPathRegistry;
+import org.netbeans.api.java.classpath.JavaClassPathConstants;
+import org.netbeans.api.java.platform.JavaPlatformManager;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery.Result;
+import org.netbeans.spi.java.classpath.ClassPathProvider;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.AnnotationProcessingQueryImplementation;
+import org.netbeans.spi.project.ui.ProjectOpenedHook;
+import org.openide.filesystems.FileObject;
+
+final class SuiteClassPathProvider extends ProjectOpenedHook implements ClassPathProvider, AnnotationProcessingQueryImplementation {
+    private final SuiteProject project;
+    private final ClassPath bootCP;
+
+    public SuiteClassPathProvider(SuiteProject project) {
+        this.project = project;
+        List<ClassPath.Entry> entries = JavaPlatformManager.getDefault().getDefaultPlatform().getBootstrapLibraries().entries();
+        List<URL> roots = new ArrayList<>();
+        for (ClassPath.Entry entry : entries) {
+            URL root = entry.getURL();
+            if (root.getPath().contains("/graal-sdk.jar")) {
+                continue;
+            }
+            if (root.getPath().contains("/graaljs-scriptengine.jar")) {
+                continue;
+            }
+            if (root.getPath().contains("/graal-sdk.src.zip")) {
+                continue;
+            }
+            roots.add(entry.getURL());
+        }
+        this.bootCP = ClassPathSupport.createClassPath(roots.toArray(new URL[0]));
+    }
+
+    @Override
+    public ClassPath findClassPath(FileObject file, String type) {
+        SuiteSources.Group g = project.getSources().findGroup(file);
+        if (g == null) {
+            return null;
+        }
+        if (ClassPath.BOOT.equals(type)) {
+            return bootCP;

Review comment:
       I am not sure about this fixed bootclasspath, as there may be different `JAVA_HOME` defined in `~/.mx/env` or `mx.{projectname}/env` 

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteSources.java
##########
@@ -0,0 +1,1195 @@
+/*
+ * 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.mx.project;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.Icon;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.java.mx.project.suitepy.MxDistribution;
+import org.netbeans.modules.java.mx.project.suitepy.MxImports;
+import org.netbeans.modules.java.mx.project.suitepy.MxLibrary;
+import org.netbeans.modules.java.mx.project.suitepy.MxProject;
+import org.netbeans.modules.java.mx.project.suitepy.MxSuite;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery;
+import org.netbeans.api.java.queries.SourceForBinaryQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+import org.netbeans.spi.java.classpath.ClassPathFactory;
+import org.netbeans.spi.java.classpath.ClassPathImplementation;
+import org.netbeans.spi.java.classpath.FlaggedClassPathImplementation;
+import org.netbeans.spi.java.classpath.PathResourceImplementation;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation2;
+import org.netbeans.spi.java.queries.SourceForBinaryQueryImplementation2;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
+import org.openide.util.Exceptions;
+import org.openide.util.Utilities;
+import java.util.stream.Collectors;
+import org.netbeans.api.java.queries.SourceLevelQuery;
+import org.netbeans.spi.java.queries.MultipleRootsUnitTestForSourceQueryImplementation;
+import org.netbeans.spi.java.queries.SourceLevelQueryImplementation2;
+import org.netbeans.spi.project.SubprojectProvider;
+
+final class SuiteSources implements Sources,
+                BinaryForSourceQueryImplementation2<SuiteSources.Group>, SourceForBinaryQueryImplementation2,
+                SourceLevelQueryImplementation2, SubprojectProvider, MultipleRootsUnitTestForSourceQueryImplementation {
+    private static final Logger LOG = Logger.getLogger(SuiteSources.class.getName());
+    private static final SuiteSources CORE;
+
+    static {
+        MxSuite coreSuite = CoreSuite.CORE_5_279_0;
+        CORE = new SuiteSources(null, null, coreSuite);
+    }
+
+    private final MxSuite suite;
+    private final List<Group> groups;
+    private final List<Library> libraries;
+    private final List<Dist> distributions;
+    private final FileObject dir;
+    /**
+     * non-null if the dependencies haven't yet been properly initialized
+     */
+    private Map<String, Dep> transitiveDeps;
+    /**
+     * avoid GC of imported projects
+     */
+    private final SuiteProject prj;
+    private final Map<String, SuiteSources> imported;
+
+    SuiteSources(SuiteProject owner, FileObject dir, MxSuite suite) {
+        final Map<String, Dep> fillDeps = new HashMap<>();
+        this.prj = owner;
+        this.dir = dir;
+        this.groups = findGroups(fillDeps, suite, dir);
+        this.libraries = findLibraries(fillDeps, suite);
+        this.imported = findImportedSuites(dir, suite, fillDeps);
+        this.distributions = findDistributions(suite, this.libraries, this.groups, fillDeps);
+        this.suite = suite;
+        this.transitiveDeps = fillDeps;
+    }
+
+    @Override
+    public String toString() {
+        return "MxSources[" + (dir == null ? "mx" : dir.toURI()) + "]";
+    }
+
+    private List<Group> findGroups(Map<String, Dep> fillDeps, MxSuite s, FileObject dir) {
+        List<Group> arr = new ArrayList<>();
+        for (Map.Entry<String, MxProject> entry : s.projects().entrySet()) {
+            String name = entry.getKey();
+            MxProject mxPrj = entry.getValue();
+            FileObject prjDir = findPrjDir(dir, name, mxPrj);
+            if (prjDir == null) {
+                fillDeps.put(name, new Group(name, mxPrj, null, null, null, name, name));
+                continue;
+            }
+            String prevName = null;
+            Group firstGroup = null;
+            String binPrefix;
+            if (mxPrj.subDir() == null) {
+                binPrefix = "mxbuild/";
+            } else {
+                binPrefix = "mxbuild/" + mxPrj.subDir() + "/";
+            }
+            for (String rel : mxPrj.sourceDirs()) {
+                FileObject srcDir = prjDir.getFileObject(rel);
+                FileObject binDir = getSubDir(dir, binPrefix + name + "/bin");
+                FileObject srcGenDir = getSubDir(dir, binPrefix + name + "/src_gen");
+                if (srcDir != null && binDir != null) {
+                    String prgName = name + "-" + rel;
+                    String displayName;
+                    if (prevName == null) {
+                        displayName = name;
+                    } else {
+                        displayName = name + "[" + rel + "]";
+                    }
+                    Group g = new Group(name, mxPrj, srcDir, srcGenDir, binDir, prgName, displayName);
+                    arr.add(g);
+                    if (firstGroup == null) {
+                        firstGroup = g;
+                    }
+                    prevName = displayName;
+                }
+            }
+            if (firstGroup != null) {
+                fillDeps.put(name, firstGroup);
+            }
+        }
+        return arr;
+    }
+
+    private static FileObject getSubDir(FileObject dir, String relPath) {
+        FileObject subDir = dir.getFileObject(relPath);
+        if (subDir == null) {
+            try {
+                subDir = FileUtil.createFolder(dir, relPath);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return subDir;
+    }
+
+    private List<Library> findLibraries(Map<String, Dep> fillDeps, MxSuite suite) {
+        final Map<String, MxLibrary> allLibraries = new HashMap<>();
+        registerLibs(allLibraries, null, suite.libraries());
+
+        List<Library> arr = new ArrayList<>();
+        for (Map.Entry<String, MxLibrary> entry : allLibraries.entrySet()) {
+            final Library library = new Library(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        for (Map.Entry<String, MxLibrary> entry : suite.jdklibraries().entrySet()) {
+            final JdkLibrary library = new JdkLibrary(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        return arr;
+    }
+
+    private static Map<String, SuiteSources> findImportedSuites(FileObject dir, MxSuite s, Map<String, Dep> fillDeps) {
+        if (dir == null) {
+            return Collections.emptyMap();
+        }
+        CORE.registerDeps("mx", fillDeps);
+        final MxImports imports = s.imports();
+        if (imports != null) {
+            Map<String, SuiteSources> imported = new LinkedHashMap<>();
+            for (MxImports.Suite imp : imports.suites()) {
+                SuiteSources impSources = findSuiteSources(dir, imp);
+                final String suiteName = imp.name();
+                if (impSources == null) {
+                    LOG.log(Level.INFO, "cannot find imported suite: {0}", suiteName);
+                    continue;
+                }
+                imported.put(suiteName, impSources);
+                impSources.registerDeps(suiteName, fillDeps);
+            }
+            return imported;
+        }
+        return Collections.emptyMap();
+    }
+
+    private List<Dist> findDistributions(MxSuite s, List<Library> libraries, List<Group> groups, Map<String, Dep> fillDeps) {
+        List<Dist> dists = new ArrayList<>();
+        for (Map.Entry<String, MxDistribution> entry : s.distributions().entrySet()) {
+            Dist d = new Dist(entry.getKey(), entry.getValue());
+            dists.add(d);
+            fillDeps.put(d.getName(), d);
+        }
+        return dists;
+    }
+
+    final synchronized void computeTransitiveDeps() {
+        Map<String, Dep> collectedDeps = this.transitiveDeps;
+        if (collectedDeps == null) {
+            return;
+        }
+        this.transitiveDeps = null;
+        for (Library l : this.libraries) {
+            transitiveDeps(l, collectedDeps);
+        }
+        for (Group g : this.groups) {
+            transitiveDeps(g, collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            transitiveDeps(d, collectedDeps);
+        }
+        for (Group g : groups) {
+            g.computeClassPath(collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            d.computeSourceRoots(collectedDeps);
+        }
+    }
+
+    private static SuiteSources findSuiteSources(FileObject dir, MxImports.Suite imp) throws IllegalArgumentException {
+        SuiteSources sources = findSuiteSources(dir.getParent(), imp.name());
+        if (sources != null) {
+            return sources;
+        }
+        if (imp.subdir()) {
+            for (FileObject subDir : dir.getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+            for (FileObject subDir : dir.getParent().getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static SuiteSources findSuiteSources(FileObject root, String name) throws IllegalArgumentException {
+        FileObject impDir = root.getFileObject(name);
+        if (impDir != null) {
+            try {
+                Project impPrj = ProjectManager.getDefault().findProject(impDir);
+                return impPrj == null ? null : impPrj.getLookup().lookup(SuiteSources.class);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceGroup[] getSourceGroups(String string) {
+        return groups();
+    }
+
+    Group[] groups() {
+        return groups.toArray(new Group[0]);
+    }
+
+    Group findGroup(FileObject fo) {
+        for (Group g : groups) {
+            if (g.contains(fo)) {
+                return g;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void addChangeListener(ChangeListener cl) {
+    }
+
+    @Override
+    public void removeChangeListener(ChangeListener cl) {
+    }
+
+    private static FileObject findPrjDir(FileObject dir, String prjName, MxProject prj) {
+        if (dir == null) {
+            return null;
+        }
+        if (prj.dir() != null) {
+            return dir.getFileObject(prj.dir());
+        }
+        if (prj.subDir() != null) {
+            dir = dir.getFileObject(prj.subDir());
+            if (dir == null) {
+                return null;
+            }
+        }
+        return dir.getFileObject(prjName);
+    }
+
+    private Collection<Dep> transitiveDeps(Dep current, Map<String, Dep> fill) {
+        current.owner().computeTransitiveDeps();
+        final Collection<Dep> currentAllDeps = current.allDeps();
+        if (currentAllDeps == Collections.<Dep>emptySet()) {
+            throw new IllegalStateException("Cyclic dep on " + current.getName());
+        } else if (currentAllDeps != null) {
+            return currentAllDeps;
+        }
+        current.setAllDeps(Collections.emptySet());
+        TreeSet<Dep> computing = new TreeSet<>();
+        computing.add(current);
+        for (String depName : current.depNames()) {
+            Dep dep = fill.get(depName);
+            if (dep == null) {
+                int colon = depName.lastIndexOf(':');
+                dep = fill.get(depName.substring(colon + 1));
+                if (dep == null) {
+                    LOG.log(Level.INFO, "dep not found: {0}", depName);
+                    continue;
+                }
+            }
+            Collection<Dep> allDeps = transitiveDeps(dep, fill);
+            computing.addAll(allDeps);
+        }
+        current.setAllDeps(computing);
+        return computing;
+    }
+
+    private static void registerLibs(Map<String, MxLibrary> collect, String prefix, Map<String, MxLibrary> libraries) {
+        for (Map.Entry<String, MxLibrary> entry : libraries.entrySet()) {
+            String key = entry.getKey();
+            MxLibrary lib = entry.getValue();
+            if (prefix == null) {
+                collect.put(key, lib);
+            } else {
+                collect.put(prefix + ":" + key, lib);
+            }
+        }
+    }
+
+    private void registerDeps(String prefix, Map<String, Dep> fillDeps) {
+        for (Library library : libraries) {
+            fillDeps.put(prefix + ":" + library.getName(), library);
+        }
+        for (Dist d : distributions) {
+            fillDeps.put(prefix + ":" + d.getName(), d);
+        }
+        for (Map.Entry<String, SuiteSources> s : imported.entrySet()) {
+            s.getValue().registerDeps(s.getKey(), fillDeps);
+        }
+    }
+
+    @Override
+    public Group findBinaryRoots2(URL url) {
+        final FileObject srcFo = URLMapper.findFileObject(url);
+        for (Group group : this.groups) {
+            if (group.contains(srcFo)) {
+                return group;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public URL[] computeRoots(Group group) {
+        if (group.binDir != null) {
+            return new URL[] { group.binDir.toURL() };
+        } else {
+            return new URL[0];
+        }
+    }
+
+    @Override
+    public boolean computePreferBinaries(Group result) {
+        return true;
+    }
+
+    @Override
+    public void computeChangeListener(Group result, boolean bln, ChangeListener cl) {
+    }
+
+    @Override
+    public SourceForBinaryQueryImplementation2.Result findSourceRoots2(URL url) {
+        this.computeTransitiveDeps();
+        for (Dist dist : this.distributions) {
+            URL jar;
+            try {
+                jar = dist.getJarRoot();
+                if (jar == null) {
+                    continue;
+                }
+            } catch (MalformedURLException ok) {
+                continue;
+            }
+            if (jar.equals(url)) {
+                List<FileObject> roots = new ArrayList<>();
+                for (Group d : dist.getContributingGroups()) {
+                    roots.add(d.srcDir);
+                    roots.add(d.srcGenDir);
+                }
+                return new ImmutableResult(roots.toArray(new FileObject[roots.size()]));
+            }
+        }
+        for (Group group : this.groups) {
+            if (group.binDir != null && group.binDir.toURL().equals(url)) {
+                return new ImmutableResult(group.srcDir, group.srcGenDir);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceForBinaryQuery.Result findSourceRoots(URL url) {
+        return findSourceRoots2(url);
+    }
+
+    final Iterable<File> jdks() {
+        Set<File> jdks = new LinkedHashSet<>();
+        String home = System.getProperty("user.home");
+        if (home != null) {
+            File userEnv = new File(new File(new File(home), ".mx"), "env");
+            findJdksInEnv(jdks, userEnv);
+        }
+        FileObject suiteEnv = dir.getFileObject("mx." + dir.getNameExt() + "/env");
+        if (suiteEnv != null) {
+            findJdksInEnv(jdks, FileUtil.toFile(suiteEnv));
+        }
+
+        String javaHomeEnv = System.getenv("JAVA_HOME");
+        if (javaHomeEnv != null) {
+            jdks.add(new File(javaHomeEnv));
+        }
+        String javaHomeProp = System.getProperty("java.home");
+        if (javaHomeProp != null) {
+            jdks.add(new File(javaHomeProp));
+        }
+        return jdks;
+    }
+
+    private void findJdksInEnv(Set<File> jdks, File env) {
+        if (env == null || !env.isFile()) {
+            return;
+        }
+        try (final FileInputStream is = new FileInputStream(env)) {
+            Properties p = new Properties();
+            p.load(is);
+
+            String javaHome = p.getProperty("JAVA_HOME");
+            if (javaHome != null) {
+                jdks.add(new File(javaHome));
+            }
+
+            String extraJavaHomes = p.getProperty("EXTRA_JAVA_HOMES");
+            if (extraJavaHomes != null) {
+                for (String extraHome : extraJavaHomes.split(File.pathSeparator)) {
+                    jdks.add(new File(extraHome));
+                }
+            }
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    @Override
+    public SourceLevelQueryImplementation2.Result getSourceLevel(FileObject fo) {
+        Group g = findGroup(fo);
+        if (g == null) {
+            return null;
+        }
+        return new SourceLevelQueryImplementation2.Result2() {
+            @Override
+            public SourceLevelQuery.Profile getProfile() {
+                return SourceLevelQuery.Profile.DEFAULT;
+            }
+
+            @Override
+            public String getSourceLevel() {
+                return g.getCompliance().getSourceLevel();
+            }
+
+            @Override
+            public void addChangeListener(ChangeListener cl) {
+            }
+
+            @Override
+            public void removeChangeListener(ChangeListener cl) {
+            }
+        };
+    }
+
+    @Override
+    public Set<? extends Project> getSubprojects() {
+        Set<Project> prjs = new HashSet<>();
+        for (SuiteSources imp : imported.values()) {
+            prjs.add(imp.prj);
+        }
+        return prjs;
+    }
+
+    @Override
+    public URL[] findUnitTests(FileObject fo) {
+        return new URL[0];

Review comment:
       no unit tests reported ?

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteSources.java
##########
@@ -0,0 +1,1195 @@
+/*
+ * 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.mx.project;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.Icon;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.java.mx.project.suitepy.MxDistribution;
+import org.netbeans.modules.java.mx.project.suitepy.MxImports;
+import org.netbeans.modules.java.mx.project.suitepy.MxLibrary;
+import org.netbeans.modules.java.mx.project.suitepy.MxProject;
+import org.netbeans.modules.java.mx.project.suitepy.MxSuite;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery;
+import org.netbeans.api.java.queries.SourceForBinaryQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+import org.netbeans.spi.java.classpath.ClassPathFactory;
+import org.netbeans.spi.java.classpath.ClassPathImplementation;
+import org.netbeans.spi.java.classpath.FlaggedClassPathImplementation;
+import org.netbeans.spi.java.classpath.PathResourceImplementation;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation2;
+import org.netbeans.spi.java.queries.SourceForBinaryQueryImplementation2;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
+import org.openide.util.Exceptions;
+import org.openide.util.Utilities;
+import java.util.stream.Collectors;
+import org.netbeans.api.java.queries.SourceLevelQuery;
+import org.netbeans.spi.java.queries.MultipleRootsUnitTestForSourceQueryImplementation;
+import org.netbeans.spi.java.queries.SourceLevelQueryImplementation2;
+import org.netbeans.spi.project.SubprojectProvider;
+
+final class SuiteSources implements Sources,
+                BinaryForSourceQueryImplementation2<SuiteSources.Group>, SourceForBinaryQueryImplementation2,
+                SourceLevelQueryImplementation2, SubprojectProvider, MultipleRootsUnitTestForSourceQueryImplementation {
+    private static final Logger LOG = Logger.getLogger(SuiteSources.class.getName());
+    private static final SuiteSources CORE;
+
+    static {
+        MxSuite coreSuite = CoreSuite.CORE_5_279_0;
+        CORE = new SuiteSources(null, null, coreSuite);
+    }
+
+    private final MxSuite suite;
+    private final List<Group> groups;
+    private final List<Library> libraries;
+    private final List<Dist> distributions;
+    private final FileObject dir;
+    /**
+     * non-null if the dependencies haven't yet been properly initialized
+     */
+    private Map<String, Dep> transitiveDeps;
+    /**
+     * avoid GC of imported projects
+     */
+    private final SuiteProject prj;
+    private final Map<String, SuiteSources> imported;
+
+    SuiteSources(SuiteProject owner, FileObject dir, MxSuite suite) {
+        final Map<String, Dep> fillDeps = new HashMap<>();
+        this.prj = owner;
+        this.dir = dir;
+        this.groups = findGroups(fillDeps, suite, dir);
+        this.libraries = findLibraries(fillDeps, suite);
+        this.imported = findImportedSuites(dir, suite, fillDeps);
+        this.distributions = findDistributions(suite, this.libraries, this.groups, fillDeps);
+        this.suite = suite;
+        this.transitiveDeps = fillDeps;
+    }
+
+    @Override
+    public String toString() {
+        return "MxSources[" + (dir == null ? "mx" : dir.toURI()) + "]";
+    }
+
+    private List<Group> findGroups(Map<String, Dep> fillDeps, MxSuite s, FileObject dir) {
+        List<Group> arr = new ArrayList<>();
+        for (Map.Entry<String, MxProject> entry : s.projects().entrySet()) {
+            String name = entry.getKey();
+            MxProject mxPrj = entry.getValue();
+            FileObject prjDir = findPrjDir(dir, name, mxPrj);
+            if (prjDir == null) {
+                fillDeps.put(name, new Group(name, mxPrj, null, null, null, name, name));
+                continue;
+            }
+            String prevName = null;
+            Group firstGroup = null;
+            String binPrefix;
+            if (mxPrj.subDir() == null) {
+                binPrefix = "mxbuild/";
+            } else {
+                binPrefix = "mxbuild/" + mxPrj.subDir() + "/";
+            }
+            for (String rel : mxPrj.sourceDirs()) {
+                FileObject srcDir = prjDir.getFileObject(rel);
+                FileObject binDir = getSubDir(dir, binPrefix + name + "/bin");
+                FileObject srcGenDir = getSubDir(dir, binPrefix + name + "/src_gen");
+                if (srcDir != null && binDir != null) {
+                    String prgName = name + "-" + rel;
+                    String displayName;
+                    if (prevName == null) {
+                        displayName = name;
+                    } else {
+                        displayName = name + "[" + rel + "]";
+                    }
+                    Group g = new Group(name, mxPrj, srcDir, srcGenDir, binDir, prgName, displayName);
+                    arr.add(g);
+                    if (firstGroup == null) {
+                        firstGroup = g;
+                    }
+                    prevName = displayName;
+                }
+            }
+            if (firstGroup != null) {
+                fillDeps.put(name, firstGroup);
+            }
+        }
+        return arr;
+    }
+
+    private static FileObject getSubDir(FileObject dir, String relPath) {
+        FileObject subDir = dir.getFileObject(relPath);
+        if (subDir == null) {
+            try {
+                subDir = FileUtil.createFolder(dir, relPath);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return subDir;
+    }
+
+    private List<Library> findLibraries(Map<String, Dep> fillDeps, MxSuite suite) {
+        final Map<String, MxLibrary> allLibraries = new HashMap<>();
+        registerLibs(allLibraries, null, suite.libraries());
+
+        List<Library> arr = new ArrayList<>();
+        for (Map.Entry<String, MxLibrary> entry : allLibraries.entrySet()) {
+            final Library library = new Library(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        for (Map.Entry<String, MxLibrary> entry : suite.jdklibraries().entrySet()) {
+            final JdkLibrary library = new JdkLibrary(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        return arr;
+    }
+
+    private static Map<String, SuiteSources> findImportedSuites(FileObject dir, MxSuite s, Map<String, Dep> fillDeps) {
+        if (dir == null) {
+            return Collections.emptyMap();
+        }
+        CORE.registerDeps("mx", fillDeps);
+        final MxImports imports = s.imports();
+        if (imports != null) {
+            Map<String, SuiteSources> imported = new LinkedHashMap<>();
+            for (MxImports.Suite imp : imports.suites()) {
+                SuiteSources impSources = findSuiteSources(dir, imp);
+                final String suiteName = imp.name();
+                if (impSources == null) {
+                    LOG.log(Level.INFO, "cannot find imported suite: {0}", suiteName);
+                    continue;
+                }
+                imported.put(suiteName, impSources);
+                impSources.registerDeps(suiteName, fillDeps);
+            }
+            return imported;
+        }
+        return Collections.emptyMap();
+    }
+
+    private List<Dist> findDistributions(MxSuite s, List<Library> libraries, List<Group> groups, Map<String, Dep> fillDeps) {
+        List<Dist> dists = new ArrayList<>();
+        for (Map.Entry<String, MxDistribution> entry : s.distributions().entrySet()) {
+            Dist d = new Dist(entry.getKey(), entry.getValue());
+            dists.add(d);
+            fillDeps.put(d.getName(), d);
+        }
+        return dists;
+    }
+
+    final synchronized void computeTransitiveDeps() {
+        Map<String, Dep> collectedDeps = this.transitiveDeps;
+        if (collectedDeps == null) {
+            return;
+        }
+        this.transitiveDeps = null;
+        for (Library l : this.libraries) {
+            transitiveDeps(l, collectedDeps);
+        }
+        for (Group g : this.groups) {
+            transitiveDeps(g, collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            transitiveDeps(d, collectedDeps);
+        }
+        for (Group g : groups) {
+            g.computeClassPath(collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            d.computeSourceRoots(collectedDeps);
+        }
+    }
+
+    private static SuiteSources findSuiteSources(FileObject dir, MxImports.Suite imp) throws IllegalArgumentException {
+        SuiteSources sources = findSuiteSources(dir.getParent(), imp.name());
+        if (sources != null) {
+            return sources;
+        }
+        if (imp.subdir()) {
+            for (FileObject subDir : dir.getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+            for (FileObject subDir : dir.getParent().getParent().getChildren()) {

Review comment:
       Is the 2-level parent limit hardocded in `mx` as well ?

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteSources.java
##########
@@ -0,0 +1,1195 @@
+/*
+ * 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.mx.project;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.Icon;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.java.mx.project.suitepy.MxDistribution;
+import org.netbeans.modules.java.mx.project.suitepy.MxImports;
+import org.netbeans.modules.java.mx.project.suitepy.MxLibrary;
+import org.netbeans.modules.java.mx.project.suitepy.MxProject;
+import org.netbeans.modules.java.mx.project.suitepy.MxSuite;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery;
+import org.netbeans.api.java.queries.SourceForBinaryQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+import org.netbeans.spi.java.classpath.ClassPathFactory;
+import org.netbeans.spi.java.classpath.ClassPathImplementation;
+import org.netbeans.spi.java.classpath.FlaggedClassPathImplementation;
+import org.netbeans.spi.java.classpath.PathResourceImplementation;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation2;
+import org.netbeans.spi.java.queries.SourceForBinaryQueryImplementation2;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
+import org.openide.util.Exceptions;
+import org.openide.util.Utilities;
+import java.util.stream.Collectors;
+import org.netbeans.api.java.queries.SourceLevelQuery;
+import org.netbeans.spi.java.queries.MultipleRootsUnitTestForSourceQueryImplementation;
+import org.netbeans.spi.java.queries.SourceLevelQueryImplementation2;
+import org.netbeans.spi.project.SubprojectProvider;
+
+final class SuiteSources implements Sources,
+                BinaryForSourceQueryImplementation2<SuiteSources.Group>, SourceForBinaryQueryImplementation2,
+                SourceLevelQueryImplementation2, SubprojectProvider, MultipleRootsUnitTestForSourceQueryImplementation {
+    private static final Logger LOG = Logger.getLogger(SuiteSources.class.getName());
+    private static final SuiteSources CORE;
+
+    static {
+        MxSuite coreSuite = CoreSuite.CORE_5_279_0;
+        CORE = new SuiteSources(null, null, coreSuite);
+    }
+
+    private final MxSuite suite;
+    private final List<Group> groups;
+    private final List<Library> libraries;
+    private final List<Dist> distributions;
+    private final FileObject dir;
+    /**
+     * non-null if the dependencies haven't yet been properly initialized
+     */
+    private Map<String, Dep> transitiveDeps;
+    /**
+     * avoid GC of imported projects
+     */
+    private final SuiteProject prj;
+    private final Map<String, SuiteSources> imported;
+
+    SuiteSources(SuiteProject owner, FileObject dir, MxSuite suite) {
+        final Map<String, Dep> fillDeps = new HashMap<>();
+        this.prj = owner;
+        this.dir = dir;
+        this.groups = findGroups(fillDeps, suite, dir);
+        this.libraries = findLibraries(fillDeps, suite);
+        this.imported = findImportedSuites(dir, suite, fillDeps);
+        this.distributions = findDistributions(suite, this.libraries, this.groups, fillDeps);
+        this.suite = suite;
+        this.transitiveDeps = fillDeps;
+    }
+
+    @Override
+    public String toString() {
+        return "MxSources[" + (dir == null ? "mx" : dir.toURI()) + "]";
+    }
+
+    private List<Group> findGroups(Map<String, Dep> fillDeps, MxSuite s, FileObject dir) {
+        List<Group> arr = new ArrayList<>();
+        for (Map.Entry<String, MxProject> entry : s.projects().entrySet()) {
+            String name = entry.getKey();
+            MxProject mxPrj = entry.getValue();
+            FileObject prjDir = findPrjDir(dir, name, mxPrj);
+            if (prjDir == null) {
+                fillDeps.put(name, new Group(name, mxPrj, null, null, null, name, name));
+                continue;
+            }
+            String prevName = null;
+            Group firstGroup = null;
+            String binPrefix;
+            if (mxPrj.subDir() == null) {
+                binPrefix = "mxbuild/";
+            } else {
+                binPrefix = "mxbuild/" + mxPrj.subDir() + "/";
+            }
+            for (String rel : mxPrj.sourceDirs()) {
+                FileObject srcDir = prjDir.getFileObject(rel);
+                FileObject binDir = getSubDir(dir, binPrefix + name + "/bin");
+                FileObject srcGenDir = getSubDir(dir, binPrefix + name + "/src_gen");
+                if (srcDir != null && binDir != null) {
+                    String prgName = name + "-" + rel;
+                    String displayName;
+                    if (prevName == null) {
+                        displayName = name;
+                    } else {
+                        displayName = name + "[" + rel + "]";
+                    }
+                    Group g = new Group(name, mxPrj, srcDir, srcGenDir, binDir, prgName, displayName);
+                    arr.add(g);
+                    if (firstGroup == null) {
+                        firstGroup = g;
+                    }
+                    prevName = displayName;
+                }
+            }
+            if (firstGroup != null) {
+                fillDeps.put(name, firstGroup);
+            }
+        }
+        return arr;
+    }
+
+    private static FileObject getSubDir(FileObject dir, String relPath) {
+        FileObject subDir = dir.getFileObject(relPath);
+        if (subDir == null) {
+            try {
+                subDir = FileUtil.createFolder(dir, relPath);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return subDir;
+    }
+
+    private List<Library> findLibraries(Map<String, Dep> fillDeps, MxSuite suite) {
+        final Map<String, MxLibrary> allLibraries = new HashMap<>();
+        registerLibs(allLibraries, null, suite.libraries());
+
+        List<Library> arr = new ArrayList<>();
+        for (Map.Entry<String, MxLibrary> entry : allLibraries.entrySet()) {
+            final Library library = new Library(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        for (Map.Entry<String, MxLibrary> entry : suite.jdklibraries().entrySet()) {
+            final JdkLibrary library = new JdkLibrary(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        return arr;
+    }
+
+    private static Map<String, SuiteSources> findImportedSuites(FileObject dir, MxSuite s, Map<String, Dep> fillDeps) {
+        if (dir == null) {
+            return Collections.emptyMap();
+        }
+        CORE.registerDeps("mx", fillDeps);
+        final MxImports imports = s.imports();
+        if (imports != null) {
+            Map<String, SuiteSources> imported = new LinkedHashMap<>();
+            for (MxImports.Suite imp : imports.suites()) {
+                SuiteSources impSources = findSuiteSources(dir, imp);
+                final String suiteName = imp.name();
+                if (impSources == null) {
+                    LOG.log(Level.INFO, "cannot find imported suite: {0}", suiteName);
+                    continue;
+                }
+                imported.put(suiteName, impSources);
+                impSources.registerDeps(suiteName, fillDeps);
+            }
+            return imported;
+        }
+        return Collections.emptyMap();
+    }
+
+    private List<Dist> findDistributions(MxSuite s, List<Library> libraries, List<Group> groups, Map<String, Dep> fillDeps) {
+        List<Dist> dists = new ArrayList<>();
+        for (Map.Entry<String, MxDistribution> entry : s.distributions().entrySet()) {
+            Dist d = new Dist(entry.getKey(), entry.getValue());
+            dists.add(d);
+            fillDeps.put(d.getName(), d);
+        }
+        return dists;
+    }
+
+    final synchronized void computeTransitiveDeps() {
+        Map<String, Dep> collectedDeps = this.transitiveDeps;
+        if (collectedDeps == null) {
+            return;
+        }
+        this.transitiveDeps = null;
+        for (Library l : this.libraries) {
+            transitiveDeps(l, collectedDeps);
+        }
+        for (Group g : this.groups) {
+            transitiveDeps(g, collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            transitiveDeps(d, collectedDeps);
+        }
+        for (Group g : groups) {
+            g.computeClassPath(collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            d.computeSourceRoots(collectedDeps);
+        }
+    }
+
+    private static SuiteSources findSuiteSources(FileObject dir, MxImports.Suite imp) throws IllegalArgumentException {
+        SuiteSources sources = findSuiteSources(dir.getParent(), imp.name());
+        if (sources != null) {
+            return sources;
+        }
+        if (imp.subdir()) {
+            for (FileObject subDir : dir.getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+            for (FileObject subDir : dir.getParent().getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static SuiteSources findSuiteSources(FileObject root, String name) throws IllegalArgumentException {
+        FileObject impDir = root.getFileObject(name);
+        if (impDir != null) {
+            try {
+                Project impPrj = ProjectManager.getDefault().findProject(impDir);
+                return impPrj == null ? null : impPrj.getLookup().lookup(SuiteSources.class);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceGroup[] getSourceGroups(String string) {
+        return groups();
+    }
+
+    Group[] groups() {
+        return groups.toArray(new Group[0]);
+    }
+
+    Group findGroup(FileObject fo) {
+        for (Group g : groups) {
+            if (g.contains(fo)) {
+                return g;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void addChangeListener(ChangeListener cl) {
+    }
+
+    @Override
+    public void removeChangeListener(ChangeListener cl) {
+    }
+
+    private static FileObject findPrjDir(FileObject dir, String prjName, MxProject prj) {
+        if (dir == null) {
+            return null;
+        }
+        if (prj.dir() != null) {
+            return dir.getFileObject(prj.dir());
+        }
+        if (prj.subDir() != null) {
+            dir = dir.getFileObject(prj.subDir());
+            if (dir == null) {
+                return null;
+            }
+        }
+        return dir.getFileObject(prjName);
+    }
+
+    private Collection<Dep> transitiveDeps(Dep current, Map<String, Dep> fill) {
+        current.owner().computeTransitiveDeps();
+        final Collection<Dep> currentAllDeps = current.allDeps();
+        if (currentAllDeps == Collections.<Dep>emptySet()) {
+            throw new IllegalStateException("Cyclic dep on " + current.getName());
+        } else if (currentAllDeps != null) {
+            return currentAllDeps;
+        }
+        current.setAllDeps(Collections.emptySet());
+        TreeSet<Dep> computing = new TreeSet<>();
+        computing.add(current);
+        for (String depName : current.depNames()) {
+            Dep dep = fill.get(depName);
+            if (dep == null) {
+                int colon = depName.lastIndexOf(':');
+                dep = fill.get(depName.substring(colon + 1));
+                if (dep == null) {
+                    LOG.log(Level.INFO, "dep not found: {0}", depName);
+                    continue;
+                }
+            }
+            Collection<Dep> allDeps = transitiveDeps(dep, fill);
+            computing.addAll(allDeps);
+        }
+        current.setAllDeps(computing);
+        return computing;
+    }
+
+    private static void registerLibs(Map<String, MxLibrary> collect, String prefix, Map<String, MxLibrary> libraries) {
+        for (Map.Entry<String, MxLibrary> entry : libraries.entrySet()) {
+            String key = entry.getKey();
+            MxLibrary lib = entry.getValue();
+            if (prefix == null) {
+                collect.put(key, lib);
+            } else {
+                collect.put(prefix + ":" + key, lib);
+            }
+        }
+    }
+
+    private void registerDeps(String prefix, Map<String, Dep> fillDeps) {
+        for (Library library : libraries) {
+            fillDeps.put(prefix + ":" + library.getName(), library);
+        }
+        for (Dist d : distributions) {
+            fillDeps.put(prefix + ":" + d.getName(), d);
+        }
+        for (Map.Entry<String, SuiteSources> s : imported.entrySet()) {
+            s.getValue().registerDeps(s.getKey(), fillDeps);
+        }
+    }
+
+    @Override
+    public Group findBinaryRoots2(URL url) {
+        final FileObject srcFo = URLMapper.findFileObject(url);
+        for (Group group : this.groups) {
+            if (group.contains(srcFo)) {
+                return group;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public URL[] computeRoots(Group group) {
+        if (group.binDir != null) {
+            return new URL[] { group.binDir.toURL() };
+        } else {
+            return new URL[0];
+        }
+    }
+
+    @Override
+    public boolean computePreferBinaries(Group result) {
+        return true;
+    }
+
+    @Override
+    public void computeChangeListener(Group result, boolean bln, ChangeListener cl) {
+    }
+
+    @Override
+    public SourceForBinaryQueryImplementation2.Result findSourceRoots2(URL url) {
+        this.computeTransitiveDeps();
+        for (Dist dist : this.distributions) {
+            URL jar;
+            try {
+                jar = dist.getJarRoot();
+                if (jar == null) {
+                    continue;
+                }
+            } catch (MalformedURLException ok) {
+                continue;
+            }
+            if (jar.equals(url)) {
+                List<FileObject> roots = new ArrayList<>();
+                for (Group d : dist.getContributingGroups()) {
+                    roots.add(d.srcDir);
+                    roots.add(d.srcGenDir);
+                }
+                return new ImmutableResult(roots.toArray(new FileObject[roots.size()]));
+            }
+        }
+        for (Group group : this.groups) {
+            if (group.binDir != null && group.binDir.toURL().equals(url)) {
+                return new ImmutableResult(group.srcDir, group.srcGenDir);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceForBinaryQuery.Result findSourceRoots(URL url) {
+        return findSourceRoots2(url);
+    }
+
+    final Iterable<File> jdks() {
+        Set<File> jdks = new LinkedHashSet<>();
+        String home = System.getProperty("user.home");
+        if (home != null) {
+            File userEnv = new File(new File(new File(home), ".mx"), "env");
+            findJdksInEnv(jdks, userEnv);
+        }
+        FileObject suiteEnv = dir.getFileObject("mx." + dir.getNameExt() + "/env");
+        if (suiteEnv != null) {
+            findJdksInEnv(jdks, FileUtil.toFile(suiteEnv));
+        }
+
+        String javaHomeEnv = System.getenv("JAVA_HOME");
+        if (javaHomeEnv != null) {
+            jdks.add(new File(javaHomeEnv));
+        }
+        String javaHomeProp = System.getProperty("java.home");
+        if (javaHomeProp != null) {
+            jdks.add(new File(javaHomeProp));
+        }
+        return jdks;
+    }
+
+    private void findJdksInEnv(Set<File> jdks, File env) {
+        if (env == null || !env.isFile()) {
+            return;
+        }
+        try (final FileInputStream is = new FileInputStream(env)) {
+            Properties p = new Properties();
+            p.load(is);
+
+            String javaHome = p.getProperty("JAVA_HOME");
+            if (javaHome != null) {
+                jdks.add(new File(javaHome));
+            }
+
+            String extraJavaHomes = p.getProperty("EXTRA_JAVA_HOMES");
+            if (extraJavaHomes != null) {
+                for (String extraHome : extraJavaHomes.split(File.pathSeparator)) {
+                    jdks.add(new File(extraHome));
+                }
+            }
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    @Override
+    public SourceLevelQueryImplementation2.Result getSourceLevel(FileObject fo) {
+        Group g = findGroup(fo);
+        if (g == null) {
+            return null;
+        }
+        return new SourceLevelQueryImplementation2.Result2() {
+            @Override
+            public SourceLevelQuery.Profile getProfile() {
+                return SourceLevelQuery.Profile.DEFAULT;
+            }
+
+            @Override
+            public String getSourceLevel() {
+                return g.getCompliance().getSourceLevel();
+            }
+
+            @Override
+            public void addChangeListener(ChangeListener cl) {
+            }
+
+            @Override
+            public void removeChangeListener(ChangeListener cl) {
+            }
+        };
+    }
+
+    @Override
+    public Set<? extends Project> getSubprojects() {
+        Set<Project> prjs = new HashSet<>();
+        for (SuiteSources imp : imported.values()) {
+            prjs.add(imp.prj);
+        }
+        return prjs;
+    }
+
+    @Override
+    public URL[] findUnitTests(FileObject fo) {
+        return new URL[0];
+    }
+
+    @Override
+    public URL[] findSources(FileObject fo) {
+        Group g = findGroup(fo);
+        return g == null ? new URL[0] : new URL[] { g.getRootFolder().toURL() };
+    }
+
+    static interface Dep extends Comparable<Dep> {
+        String getName();
+
+        Collection<String> depNames();
+
+        Collection<Dep> allDeps();
+
+        void setAllDeps(Collection<Dep> set);
+
+        @Override
+        public default int compareTo(Dep o) {
+            return getName().compareTo(o.getName());
+        }
+
+        SuiteSources owner();
+    }
+
+    final class Dist implements Dep, FlaggedClassPathImplementation {
+        final String name;
+        final MxDistribution dist;
+        Collection<Dep> allDeps;
+        private final PropertyChangeSupport support = new PropertyChangeSupport(this);
+        private Boolean exists;
+        private Collection<Group> groups;
+
+        public Dist(String name, MxDistribution dist) {
+            this.name = name;
+            this.dist = dist;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            Set<String> deps = new TreeSet<>();
+            deps.addAll(dist.distDependencies());
+            deps.addAll(dist.exclude());
+            return deps;
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return this.allDeps;
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            this.allDeps = set;
+        }
+
+        @Override
+        public String getName() {
+            return this.name;
+        }
+
+        @Override
+        public Set<ClassPath.Flag> getFlags() {
+            return exists ? Collections.emptySet() : Collections.singleton(ClassPath.Flag.INCOMPLETE);
+        }
+
+        private FileObject getJar(boolean ignore) {
+            if (SuiteSources.this.dir == null) {
+                return null;
+            }
+            FileObject dists = SuiteSources.this.dir.getFileObject("mxbuild/dists");
+            if (dists == null) {
+                return null;
+            }
+            List<FileObject> dist = Arrays.stream(dists.getChildren()).filter((fo) -> fo.isFolder() && fo.getName().startsWith("jdk")).collect(Collectors.toList());
+            dist.sort((fo1, fo2) -> fo2.getName().compareTo(fo1.getName()));
+            for (FileObject jdkDir : dist) {
+                FileObject jar = jdkDir.getFileObject(name.toLowerCase().replace("_", "-") + ".jar");
+                if (jar != null) {
+                    return jar;
+                }
+            }
+            FileObject jar = dists.getFileObject(name.toLowerCase().replace("_", "-") + ".jar");
+            if (jar != null) {
+                return jar;
+            }
+            return null;
+        }
+
+        @Override
+        public List<? extends PathResourceImplementation> getResources() {
+            computeTransitiveDeps();
+            FileObject jar = getJar(exists == null);
+            final boolean existsNow = jar != null && jar.isData();
+            if (exists == null) {
+                exists = existsNow;
+            } else {
+                if (exists != existsNow) {
+                    exists = existsNow;
+                    support.firePropertyChange(PROP_FLAGS, !exists, (boolean) exists);
+                }
+            }
+            if (jar != null) {
+                PathResourceImplementation res;
+                try {
+                    res = ClassPathSupport.createResource(getJarRoot());
+                    return Collections.singletonList(res);
+                } catch (MalformedURLException ex) {
+                    // OK
+                }
+            }
+            return Collections.emptyList();
+        }
+
+        private URL getJarRoot() throws MalformedURLException {
+            FileObject jar = getJar(true);
+            if (jar != null) {
+                return new URL("jar:" + jar.toURL() + "!/");
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener pl) {
+            support.addPropertyChangeListener(pl);
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener pl) {
+            support.removePropertyChangeListener(pl);
+        }
+
+        @Override
+        public SuiteSources owner() {
+            return SuiteSources.this;
+        }
+
+        private void computeSourceRoots(Map<String, Dep> collectedDeps) {
+            if (groups != null) {
+                return;
+            }
+            Set<Group> contributingGroups = new LinkedHashSet<>();
+            for (String d : this.dist.dependencies()) {
+                Dep dep = collectedDeps.get(d);
+                if (dep == null || dep.allDeps() == null) {
+                    continue;
+                }
+                for (Dep d2 : dep.allDeps()) {
+                    if (d2 instanceof Group) {
+                        contributingGroups.add((Group) d2);
+                    }
+                }
+            }
+            for (String d : this.dist.distDependencies()) {
+                final Dep anyDep = collectedDeps.get(d);
+                if (anyDep instanceof Dist) {
+                    Dist dep = (Dist) anyDep;
+                    dep.computeSourceRoots(collectedDeps);
+                    contributingGroups.removeAll(dep.getContributingGroups());
+                }
+            }
+            groups = contributingGroups;
+        }
+
+        public Collection<Group> getContributingGroups() {
+            return groups;
+        }
+
+        @Override
+        public String toString() {
+            return "Dist[name=" + name + "]";
+        }
+    }
+
+    final class Group implements SourceGroup, Dep, AnnotationProcessingQuery.Result,
+            Compliance.Provider {
+        private final String mxName;
+        private final MxProject mxPrj;
+        private final FileObject srcDir;
+        private final FileObject srcGenDir;
+        private final FileObject binDir;
+        private final String name;
+        private final String displayName;
+        private final Compliance compliance;
+        private ClassPath sourceCP;
+        private ClassPath cp;
+        private ClassPath processorPath;
+        private Collection<Dep> allDeps;
+
+        Group(String mxName, MxProject mxPrj, FileObject srcDir, FileObject srcGenDir, FileObject binDir, String name, String displayName) {
+            this.mxName = mxName;
+            this.mxPrj = mxPrj;
+            this.srcDir = srcDir;
+            this.srcGenDir = srcGenDir;
+            this.binDir = binDir;
+            this.name = name;
+            this.displayName = displayName;
+            this.compliance = Compliance.parse(mxPrj.javaCompliance());
+        }
+
+        @Override
+        public FileObject getRootFolder() {
+            return srcDir;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getDisplayName() {
+            return displayName;
+        }
+
+        @Override
+        public Icon getIcon(boolean opened) {
+            return null;
+        }
+
+        @Override
+        public Compliance getCompliance() {
+            return compliance;
+        }
+
+        @Override
+        public boolean contains(FileObject file) {
+            if (file == srcDir || file == srcGenDir || FileUtil.isParentOf(srcDir, file) || (srcGenDir != null && FileUtil.isParentOf(srcGenDir, file))) {
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener l) {
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener l) {
+        }
+
+        @Override
+        public String toString() {
+            return "SuiteSources.Group[name=" + name + ",rootFolder=" + srcDir + "]"; // NOI18N
+        }
+
+        ClassPath getSourceCP() {
+            computeTransitiveDeps();
+            return sourceCP;
+        }
+
+        ClassPath getCP() {
+            computeTransitiveDeps();
+            return cp;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            return mxPrj.dependencies();
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            allDeps = set;
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return allDeps;
+        }
+
+        private void computeClassPath(Map<String, Dep> transDeps) {
+            for (Dep d : transDeps.values()) {
+                d.owner().computeTransitiveDeps();
+            }
+
+            List<Group> arr = new ArrayList<>();
+            List<ClassPathImplementation> libs = new ArrayList<>();
+            processTransDep(transDeps.get(mxName), arr, libs);
+            cp = composeClassPath(arr, libs);
+            List<FileObject> roots = new ArrayList<>();
+            if (srcDir != null) {
+                roots.add(srcDir);
+            }
+            if (srcGenDir != null) {
+                roots.add(srcGenDir);
+            }
+            sourceCP = ClassPathSupport.createClassPath(roots.toArray(new FileObject[roots.size()]));
+
+            if (mxPrj.annotationProcessors().isEmpty()) {
+                processorPath = null;
+            } else {
+                List<Group> groups = new ArrayList<>();
+                List<ClassPathImplementation> jars = new ArrayList<>();
+                for (String dep : mxPrj.annotationProcessors()) {
+                    processTransDep(transDeps.get(dep), groups, jars);
+                }
+                processorPath = composeClassPath(groups, jars);
+            }
+        }
+
+        private void processTransDep(Dep dep, List<Group> addGroups, List<ClassPathImplementation> addJars) {
+            if (dep != null) {
+                dep.owner().computeTransitiveDeps();
+                for (Dep d : dep.allDeps()) {
+                    if (d == this) {
+                        continue;
+                    }
+                    d.owner().computeTransitiveDeps();
+                    if (d instanceof Group) {
+                        addGroups.add((Group) d);
+                    } else if (d instanceof ClassPathImplementation) {
+                        addJars.add((ClassPathImplementation) d);
+                    }
+                }
+            }
+        }
+
+        private ClassPath composeClassPath(List<Group> arr, List<ClassPathImplementation> libs) {
+            Set<FileObject> roots = new LinkedHashSet<>();
+            final int depsCount = arr.size();
+            for (int i = 0; i < depsCount; i++) {
+                final Group g = arr.get(i);
+                if (g.binDir != null) {
+                    roots.add(g.binDir);
+                }
+            }
+            ClassPath prjCp = ClassPathSupport.createClassPath(roots.toArray(new FileObject[0]));
+            if (!libs.isEmpty()) {
+                if (libs.size() == 1) {
+                    prjCp = ClassPathSupport.createProxyClassPath(prjCp,
+                                                                  ClassPathFactory.createClassPath(libs.get(0))
+                    );
+                } else {
+                    prjCp = ClassPathSupport.createProxyClassPath(prjCp,
+                                                                  ClassPathFactory.createClassPath(
+                                                                                  ClassPathSupport.createProxyClassPathImplementation(
+                                                                                                  libs.toArray(new ClassPathImplementation[0])
+                                                                                  )
+                                                                  )
+                    );
+                }
+            }
+            return prjCp;
+        }
+
+        ClassPath getProcessorCP() {
+            computeTransitiveDeps();
+            return processorPath;
+        }
+
+        @Override
+        public Set<? extends AnnotationProcessingQuery.Trigger> annotationProcessingEnabled() {
+            return EnumSet.of(AnnotationProcessingQuery.Trigger.ON_SCAN, AnnotationProcessingQuery.Trigger.IN_EDITOR);
+        }
+
+        @Override
+        public Iterable<? extends String> annotationProcessorsToRun() {
+            return null;
+        }
+
+        @Override
+        public URL sourceOutputDirectory() {
+            return srcGenDir == null ? null : srcGenDir.toURL();
+        }
+
+        @Override
+        public Map<? extends String, ? extends String> processorOptions() {
+            return Collections.emptyMap();
+        }
+
+        @Override
+        public void addChangeListener(ChangeListener l) {
+        }
+
+        @Override
+        public void removeChangeListener(ChangeListener l) {
+        }
+
+        @Override
+        public SuiteSources owner() {
+            return SuiteSources.this;
+        }
+    }
+
+    private class Library implements FlaggedClassPathImplementation, Dep {
+        final MxLibrary lib;
+        final PropertyChangeSupport support = new PropertyChangeSupport(this);
+        final String libName;
+        Collection<Dep> allDeps;
+        Boolean exists;
+
+        Library(String libName, MxLibrary lib) {
+            this.libName = libName;
+            this.lib = getOSSLibrary(lib);
+        }
+
+        final MxLibrary getOSSLibrary(MxLibrary lib) {
+            if (lib.sha1() == null && !lib.os_arch().isEmpty()) {
+                Map<String, MxLibrary.Arch> os_dep_libs = lib.os_arch();
+                String os = System.getProperty("os.name").toLowerCase();
+                for (Map.Entry<String, MxLibrary.Arch> entry : os_dep_libs.entrySet()) {
+                    if (os.contains(entry.getKey())) {
+                        return entry.getValue().amd64();
+                    }
+                }
+            }
+            return lib;
+        }
+
+        File getJar(boolean dumpIfMissing) {
+            File mxCache;
+            String cache = System.getenv("MX_CACHE_DIR");
+            if (cache != null) {
+                mxCache = new File(cache);
+            } else {
+                mxCache = new File(new File(new File(System.getProperty("user.home")), ".mx"), "cache");
+            }
+            int prefix = libName.indexOf(':');
+            final String simpleName = libName.substring(prefix + 1);
+
+            File simpleJar = new File(mxCache, simpleName + "_" + lib.sha1() + ".jar");
+            if (simpleJar.exists()) {
+                return simpleJar;
+            }
+            File dir = new File(mxCache, simpleName + "_" + lib.sha1());
+            File jar = new File(dir, simpleName.replace('_', '-').toLowerCase(Locale.ENGLISH) + ".jar");
+
+            if (dumpIfMissing && !jar.exists()) {
+                for (File f = jar;; f = f.getParentFile()) {
+                    if (!f.exists()) {
+                        LOG.log(Level.WARNING, "{0} does not exist", f);
+                    } else {
+                        StringBuilder sb = new StringBuilder();
+                        sb.append(f).append(" exists:\n");
+                        String[] kids = f.list();
+                        if (kids != null) {
+                            for (String n : kids) {
+                                sb.append("  ").append(n).append("\n");
+                            }
+                        }
+                        LOG.log(Level.INFO, sb.toString());
+                        break;
+                    }
+                }
+            }
+            return jar;
+        }
+
+        @Override
+        public String getName() {
+            return libName;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            return lib.dependencies();
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return allDeps;
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            this.allDeps = set;
+        }
+
+        @Override
+        public Set<ClassPath.Flag> getFlags() {
+            return exists ? Collections.emptySet() : Collections.singleton(ClassPath.Flag.INCOMPLETE);

Review comment:
       better check exists != null as well

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteSources.java
##########
@@ -0,0 +1,1195 @@
+/*
+ * 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.mx.project;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.Icon;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.java.mx.project.suitepy.MxDistribution;
+import org.netbeans.modules.java.mx.project.suitepy.MxImports;
+import org.netbeans.modules.java.mx.project.suitepy.MxLibrary;
+import org.netbeans.modules.java.mx.project.suitepy.MxProject;
+import org.netbeans.modules.java.mx.project.suitepy.MxSuite;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery;
+import org.netbeans.api.java.queries.SourceForBinaryQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+import org.netbeans.spi.java.classpath.ClassPathFactory;
+import org.netbeans.spi.java.classpath.ClassPathImplementation;
+import org.netbeans.spi.java.classpath.FlaggedClassPathImplementation;
+import org.netbeans.spi.java.classpath.PathResourceImplementation;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation2;
+import org.netbeans.spi.java.queries.SourceForBinaryQueryImplementation2;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
+import org.openide.util.Exceptions;
+import org.openide.util.Utilities;
+import java.util.stream.Collectors;
+import org.netbeans.api.java.queries.SourceLevelQuery;
+import org.netbeans.spi.java.queries.MultipleRootsUnitTestForSourceQueryImplementation;
+import org.netbeans.spi.java.queries.SourceLevelQueryImplementation2;
+import org.netbeans.spi.project.SubprojectProvider;
+
+final class SuiteSources implements Sources,
+                BinaryForSourceQueryImplementation2<SuiteSources.Group>, SourceForBinaryQueryImplementation2,
+                SourceLevelQueryImplementation2, SubprojectProvider, MultipleRootsUnitTestForSourceQueryImplementation {
+    private static final Logger LOG = Logger.getLogger(SuiteSources.class.getName());
+    private static final SuiteSources CORE;
+
+    static {
+        MxSuite coreSuite = CoreSuite.CORE_5_279_0;
+        CORE = new SuiteSources(null, null, coreSuite);
+    }
+
+    private final MxSuite suite;
+    private final List<Group> groups;
+    private final List<Library> libraries;
+    private final List<Dist> distributions;
+    private final FileObject dir;
+    /**
+     * non-null if the dependencies haven't yet been properly initialized
+     */
+    private Map<String, Dep> transitiveDeps;
+    /**
+     * avoid GC of imported projects
+     */
+    private final SuiteProject prj;
+    private final Map<String, SuiteSources> imported;
+
+    SuiteSources(SuiteProject owner, FileObject dir, MxSuite suite) {
+        final Map<String, Dep> fillDeps = new HashMap<>();
+        this.prj = owner;
+        this.dir = dir;
+        this.groups = findGroups(fillDeps, suite, dir);
+        this.libraries = findLibraries(fillDeps, suite);
+        this.imported = findImportedSuites(dir, suite, fillDeps);
+        this.distributions = findDistributions(suite, this.libraries, this.groups, fillDeps);
+        this.suite = suite;
+        this.transitiveDeps = fillDeps;
+    }
+
+    @Override
+    public String toString() {
+        return "MxSources[" + (dir == null ? "mx" : dir.toURI()) + "]";
+    }
+
+    private List<Group> findGroups(Map<String, Dep> fillDeps, MxSuite s, FileObject dir) {
+        List<Group> arr = new ArrayList<>();
+        for (Map.Entry<String, MxProject> entry : s.projects().entrySet()) {
+            String name = entry.getKey();
+            MxProject mxPrj = entry.getValue();
+            FileObject prjDir = findPrjDir(dir, name, mxPrj);
+            if (prjDir == null) {
+                fillDeps.put(name, new Group(name, mxPrj, null, null, null, name, name));
+                continue;
+            }
+            String prevName = null;
+            Group firstGroup = null;
+            String binPrefix;
+            if (mxPrj.subDir() == null) {
+                binPrefix = "mxbuild/";
+            } else {
+                binPrefix = "mxbuild/" + mxPrj.subDir() + "/";
+            }
+            for (String rel : mxPrj.sourceDirs()) {
+                FileObject srcDir = prjDir.getFileObject(rel);
+                FileObject binDir = getSubDir(dir, binPrefix + name + "/bin");
+                FileObject srcGenDir = getSubDir(dir, binPrefix + name + "/src_gen");
+                if (srcDir != null && binDir != null) {
+                    String prgName = name + "-" + rel;
+                    String displayName;
+                    if (prevName == null) {
+                        displayName = name;
+                    } else {
+                        displayName = name + "[" + rel + "]";
+                    }
+                    Group g = new Group(name, mxPrj, srcDir, srcGenDir, binDir, prgName, displayName);
+                    arr.add(g);
+                    if (firstGroup == null) {
+                        firstGroup = g;
+                    }
+                    prevName = displayName;
+                }
+            }
+            if (firstGroup != null) {
+                fillDeps.put(name, firstGroup);

Review comment:
       Silly question, but why just the 1st group ?

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteSources.java
##########
@@ -0,0 +1,1195 @@
+/*
+ * 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.mx.project;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.Icon;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.java.mx.project.suitepy.MxDistribution;
+import org.netbeans.modules.java.mx.project.suitepy.MxImports;
+import org.netbeans.modules.java.mx.project.suitepy.MxLibrary;
+import org.netbeans.modules.java.mx.project.suitepy.MxProject;
+import org.netbeans.modules.java.mx.project.suitepy.MxSuite;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery;
+import org.netbeans.api.java.queries.SourceForBinaryQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+import org.netbeans.spi.java.classpath.ClassPathFactory;
+import org.netbeans.spi.java.classpath.ClassPathImplementation;
+import org.netbeans.spi.java.classpath.FlaggedClassPathImplementation;
+import org.netbeans.spi.java.classpath.PathResourceImplementation;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation2;
+import org.netbeans.spi.java.queries.SourceForBinaryQueryImplementation2;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
+import org.openide.util.Exceptions;
+import org.openide.util.Utilities;
+import java.util.stream.Collectors;
+import org.netbeans.api.java.queries.SourceLevelQuery;
+import org.netbeans.spi.java.queries.MultipleRootsUnitTestForSourceQueryImplementation;
+import org.netbeans.spi.java.queries.SourceLevelQueryImplementation2;
+import org.netbeans.spi.project.SubprojectProvider;
+
+final class SuiteSources implements Sources,
+                BinaryForSourceQueryImplementation2<SuiteSources.Group>, SourceForBinaryQueryImplementation2,
+                SourceLevelQueryImplementation2, SubprojectProvider, MultipleRootsUnitTestForSourceQueryImplementation {
+    private static final Logger LOG = Logger.getLogger(SuiteSources.class.getName());
+    private static final SuiteSources CORE;
+
+    static {
+        MxSuite coreSuite = CoreSuite.CORE_5_279_0;
+        CORE = new SuiteSources(null, null, coreSuite);
+    }
+
+    private final MxSuite suite;
+    private final List<Group> groups;
+    private final List<Library> libraries;
+    private final List<Dist> distributions;
+    private final FileObject dir;
+    /**
+     * non-null if the dependencies haven't yet been properly initialized
+     */
+    private Map<String, Dep> transitiveDeps;
+    /**
+     * avoid GC of imported projects
+     */
+    private final SuiteProject prj;
+    private final Map<String, SuiteSources> imported;
+
+    SuiteSources(SuiteProject owner, FileObject dir, MxSuite suite) {
+        final Map<String, Dep> fillDeps = new HashMap<>();
+        this.prj = owner;
+        this.dir = dir;
+        this.groups = findGroups(fillDeps, suite, dir);
+        this.libraries = findLibraries(fillDeps, suite);
+        this.imported = findImportedSuites(dir, suite, fillDeps);
+        this.distributions = findDistributions(suite, this.libraries, this.groups, fillDeps);
+        this.suite = suite;
+        this.transitiveDeps = fillDeps;
+    }
+
+    @Override
+    public String toString() {
+        return "MxSources[" + (dir == null ? "mx" : dir.toURI()) + "]";
+    }
+
+    private List<Group> findGroups(Map<String, Dep> fillDeps, MxSuite s, FileObject dir) {
+        List<Group> arr = new ArrayList<>();
+        for (Map.Entry<String, MxProject> entry : s.projects().entrySet()) {
+            String name = entry.getKey();
+            MxProject mxPrj = entry.getValue();
+            FileObject prjDir = findPrjDir(dir, name, mxPrj);
+            if (prjDir == null) {
+                fillDeps.put(name, new Group(name, mxPrj, null, null, null, name, name));
+                continue;
+            }
+            String prevName = null;
+            Group firstGroup = null;
+            String binPrefix;
+            if (mxPrj.subDir() == null) {
+                binPrefix = "mxbuild/";
+            } else {
+                binPrefix = "mxbuild/" + mxPrj.subDir() + "/";
+            }
+            for (String rel : mxPrj.sourceDirs()) {
+                FileObject srcDir = prjDir.getFileObject(rel);
+                FileObject binDir = getSubDir(dir, binPrefix + name + "/bin");
+                FileObject srcGenDir = getSubDir(dir, binPrefix + name + "/src_gen");
+                if (srcDir != null && binDir != null) {
+                    String prgName = name + "-" + rel;
+                    String displayName;
+                    if (prevName == null) {
+                        displayName = name;
+                    } else {
+                        displayName = name + "[" + rel + "]";
+                    }
+                    Group g = new Group(name, mxPrj, srcDir, srcGenDir, binDir, prgName, displayName);
+                    arr.add(g);
+                    if (firstGroup == null) {
+                        firstGroup = g;
+                    }
+                    prevName = displayName;
+                }
+            }
+            if (firstGroup != null) {
+                fillDeps.put(name, firstGroup);
+            }
+        }
+        return arr;
+    }
+
+    private static FileObject getSubDir(FileObject dir, String relPath) {
+        FileObject subDir = dir.getFileObject(relPath);
+        if (subDir == null) {
+            try {
+                subDir = FileUtil.createFolder(dir, relPath);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return subDir;
+    }
+
+    private List<Library> findLibraries(Map<String, Dep> fillDeps, MxSuite suite) {
+        final Map<String, MxLibrary> allLibraries = new HashMap<>();
+        registerLibs(allLibraries, null, suite.libraries());
+
+        List<Library> arr = new ArrayList<>();
+        for (Map.Entry<String, MxLibrary> entry : allLibraries.entrySet()) {
+            final Library library = new Library(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        for (Map.Entry<String, MxLibrary> entry : suite.jdklibraries().entrySet()) {
+            final JdkLibrary library = new JdkLibrary(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        return arr;
+    }
+
+    private static Map<String, SuiteSources> findImportedSuites(FileObject dir, MxSuite s, Map<String, Dep> fillDeps) {
+        if (dir == null) {
+            return Collections.emptyMap();
+        }
+        CORE.registerDeps("mx", fillDeps);
+        final MxImports imports = s.imports();
+        if (imports != null) {
+            Map<String, SuiteSources> imported = new LinkedHashMap<>();
+            for (MxImports.Suite imp : imports.suites()) {
+                SuiteSources impSources = findSuiteSources(dir, imp);
+                final String suiteName = imp.name();
+                if (impSources == null) {
+                    LOG.log(Level.INFO, "cannot find imported suite: {0}", suiteName);
+                    continue;
+                }
+                imported.put(suiteName, impSources);
+                impSources.registerDeps(suiteName, fillDeps);
+            }
+            return imported;
+        }
+        return Collections.emptyMap();
+    }
+
+    private List<Dist> findDistributions(MxSuite s, List<Library> libraries, List<Group> groups, Map<String, Dep> fillDeps) {
+        List<Dist> dists = new ArrayList<>();
+        for (Map.Entry<String, MxDistribution> entry : s.distributions().entrySet()) {
+            Dist d = new Dist(entry.getKey(), entry.getValue());
+            dists.add(d);
+            fillDeps.put(d.getName(), d);
+        }
+        return dists;
+    }
+
+    final synchronized void computeTransitiveDeps() {
+        Map<String, Dep> collectedDeps = this.transitiveDeps;
+        if (collectedDeps == null) {
+            return;
+        }
+        this.transitiveDeps = null;
+        for (Library l : this.libraries) {
+            transitiveDeps(l, collectedDeps);
+        }
+        for (Group g : this.groups) {
+            transitiveDeps(g, collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            transitiveDeps(d, collectedDeps);
+        }
+        for (Group g : groups) {
+            g.computeClassPath(collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            d.computeSourceRoots(collectedDeps);
+        }
+    }
+
+    private static SuiteSources findSuiteSources(FileObject dir, MxImports.Suite imp) throws IllegalArgumentException {
+        SuiteSources sources = findSuiteSources(dir.getParent(), imp.name());
+        if (sources != null) {
+            return sources;
+        }
+        if (imp.subdir()) {
+            for (FileObject subDir : dir.getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+            for (FileObject subDir : dir.getParent().getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static SuiteSources findSuiteSources(FileObject root, String name) throws IllegalArgumentException {
+        FileObject impDir = root.getFileObject(name);
+        if (impDir != null) {
+            try {
+                Project impPrj = ProjectManager.getDefault().findProject(impDir);
+                return impPrj == null ? null : impPrj.getLookup().lookup(SuiteSources.class);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceGroup[] getSourceGroups(String string) {
+        return groups();
+    }
+
+    Group[] groups() {
+        return groups.toArray(new Group[0]);
+    }
+
+    Group findGroup(FileObject fo) {
+        for (Group g : groups) {
+            if (g.contains(fo)) {
+                return g;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void addChangeListener(ChangeListener cl) {
+    }
+
+    @Override
+    public void removeChangeListener(ChangeListener cl) {
+    }
+
+    private static FileObject findPrjDir(FileObject dir, String prjName, MxProject prj) {
+        if (dir == null) {
+            return null;
+        }
+        if (prj.dir() != null) {
+            return dir.getFileObject(prj.dir());
+        }
+        if (prj.subDir() != null) {
+            dir = dir.getFileObject(prj.subDir());
+            if (dir == null) {
+                return null;
+            }
+        }
+        return dir.getFileObject(prjName);
+    }
+
+    private Collection<Dep> transitiveDeps(Dep current, Map<String, Dep> fill) {
+        current.owner().computeTransitiveDeps();
+        final Collection<Dep> currentAllDeps = current.allDeps();
+        if (currentAllDeps == Collections.<Dep>emptySet()) {
+            throw new IllegalStateException("Cyclic dep on " + current.getName());
+        } else if (currentAllDeps != null) {
+            return currentAllDeps;
+        }
+        current.setAllDeps(Collections.emptySet());
+        TreeSet<Dep> computing = new TreeSet<>();
+        computing.add(current);
+        for (String depName : current.depNames()) {
+            Dep dep = fill.get(depName);
+            if (dep == null) {
+                int colon = depName.lastIndexOf(':');
+                dep = fill.get(depName.substring(colon + 1));
+                if (dep == null) {
+                    LOG.log(Level.INFO, "dep not found: {0}", depName);
+                    continue;
+                }
+            }
+            Collection<Dep> allDeps = transitiveDeps(dep, fill);
+            computing.addAll(allDeps);
+        }
+        current.setAllDeps(computing);
+        return computing;
+    }
+
+    private static void registerLibs(Map<String, MxLibrary> collect, String prefix, Map<String, MxLibrary> libraries) {
+        for (Map.Entry<String, MxLibrary> entry : libraries.entrySet()) {
+            String key = entry.getKey();
+            MxLibrary lib = entry.getValue();
+            if (prefix == null) {
+                collect.put(key, lib);
+            } else {
+                collect.put(prefix + ":" + key, lib);
+            }
+        }
+    }
+
+    private void registerDeps(String prefix, Map<String, Dep> fillDeps) {
+        for (Library library : libraries) {
+            fillDeps.put(prefix + ":" + library.getName(), library);
+        }
+        for (Dist d : distributions) {
+            fillDeps.put(prefix + ":" + d.getName(), d);
+        }
+        for (Map.Entry<String, SuiteSources> s : imported.entrySet()) {
+            s.getValue().registerDeps(s.getKey(), fillDeps);
+        }
+    }
+
+    @Override
+    public Group findBinaryRoots2(URL url) {
+        final FileObject srcFo = URLMapper.findFileObject(url);
+        for (Group group : this.groups) {
+            if (group.contains(srcFo)) {
+                return group;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public URL[] computeRoots(Group group) {
+        if (group.binDir != null) {
+            return new URL[] { group.binDir.toURL() };
+        } else {
+            return new URL[0];
+        }
+    }
+
+    @Override
+    public boolean computePreferBinaries(Group result) {
+        return true;
+    }
+
+    @Override
+    public void computeChangeListener(Group result, boolean bln, ChangeListener cl) {
+    }
+
+    @Override
+    public SourceForBinaryQueryImplementation2.Result findSourceRoots2(URL url) {
+        this.computeTransitiveDeps();
+        for (Dist dist : this.distributions) {
+            URL jar;
+            try {
+                jar = dist.getJarRoot();
+                if (jar == null) {
+                    continue;
+                }
+            } catch (MalformedURLException ok) {
+                continue;
+            }
+            if (jar.equals(url)) {
+                List<FileObject> roots = new ArrayList<>();
+                for (Group d : dist.getContributingGroups()) {
+                    roots.add(d.srcDir);
+                    roots.add(d.srcGenDir);
+                }
+                return new ImmutableResult(roots.toArray(new FileObject[roots.size()]));
+            }
+        }
+        for (Group group : this.groups) {
+            if (group.binDir != null && group.binDir.toURL().equals(url)) {
+                return new ImmutableResult(group.srcDir, group.srcGenDir);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceForBinaryQuery.Result findSourceRoots(URL url) {
+        return findSourceRoots2(url);
+    }
+
+    final Iterable<File> jdks() {
+        Set<File> jdks = new LinkedHashSet<>();
+        String home = System.getProperty("user.home");
+        if (home != null) {
+            File userEnv = new File(new File(new File(home), ".mx"), "env");
+            findJdksInEnv(jdks, userEnv);
+        }
+        FileObject suiteEnv = dir.getFileObject("mx." + dir.getNameExt() + "/env");
+        if (suiteEnv != null) {
+            findJdksInEnv(jdks, FileUtil.toFile(suiteEnv));
+        }
+
+        String javaHomeEnv = System.getenv("JAVA_HOME");
+        if (javaHomeEnv != null) {
+            jdks.add(new File(javaHomeEnv));
+        }
+        String javaHomeProp = System.getProperty("java.home");
+        if (javaHomeProp != null) {
+            jdks.add(new File(javaHomeProp));
+        }
+        return jdks;
+    }
+
+    private void findJdksInEnv(Set<File> jdks, File env) {
+        if (env == null || !env.isFile()) {
+            return;
+        }
+        try (final FileInputStream is = new FileInputStream(env)) {
+            Properties p = new Properties();
+            p.load(is);
+
+            String javaHome = p.getProperty("JAVA_HOME");
+            if (javaHome != null) {
+                jdks.add(new File(javaHome));
+            }
+
+            String extraJavaHomes = p.getProperty("EXTRA_JAVA_HOMES");
+            if (extraJavaHomes != null) {
+                for (String extraHome : extraJavaHomes.split(File.pathSeparator)) {
+                    jdks.add(new File(extraHome));
+                }
+            }
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    @Override
+    public SourceLevelQueryImplementation2.Result getSourceLevel(FileObject fo) {
+        Group g = findGroup(fo);
+        if (g == null) {
+            return null;
+        }
+        return new SourceLevelQueryImplementation2.Result2() {
+            @Override
+            public SourceLevelQuery.Profile getProfile() {
+                return SourceLevelQuery.Profile.DEFAULT;
+            }
+
+            @Override
+            public String getSourceLevel() {
+                return g.getCompliance().getSourceLevel();
+            }
+
+            @Override
+            public void addChangeListener(ChangeListener cl) {
+            }
+
+            @Override
+            public void removeChangeListener(ChangeListener cl) {
+            }
+        };
+    }
+
+    @Override
+    public Set<? extends Project> getSubprojects() {
+        Set<Project> prjs = new HashSet<>();
+        for (SuiteSources imp : imported.values()) {
+            prjs.add(imp.prj);
+        }
+        return prjs;
+    }
+
+    @Override
+    public URL[] findUnitTests(FileObject fo) {
+        return new URL[0];
+    }
+
+    @Override
+    public URL[] findSources(FileObject fo) {
+        Group g = findGroup(fo);
+        return g == null ? new URL[0] : new URL[] { g.getRootFolder().toURL() };
+    }
+
+    static interface Dep extends Comparable<Dep> {
+        String getName();
+
+        Collection<String> depNames();
+
+        Collection<Dep> allDeps();
+
+        void setAllDeps(Collection<Dep> set);
+
+        @Override
+        public default int compareTo(Dep o) {
+            return getName().compareTo(o.getName());
+        }
+
+        SuiteSources owner();
+    }
+
+    final class Dist implements Dep, FlaggedClassPathImplementation {
+        final String name;
+        final MxDistribution dist;
+        Collection<Dep> allDeps;
+        private final PropertyChangeSupport support = new PropertyChangeSupport(this);
+        private Boolean exists;
+        private Collection<Group> groups;
+
+        public Dist(String name, MxDistribution dist) {
+            this.name = name;
+            this.dist = dist;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            Set<String> deps = new TreeSet<>();
+            deps.addAll(dist.distDependencies());
+            deps.addAll(dist.exclude());
+            return deps;
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return this.allDeps;
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            this.allDeps = set;
+        }
+
+        @Override
+        public String getName() {
+            return this.name;
+        }
+
+        @Override
+        public Set<ClassPath.Flag> getFlags() {
+            return exists ? Collections.emptySet() : Collections.singleton(ClassPath.Flag.INCOMPLETE);
+        }
+
+        private FileObject getJar(boolean ignore) {
+            if (SuiteSources.this.dir == null) {
+                return null;
+            }
+            FileObject dists = SuiteSources.this.dir.getFileObject("mxbuild/dists");
+            if (dists == null) {
+                return null;
+            }
+            List<FileObject> dist = Arrays.stream(dists.getChildren()).filter((fo) -> fo.isFolder() && fo.getName().startsWith("jdk")).collect(Collectors.toList());
+            dist.sort((fo1, fo2) -> fo2.getName().compareTo(fo1.getName()));
+            for (FileObject jdkDir : dist) {
+                FileObject jar = jdkDir.getFileObject(name.toLowerCase().replace("_", "-") + ".jar");
+                if (jar != null) {
+                    return jar;
+                }
+            }
+            FileObject jar = dists.getFileObject(name.toLowerCase().replace("_", "-") + ".jar");
+            if (jar != null) {
+                return jar;
+            }
+            return null;
+        }
+
+        @Override
+        public List<? extends PathResourceImplementation> getResources() {
+            computeTransitiveDeps();
+            FileObject jar = getJar(exists == null);
+            final boolean existsNow = jar != null && jar.isData();
+            if (exists == null) {
+                exists = existsNow;
+            } else {
+                if (exists != existsNow) {
+                    exists = existsNow;
+                    support.firePropertyChange(PROP_FLAGS, !exists, (boolean) exists);
+                }
+            }
+            if (jar != null) {
+                PathResourceImplementation res;
+                try {
+                    res = ClassPathSupport.createResource(getJarRoot());
+                    return Collections.singletonList(res);
+                } catch (MalformedURLException ex) {
+                    // OK
+                }
+            }
+            return Collections.emptyList();
+        }
+
+        private URL getJarRoot() throws MalformedURLException {
+            FileObject jar = getJar(true);
+            if (jar != null) {
+                return new URL("jar:" + jar.toURL() + "!/");
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener pl) {
+            support.addPropertyChangeListener(pl);
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener pl) {
+            support.removePropertyChangeListener(pl);
+        }
+
+        @Override
+        public SuiteSources owner() {
+            return SuiteSources.this;
+        }
+
+        private void computeSourceRoots(Map<String, Dep> collectedDeps) {
+            if (groups != null) {
+                return;
+            }
+            Set<Group> contributingGroups = new LinkedHashSet<>();
+            for (String d : this.dist.dependencies()) {
+                Dep dep = collectedDeps.get(d);
+                if (dep == null || dep.allDeps() == null) {
+                    continue;
+                }
+                for (Dep d2 : dep.allDeps()) {
+                    if (d2 instanceof Group) {
+                        contributingGroups.add((Group) d2);
+                    }
+                }
+            }
+            for (String d : this.dist.distDependencies()) {
+                final Dep anyDep = collectedDeps.get(d);
+                if (anyDep instanceof Dist) {
+                    Dist dep = (Dist) anyDep;
+                    dep.computeSourceRoots(collectedDeps);
+                    contributingGroups.removeAll(dep.getContributingGroups());
+                }
+            }
+            groups = contributingGroups;
+        }
+
+        public Collection<Group> getContributingGroups() {
+            return groups;
+        }
+
+        @Override
+        public String toString() {
+            return "Dist[name=" + name + "]";
+        }
+    }
+
+    final class Group implements SourceGroup, Dep, AnnotationProcessingQuery.Result,
+            Compliance.Provider {
+        private final String mxName;
+        private final MxProject mxPrj;
+        private final FileObject srcDir;
+        private final FileObject srcGenDir;
+        private final FileObject binDir;
+        private final String name;
+        private final String displayName;
+        private final Compliance compliance;
+        private ClassPath sourceCP;
+        private ClassPath cp;
+        private ClassPath processorPath;
+        private Collection<Dep> allDeps;
+
+        Group(String mxName, MxProject mxPrj, FileObject srcDir, FileObject srcGenDir, FileObject binDir, String name, String displayName) {
+            this.mxName = mxName;
+            this.mxPrj = mxPrj;
+            this.srcDir = srcDir;
+            this.srcGenDir = srcGenDir;
+            this.binDir = binDir;
+            this.name = name;
+            this.displayName = displayName;
+            this.compliance = Compliance.parse(mxPrj.javaCompliance());
+        }
+
+        @Override
+        public FileObject getRootFolder() {
+            return srcDir;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getDisplayName() {
+            return displayName;
+        }
+
+        @Override
+        public Icon getIcon(boolean opened) {
+            return null;
+        }
+
+        @Override
+        public Compliance getCompliance() {
+            return compliance;
+        }
+
+        @Override
+        public boolean contains(FileObject file) {
+            if (file == srcDir || file == srcGenDir || FileUtil.isParentOf(srcDir, file) || (srcGenDir != null && FileUtil.isParentOf(srcGenDir, file))) {
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener l) {
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener l) {
+        }
+
+        @Override
+        public String toString() {
+            return "SuiteSources.Group[name=" + name + ",rootFolder=" + srcDir + "]"; // NOI18N
+        }
+
+        ClassPath getSourceCP() {
+            computeTransitiveDeps();
+            return sourceCP;
+        }
+
+        ClassPath getCP() {
+            computeTransitiveDeps();
+            return cp;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            return mxPrj.dependencies();
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            allDeps = set;
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return allDeps;
+        }
+
+        private void computeClassPath(Map<String, Dep> transDeps) {
+            for (Dep d : transDeps.values()) {
+                d.owner().computeTransitiveDeps();

Review comment:
       `processTransDeps` computes transitive deps on `d.owner()`, isn't it computed 2nd time here for all deps not just those used in classpath construction ?

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteSources.java
##########
@@ -0,0 +1,1195 @@
+/*
+ * 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.mx.project;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.Icon;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.java.mx.project.suitepy.MxDistribution;
+import org.netbeans.modules.java.mx.project.suitepy.MxImports;
+import org.netbeans.modules.java.mx.project.suitepy.MxLibrary;
+import org.netbeans.modules.java.mx.project.suitepy.MxProject;
+import org.netbeans.modules.java.mx.project.suitepy.MxSuite;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery;
+import org.netbeans.api.java.queries.SourceForBinaryQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+import org.netbeans.spi.java.classpath.ClassPathFactory;
+import org.netbeans.spi.java.classpath.ClassPathImplementation;
+import org.netbeans.spi.java.classpath.FlaggedClassPathImplementation;
+import org.netbeans.spi.java.classpath.PathResourceImplementation;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation2;
+import org.netbeans.spi.java.queries.SourceForBinaryQueryImplementation2;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
+import org.openide.util.Exceptions;
+import org.openide.util.Utilities;
+import java.util.stream.Collectors;
+import org.netbeans.api.java.queries.SourceLevelQuery;
+import org.netbeans.spi.java.queries.MultipleRootsUnitTestForSourceQueryImplementation;
+import org.netbeans.spi.java.queries.SourceLevelQueryImplementation2;
+import org.netbeans.spi.project.SubprojectProvider;
+
+final class SuiteSources implements Sources,
+                BinaryForSourceQueryImplementation2<SuiteSources.Group>, SourceForBinaryQueryImplementation2,
+                SourceLevelQueryImplementation2, SubprojectProvider, MultipleRootsUnitTestForSourceQueryImplementation {
+    private static final Logger LOG = Logger.getLogger(SuiteSources.class.getName());
+    private static final SuiteSources CORE;
+
+    static {
+        MxSuite coreSuite = CoreSuite.CORE_5_279_0;
+        CORE = new SuiteSources(null, null, coreSuite);
+    }
+
+    private final MxSuite suite;
+    private final List<Group> groups;
+    private final List<Library> libraries;
+    private final List<Dist> distributions;
+    private final FileObject dir;
+    /**
+     * non-null if the dependencies haven't yet been properly initialized
+     */
+    private Map<String, Dep> transitiveDeps;
+    /**
+     * avoid GC of imported projects
+     */
+    private final SuiteProject prj;
+    private final Map<String, SuiteSources> imported;
+
+    SuiteSources(SuiteProject owner, FileObject dir, MxSuite suite) {
+        final Map<String, Dep> fillDeps = new HashMap<>();
+        this.prj = owner;
+        this.dir = dir;
+        this.groups = findGroups(fillDeps, suite, dir);
+        this.libraries = findLibraries(fillDeps, suite);
+        this.imported = findImportedSuites(dir, suite, fillDeps);
+        this.distributions = findDistributions(suite, this.libraries, this.groups, fillDeps);
+        this.suite = suite;
+        this.transitiveDeps = fillDeps;
+    }
+
+    @Override
+    public String toString() {
+        return "MxSources[" + (dir == null ? "mx" : dir.toURI()) + "]";
+    }
+
+    private List<Group> findGroups(Map<String, Dep> fillDeps, MxSuite s, FileObject dir) {
+        List<Group> arr = new ArrayList<>();
+        for (Map.Entry<String, MxProject> entry : s.projects().entrySet()) {
+            String name = entry.getKey();
+            MxProject mxPrj = entry.getValue();
+            FileObject prjDir = findPrjDir(dir, name, mxPrj);
+            if (prjDir == null) {
+                fillDeps.put(name, new Group(name, mxPrj, null, null, null, name, name));
+                continue;
+            }
+            String prevName = null;
+            Group firstGroup = null;
+            String binPrefix;
+            if (mxPrj.subDir() == null) {
+                binPrefix = "mxbuild/";
+            } else {
+                binPrefix = "mxbuild/" + mxPrj.subDir() + "/";
+            }
+            for (String rel : mxPrj.sourceDirs()) {
+                FileObject srcDir = prjDir.getFileObject(rel);
+                FileObject binDir = getSubDir(dir, binPrefix + name + "/bin");
+                FileObject srcGenDir = getSubDir(dir, binPrefix + name + "/src_gen");
+                if (srcDir != null && binDir != null) {
+                    String prgName = name + "-" + rel;
+                    String displayName;
+                    if (prevName == null) {
+                        displayName = name;
+                    } else {
+                        displayName = name + "[" + rel + "]";
+                    }
+                    Group g = new Group(name, mxPrj, srcDir, srcGenDir, binDir, prgName, displayName);
+                    arr.add(g);
+                    if (firstGroup == null) {
+                        firstGroup = g;
+                    }
+                    prevName = displayName;
+                }
+            }
+            if (firstGroup != null) {
+                fillDeps.put(name, firstGroup);
+            }
+        }
+        return arr;
+    }
+
+    private static FileObject getSubDir(FileObject dir, String relPath) {
+        FileObject subDir = dir.getFileObject(relPath);
+        if (subDir == null) {
+            try {
+                subDir = FileUtil.createFolder(dir, relPath);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return subDir;
+    }
+
+    private List<Library> findLibraries(Map<String, Dep> fillDeps, MxSuite suite) {
+        final Map<String, MxLibrary> allLibraries = new HashMap<>();
+        registerLibs(allLibraries, null, suite.libraries());
+
+        List<Library> arr = new ArrayList<>();
+        for (Map.Entry<String, MxLibrary> entry : allLibraries.entrySet()) {
+            final Library library = new Library(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        for (Map.Entry<String, MxLibrary> entry : suite.jdklibraries().entrySet()) {
+            final JdkLibrary library = new JdkLibrary(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        return arr;
+    }
+
+    private static Map<String, SuiteSources> findImportedSuites(FileObject dir, MxSuite s, Map<String, Dep> fillDeps) {
+        if (dir == null) {
+            return Collections.emptyMap();
+        }
+        CORE.registerDeps("mx", fillDeps);
+        final MxImports imports = s.imports();
+        if (imports != null) {
+            Map<String, SuiteSources> imported = new LinkedHashMap<>();
+            for (MxImports.Suite imp : imports.suites()) {
+                SuiteSources impSources = findSuiteSources(dir, imp);
+                final String suiteName = imp.name();
+                if (impSources == null) {
+                    LOG.log(Level.INFO, "cannot find imported suite: {0}", suiteName);
+                    continue;
+                }
+                imported.put(suiteName, impSources);
+                impSources.registerDeps(suiteName, fillDeps);
+            }
+            return imported;
+        }
+        return Collections.emptyMap();
+    }
+
+    private List<Dist> findDistributions(MxSuite s, List<Library> libraries, List<Group> groups, Map<String, Dep> fillDeps) {
+        List<Dist> dists = new ArrayList<>();
+        for (Map.Entry<String, MxDistribution> entry : s.distributions().entrySet()) {
+            Dist d = new Dist(entry.getKey(), entry.getValue());
+            dists.add(d);
+            fillDeps.put(d.getName(), d);
+        }
+        return dists;
+    }
+
+    final synchronized void computeTransitiveDeps() {
+        Map<String, Dep> collectedDeps = this.transitiveDeps;
+        if (collectedDeps == null) {
+            return;
+        }
+        this.transitiveDeps = null;
+        for (Library l : this.libraries) {
+            transitiveDeps(l, collectedDeps);
+        }
+        for (Group g : this.groups) {
+            transitiveDeps(g, collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            transitiveDeps(d, collectedDeps);
+        }
+        for (Group g : groups) {
+            g.computeClassPath(collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            d.computeSourceRoots(collectedDeps);
+        }
+    }
+
+    private static SuiteSources findSuiteSources(FileObject dir, MxImports.Suite imp) throws IllegalArgumentException {
+        SuiteSources sources = findSuiteSources(dir.getParent(), imp.name());
+        if (sources != null) {
+            return sources;
+        }
+        if (imp.subdir()) {
+            for (FileObject subDir : dir.getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+            for (FileObject subDir : dir.getParent().getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static SuiteSources findSuiteSources(FileObject root, String name) throws IllegalArgumentException {
+        FileObject impDir = root.getFileObject(name);
+        if (impDir != null) {
+            try {
+                Project impPrj = ProjectManager.getDefault().findProject(impDir);
+                return impPrj == null ? null : impPrj.getLookup().lookup(SuiteSources.class);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceGroup[] getSourceGroups(String string) {
+        return groups();
+    }
+
+    Group[] groups() {
+        return groups.toArray(new Group[0]);
+    }
+
+    Group findGroup(FileObject fo) {
+        for (Group g : groups) {
+            if (g.contains(fo)) {
+                return g;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void addChangeListener(ChangeListener cl) {
+    }
+
+    @Override
+    public void removeChangeListener(ChangeListener cl) {
+    }
+
+    private static FileObject findPrjDir(FileObject dir, String prjName, MxProject prj) {
+        if (dir == null) {
+            return null;
+        }
+        if (prj.dir() != null) {
+            return dir.getFileObject(prj.dir());
+        }
+        if (prj.subDir() != null) {
+            dir = dir.getFileObject(prj.subDir());
+            if (dir == null) {
+                return null;
+            }
+        }
+        return dir.getFileObject(prjName);
+    }
+
+    private Collection<Dep> transitiveDeps(Dep current, Map<String, Dep> fill) {
+        current.owner().computeTransitiveDeps();
+        final Collection<Dep> currentAllDeps = current.allDeps();
+        if (currentAllDeps == Collections.<Dep>emptySet()) {
+            throw new IllegalStateException("Cyclic dep on " + current.getName());
+        } else if (currentAllDeps != null) {
+            return currentAllDeps;
+        }
+        current.setAllDeps(Collections.emptySet());
+        TreeSet<Dep> computing = new TreeSet<>();
+        computing.add(current);
+        for (String depName : current.depNames()) {
+            Dep dep = fill.get(depName);
+            if (dep == null) {
+                int colon = depName.lastIndexOf(':');
+                dep = fill.get(depName.substring(colon + 1));
+                if (dep == null) {
+                    LOG.log(Level.INFO, "dep not found: {0}", depName);
+                    continue;
+                }
+            }
+            Collection<Dep> allDeps = transitiveDeps(dep, fill);
+            computing.addAll(allDeps);
+        }
+        current.setAllDeps(computing);
+        return computing;
+    }
+
+    private static void registerLibs(Map<String, MxLibrary> collect, String prefix, Map<String, MxLibrary> libraries) {
+        for (Map.Entry<String, MxLibrary> entry : libraries.entrySet()) {
+            String key = entry.getKey();
+            MxLibrary lib = entry.getValue();
+            if (prefix == null) {
+                collect.put(key, lib);
+            } else {
+                collect.put(prefix + ":" + key, lib);
+            }
+        }
+    }
+
+    private void registerDeps(String prefix, Map<String, Dep> fillDeps) {
+        for (Library library : libraries) {
+            fillDeps.put(prefix + ":" + library.getName(), library);
+        }
+        for (Dist d : distributions) {
+            fillDeps.put(prefix + ":" + d.getName(), d);
+        }
+        for (Map.Entry<String, SuiteSources> s : imported.entrySet()) {
+            s.getValue().registerDeps(s.getKey(), fillDeps);
+        }
+    }
+
+    @Override
+    public Group findBinaryRoots2(URL url) {
+        final FileObject srcFo = URLMapper.findFileObject(url);
+        for (Group group : this.groups) {
+            if (group.contains(srcFo)) {
+                return group;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public URL[] computeRoots(Group group) {
+        if (group.binDir != null) {
+            return new URL[] { group.binDir.toURL() };
+        } else {
+            return new URL[0];
+        }
+    }
+
+    @Override
+    public boolean computePreferBinaries(Group result) {
+        return true;
+    }
+
+    @Override
+    public void computeChangeListener(Group result, boolean bln, ChangeListener cl) {
+    }
+
+    @Override
+    public SourceForBinaryQueryImplementation2.Result findSourceRoots2(URL url) {
+        this.computeTransitiveDeps();
+        for (Dist dist : this.distributions) {
+            URL jar;
+            try {
+                jar = dist.getJarRoot();
+                if (jar == null) {
+                    continue;
+                }
+            } catch (MalformedURLException ok) {
+                continue;
+            }
+            if (jar.equals(url)) {
+                List<FileObject> roots = new ArrayList<>();
+                for (Group d : dist.getContributingGroups()) {
+                    roots.add(d.srcDir);
+                    roots.add(d.srcGenDir);
+                }
+                return new ImmutableResult(roots.toArray(new FileObject[roots.size()]));
+            }
+        }
+        for (Group group : this.groups) {
+            if (group.binDir != null && group.binDir.toURL().equals(url)) {
+                return new ImmutableResult(group.srcDir, group.srcGenDir);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceForBinaryQuery.Result findSourceRoots(URL url) {
+        return findSourceRoots2(url);
+    }
+
+    final Iterable<File> jdks() {
+        Set<File> jdks = new LinkedHashSet<>();
+        String home = System.getProperty("user.home");
+        if (home != null) {
+            File userEnv = new File(new File(new File(home), ".mx"), "env");
+            findJdksInEnv(jdks, userEnv);
+        }
+        FileObject suiteEnv = dir.getFileObject("mx." + dir.getNameExt() + "/env");
+        if (suiteEnv != null) {
+            findJdksInEnv(jdks, FileUtil.toFile(suiteEnv));
+        }
+
+        String javaHomeEnv = System.getenv("JAVA_HOME");
+        if (javaHomeEnv != null) {
+            jdks.add(new File(javaHomeEnv));
+        }
+        String javaHomeProp = System.getProperty("java.home");
+        if (javaHomeProp != null) {
+            jdks.add(new File(javaHomeProp));
+        }
+        return jdks;
+    }
+
+    private void findJdksInEnv(Set<File> jdks, File env) {
+        if (env == null || !env.isFile()) {
+            return;
+        }
+        try (final FileInputStream is = new FileInputStream(env)) {
+            Properties p = new Properties();
+            p.load(is);
+
+            String javaHome = p.getProperty("JAVA_HOME");
+            if (javaHome != null) {
+                jdks.add(new File(javaHome));
+            }
+
+            String extraJavaHomes = p.getProperty("EXTRA_JAVA_HOMES");
+            if (extraJavaHomes != null) {
+                for (String extraHome : extraJavaHomes.split(File.pathSeparator)) {
+                    jdks.add(new File(extraHome));
+                }
+            }
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    @Override
+    public SourceLevelQueryImplementation2.Result getSourceLevel(FileObject fo) {
+        Group g = findGroup(fo);
+        if (g == null) {
+            return null;
+        }
+        return new SourceLevelQueryImplementation2.Result2() {
+            @Override
+            public SourceLevelQuery.Profile getProfile() {
+                return SourceLevelQuery.Profile.DEFAULT;
+            }
+
+            @Override
+            public String getSourceLevel() {
+                return g.getCompliance().getSourceLevel();
+            }
+
+            @Override
+            public void addChangeListener(ChangeListener cl) {
+            }
+
+            @Override
+            public void removeChangeListener(ChangeListener cl) {
+            }
+        };
+    }
+
+    @Override
+    public Set<? extends Project> getSubprojects() {
+        Set<Project> prjs = new HashSet<>();
+        for (SuiteSources imp : imported.values()) {
+            prjs.add(imp.prj);
+        }
+        return prjs;
+    }
+
+    @Override
+    public URL[] findUnitTests(FileObject fo) {
+        return new URL[0];
+    }
+
+    @Override
+    public URL[] findSources(FileObject fo) {
+        Group g = findGroup(fo);
+        return g == null ? new URL[0] : new URL[] { g.getRootFolder().toURL() };
+    }
+
+    static interface Dep extends Comparable<Dep> {
+        String getName();
+
+        Collection<String> depNames();
+
+        Collection<Dep> allDeps();
+
+        void setAllDeps(Collection<Dep> set);
+
+        @Override
+        public default int compareTo(Dep o) {
+            return getName().compareTo(o.getName());
+        }
+
+        SuiteSources owner();
+    }
+
+    final class Dist implements Dep, FlaggedClassPathImplementation {
+        final String name;
+        final MxDistribution dist;
+        Collection<Dep> allDeps;
+        private final PropertyChangeSupport support = new PropertyChangeSupport(this);
+        private Boolean exists;
+        private Collection<Group> groups;
+
+        public Dist(String name, MxDistribution dist) {
+            this.name = name;
+            this.dist = dist;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            Set<String> deps = new TreeSet<>();
+            deps.addAll(dist.distDependencies());
+            deps.addAll(dist.exclude());
+            return deps;
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return this.allDeps;
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            this.allDeps = set;
+        }
+
+        @Override
+        public String getName() {
+            return this.name;
+        }
+
+        @Override
+        public Set<ClassPath.Flag> getFlags() {
+            return exists ? Collections.emptySet() : Collections.singleton(ClassPath.Flag.INCOMPLETE);
+        }
+
+        private FileObject getJar(boolean ignore) {

Review comment:
       Maybe an useless parameter

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteSources.java
##########
@@ -0,0 +1,1195 @@
+/*
+ * 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.mx.project;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.Icon;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.java.mx.project.suitepy.MxDistribution;
+import org.netbeans.modules.java.mx.project.suitepy.MxImports;
+import org.netbeans.modules.java.mx.project.suitepy.MxLibrary;
+import org.netbeans.modules.java.mx.project.suitepy.MxProject;
+import org.netbeans.modules.java.mx.project.suitepy.MxSuite;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery;
+import org.netbeans.api.java.queries.SourceForBinaryQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+import org.netbeans.spi.java.classpath.ClassPathFactory;
+import org.netbeans.spi.java.classpath.ClassPathImplementation;
+import org.netbeans.spi.java.classpath.FlaggedClassPathImplementation;
+import org.netbeans.spi.java.classpath.PathResourceImplementation;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation2;
+import org.netbeans.spi.java.queries.SourceForBinaryQueryImplementation2;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
+import org.openide.util.Exceptions;
+import org.openide.util.Utilities;
+import java.util.stream.Collectors;
+import org.netbeans.api.java.queries.SourceLevelQuery;
+import org.netbeans.spi.java.queries.MultipleRootsUnitTestForSourceQueryImplementation;
+import org.netbeans.spi.java.queries.SourceLevelQueryImplementation2;
+import org.netbeans.spi.project.SubprojectProvider;
+
+final class SuiteSources implements Sources,
+                BinaryForSourceQueryImplementation2<SuiteSources.Group>, SourceForBinaryQueryImplementation2,
+                SourceLevelQueryImplementation2, SubprojectProvider, MultipleRootsUnitTestForSourceQueryImplementation {
+    private static final Logger LOG = Logger.getLogger(SuiteSources.class.getName());
+    private static final SuiteSources CORE;
+
+    static {
+        MxSuite coreSuite = CoreSuite.CORE_5_279_0;
+        CORE = new SuiteSources(null, null, coreSuite);
+    }
+
+    private final MxSuite suite;
+    private final List<Group> groups;
+    private final List<Library> libraries;
+    private final List<Dist> distributions;
+    private final FileObject dir;
+    /**
+     * non-null if the dependencies haven't yet been properly initialized
+     */
+    private Map<String, Dep> transitiveDeps;
+    /**
+     * avoid GC of imported projects
+     */
+    private final SuiteProject prj;
+    private final Map<String, SuiteSources> imported;
+
+    SuiteSources(SuiteProject owner, FileObject dir, MxSuite suite) {
+        final Map<String, Dep> fillDeps = new HashMap<>();
+        this.prj = owner;
+        this.dir = dir;
+        this.groups = findGroups(fillDeps, suite, dir);
+        this.libraries = findLibraries(fillDeps, suite);
+        this.imported = findImportedSuites(dir, suite, fillDeps);
+        this.distributions = findDistributions(suite, this.libraries, this.groups, fillDeps);
+        this.suite = suite;
+        this.transitiveDeps = fillDeps;
+    }
+
+    @Override
+    public String toString() {
+        return "MxSources[" + (dir == null ? "mx" : dir.toURI()) + "]";
+    }
+
+    private List<Group> findGroups(Map<String, Dep> fillDeps, MxSuite s, FileObject dir) {
+        List<Group> arr = new ArrayList<>();
+        for (Map.Entry<String, MxProject> entry : s.projects().entrySet()) {
+            String name = entry.getKey();
+            MxProject mxPrj = entry.getValue();
+            FileObject prjDir = findPrjDir(dir, name, mxPrj);
+            if (prjDir == null) {
+                fillDeps.put(name, new Group(name, mxPrj, null, null, null, name, name));
+                continue;
+            }
+            String prevName = null;
+            Group firstGroup = null;
+            String binPrefix;
+            if (mxPrj.subDir() == null) {
+                binPrefix = "mxbuild/";
+            } else {
+                binPrefix = "mxbuild/" + mxPrj.subDir() + "/";
+            }
+            for (String rel : mxPrj.sourceDirs()) {
+                FileObject srcDir = prjDir.getFileObject(rel);
+                FileObject binDir = getSubDir(dir, binPrefix + name + "/bin");
+                FileObject srcGenDir = getSubDir(dir, binPrefix + name + "/src_gen");
+                if (srcDir != null && binDir != null) {
+                    String prgName = name + "-" + rel;
+                    String displayName;
+                    if (prevName == null) {
+                        displayName = name;
+                    } else {
+                        displayName = name + "[" + rel + "]";
+                    }
+                    Group g = new Group(name, mxPrj, srcDir, srcGenDir, binDir, prgName, displayName);
+                    arr.add(g);
+                    if (firstGroup == null) {
+                        firstGroup = g;
+                    }
+                    prevName = displayName;
+                }
+            }
+            if (firstGroup != null) {
+                fillDeps.put(name, firstGroup);
+            }
+        }
+        return arr;
+    }
+
+    private static FileObject getSubDir(FileObject dir, String relPath) {
+        FileObject subDir = dir.getFileObject(relPath);
+        if (subDir == null) {
+            try {
+                subDir = FileUtil.createFolder(dir, relPath);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return subDir;
+    }
+
+    private List<Library> findLibraries(Map<String, Dep> fillDeps, MxSuite suite) {
+        final Map<String, MxLibrary> allLibraries = new HashMap<>();
+        registerLibs(allLibraries, null, suite.libraries());
+
+        List<Library> arr = new ArrayList<>();
+        for (Map.Entry<String, MxLibrary> entry : allLibraries.entrySet()) {
+            final Library library = new Library(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        for (Map.Entry<String, MxLibrary> entry : suite.jdklibraries().entrySet()) {
+            final JdkLibrary library = new JdkLibrary(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        return arr;
+    }
+
+    private static Map<String, SuiteSources> findImportedSuites(FileObject dir, MxSuite s, Map<String, Dep> fillDeps) {
+        if (dir == null) {
+            return Collections.emptyMap();
+        }
+        CORE.registerDeps("mx", fillDeps);
+        final MxImports imports = s.imports();
+        if (imports != null) {
+            Map<String, SuiteSources> imported = new LinkedHashMap<>();
+            for (MxImports.Suite imp : imports.suites()) {
+                SuiteSources impSources = findSuiteSources(dir, imp);
+                final String suiteName = imp.name();
+                if (impSources == null) {
+                    LOG.log(Level.INFO, "cannot find imported suite: {0}", suiteName);
+                    continue;
+                }
+                imported.put(suiteName, impSources);
+                impSources.registerDeps(suiteName, fillDeps);
+            }
+            return imported;
+        }
+        return Collections.emptyMap();
+    }
+
+    private List<Dist> findDistributions(MxSuite s, List<Library> libraries, List<Group> groups, Map<String, Dep> fillDeps) {
+        List<Dist> dists = new ArrayList<>();
+        for (Map.Entry<String, MxDistribution> entry : s.distributions().entrySet()) {
+            Dist d = new Dist(entry.getKey(), entry.getValue());
+            dists.add(d);
+            fillDeps.put(d.getName(), d);
+        }
+        return dists;
+    }
+
+    final synchronized void computeTransitiveDeps() {
+        Map<String, Dep> collectedDeps = this.transitiveDeps;
+        if (collectedDeps == null) {
+            return;
+        }
+        this.transitiveDeps = null;
+        for (Library l : this.libraries) {
+            transitiveDeps(l, collectedDeps);
+        }
+        for (Group g : this.groups) {
+            transitiveDeps(g, collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            transitiveDeps(d, collectedDeps);
+        }
+        for (Group g : groups) {
+            g.computeClassPath(collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            d.computeSourceRoots(collectedDeps);
+        }
+    }
+
+    private static SuiteSources findSuiteSources(FileObject dir, MxImports.Suite imp) throws IllegalArgumentException {
+        SuiteSources sources = findSuiteSources(dir.getParent(), imp.name());
+        if (sources != null) {
+            return sources;
+        }
+        if (imp.subdir()) {
+            for (FileObject subDir : dir.getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+            for (FileObject subDir : dir.getParent().getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static SuiteSources findSuiteSources(FileObject root, String name) throws IllegalArgumentException {
+        FileObject impDir = root.getFileObject(name);
+        if (impDir != null) {
+            try {
+                Project impPrj = ProjectManager.getDefault().findProject(impDir);
+                return impPrj == null ? null : impPrj.getLookup().lookup(SuiteSources.class);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceGroup[] getSourceGroups(String string) {
+        return groups();
+    }
+
+    Group[] groups() {
+        return groups.toArray(new Group[0]);
+    }
+
+    Group findGroup(FileObject fo) {
+        for (Group g : groups) {
+            if (g.contains(fo)) {
+                return g;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void addChangeListener(ChangeListener cl) {
+    }
+
+    @Override
+    public void removeChangeListener(ChangeListener cl) {
+    }
+
+    private static FileObject findPrjDir(FileObject dir, String prjName, MxProject prj) {
+        if (dir == null) {
+            return null;
+        }
+        if (prj.dir() != null) {
+            return dir.getFileObject(prj.dir());
+        }
+        if (prj.subDir() != null) {
+            dir = dir.getFileObject(prj.subDir());
+            if (dir == null) {
+                return null;
+            }
+        }
+        return dir.getFileObject(prjName);
+    }
+
+    private Collection<Dep> transitiveDeps(Dep current, Map<String, Dep> fill) {
+        current.owner().computeTransitiveDeps();
+        final Collection<Dep> currentAllDeps = current.allDeps();
+        if (currentAllDeps == Collections.<Dep>emptySet()) {
+            throw new IllegalStateException("Cyclic dep on " + current.getName());
+        } else if (currentAllDeps != null) {
+            return currentAllDeps;
+        }
+        current.setAllDeps(Collections.emptySet());
+        TreeSet<Dep> computing = new TreeSet<>();
+        computing.add(current);
+        for (String depName : current.depNames()) {
+            Dep dep = fill.get(depName);
+            if (dep == null) {
+                int colon = depName.lastIndexOf(':');
+                dep = fill.get(depName.substring(colon + 1));
+                if (dep == null) {
+                    LOG.log(Level.INFO, "dep not found: {0}", depName);
+                    continue;
+                }
+            }
+            Collection<Dep> allDeps = transitiveDeps(dep, fill);
+            computing.addAll(allDeps);
+        }
+        current.setAllDeps(computing);
+        return computing;
+    }
+
+    private static void registerLibs(Map<String, MxLibrary> collect, String prefix, Map<String, MxLibrary> libraries) {
+        for (Map.Entry<String, MxLibrary> entry : libraries.entrySet()) {
+            String key = entry.getKey();
+            MxLibrary lib = entry.getValue();
+            if (prefix == null) {
+                collect.put(key, lib);
+            } else {
+                collect.put(prefix + ":" + key, lib);
+            }
+        }
+    }
+
+    private void registerDeps(String prefix, Map<String, Dep> fillDeps) {
+        for (Library library : libraries) {
+            fillDeps.put(prefix + ":" + library.getName(), library);
+        }
+        for (Dist d : distributions) {
+            fillDeps.put(prefix + ":" + d.getName(), d);
+        }
+        for (Map.Entry<String, SuiteSources> s : imported.entrySet()) {
+            s.getValue().registerDeps(s.getKey(), fillDeps);
+        }
+    }
+
+    @Override
+    public Group findBinaryRoots2(URL url) {
+        final FileObject srcFo = URLMapper.findFileObject(url);
+        for (Group group : this.groups) {
+            if (group.contains(srcFo)) {
+                return group;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public URL[] computeRoots(Group group) {
+        if (group.binDir != null) {
+            return new URL[] { group.binDir.toURL() };
+        } else {
+            return new URL[0];
+        }
+    }
+
+    @Override
+    public boolean computePreferBinaries(Group result) {
+        return true;
+    }
+
+    @Override
+    public void computeChangeListener(Group result, boolean bln, ChangeListener cl) {
+    }
+
+    @Override
+    public SourceForBinaryQueryImplementation2.Result findSourceRoots2(URL url) {
+        this.computeTransitiveDeps();
+        for (Dist dist : this.distributions) {
+            URL jar;
+            try {
+                jar = dist.getJarRoot();
+                if (jar == null) {
+                    continue;
+                }
+            } catch (MalformedURLException ok) {
+                continue;
+            }
+            if (jar.equals(url)) {
+                List<FileObject> roots = new ArrayList<>();
+                for (Group d : dist.getContributingGroups()) {
+                    roots.add(d.srcDir);
+                    roots.add(d.srcGenDir);
+                }
+                return new ImmutableResult(roots.toArray(new FileObject[roots.size()]));
+            }
+        }
+        for (Group group : this.groups) {
+            if (group.binDir != null && group.binDir.toURL().equals(url)) {
+                return new ImmutableResult(group.srcDir, group.srcGenDir);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceForBinaryQuery.Result findSourceRoots(URL url) {
+        return findSourceRoots2(url);
+    }
+
+    final Iterable<File> jdks() {
+        Set<File> jdks = new LinkedHashSet<>();
+        String home = System.getProperty("user.home");
+        if (home != null) {
+            File userEnv = new File(new File(new File(home), ".mx"), "env");
+            findJdksInEnv(jdks, userEnv);
+        }
+        FileObject suiteEnv = dir.getFileObject("mx." + dir.getNameExt() + "/env");
+        if (suiteEnv != null) {
+            findJdksInEnv(jdks, FileUtil.toFile(suiteEnv));
+        }
+
+        String javaHomeEnv = System.getenv("JAVA_HOME");
+        if (javaHomeEnv != null) {
+            jdks.add(new File(javaHomeEnv));
+        }
+        String javaHomeProp = System.getProperty("java.home");
+        if (javaHomeProp != null) {
+            jdks.add(new File(javaHomeProp));
+        }
+        return jdks;
+    }
+
+    private void findJdksInEnv(Set<File> jdks, File env) {
+        if (env == null || !env.isFile()) {
+            return;
+        }
+        try (final FileInputStream is = new FileInputStream(env)) {
+            Properties p = new Properties();
+            p.load(is);
+
+            String javaHome = p.getProperty("JAVA_HOME");
+            if (javaHome != null) {
+                jdks.add(new File(javaHome));
+            }
+
+            String extraJavaHomes = p.getProperty("EXTRA_JAVA_HOMES");
+            if (extraJavaHomes != null) {
+                for (String extraHome : extraJavaHomes.split(File.pathSeparator)) {
+                    jdks.add(new File(extraHome));
+                }
+            }
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    @Override
+    public SourceLevelQueryImplementation2.Result getSourceLevel(FileObject fo) {
+        Group g = findGroup(fo);
+        if (g == null) {
+            return null;
+        }
+        return new SourceLevelQueryImplementation2.Result2() {
+            @Override
+            public SourceLevelQuery.Profile getProfile() {
+                return SourceLevelQuery.Profile.DEFAULT;
+            }
+
+            @Override
+            public String getSourceLevel() {
+                return g.getCompliance().getSourceLevel();
+            }
+
+            @Override
+            public void addChangeListener(ChangeListener cl) {
+            }
+
+            @Override
+            public void removeChangeListener(ChangeListener cl) {
+            }
+        };
+    }
+
+    @Override
+    public Set<? extends Project> getSubprojects() {
+        Set<Project> prjs = new HashSet<>();
+        for (SuiteSources imp : imported.values()) {
+            prjs.add(imp.prj);
+        }
+        return prjs;
+    }
+
+    @Override
+    public URL[] findUnitTests(FileObject fo) {
+        return new URL[0];
+    }
+
+    @Override
+    public URL[] findSources(FileObject fo) {
+        Group g = findGroup(fo);
+        return g == null ? new URL[0] : new URL[] { g.getRootFolder().toURL() };
+    }
+
+    static interface Dep extends Comparable<Dep> {
+        String getName();
+
+        Collection<String> depNames();
+
+        Collection<Dep> allDeps();
+
+        void setAllDeps(Collection<Dep> set);
+
+        @Override
+        public default int compareTo(Dep o) {
+            return getName().compareTo(o.getName());
+        }
+
+        SuiteSources owner();
+    }
+
+    final class Dist implements Dep, FlaggedClassPathImplementation {
+        final String name;
+        final MxDistribution dist;
+        Collection<Dep> allDeps;
+        private final PropertyChangeSupport support = new PropertyChangeSupport(this);
+        private Boolean exists;
+        private Collection<Group> groups;
+
+        public Dist(String name, MxDistribution dist) {
+            this.name = name;
+            this.dist = dist;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            Set<String> deps = new TreeSet<>();
+            deps.addAll(dist.distDependencies());
+            deps.addAll(dist.exclude());
+            return deps;
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return this.allDeps;
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            this.allDeps = set;
+        }
+
+        @Override
+        public String getName() {
+            return this.name;
+        }
+
+        @Override
+        public Set<ClassPath.Flag> getFlags() {
+            return exists ? Collections.emptySet() : Collections.singleton(ClassPath.Flag.INCOMPLETE);
+        }
+
+        private FileObject getJar(boolean ignore) {
+            if (SuiteSources.this.dir == null) {
+                return null;
+            }
+            FileObject dists = SuiteSources.this.dir.getFileObject("mxbuild/dists");
+            if (dists == null) {
+                return null;
+            }
+            List<FileObject> dist = Arrays.stream(dists.getChildren()).filter((fo) -> fo.isFolder() && fo.getName().startsWith("jdk")).collect(Collectors.toList());
+            dist.sort((fo1, fo2) -> fo2.getName().compareTo(fo1.getName()));
+            for (FileObject jdkDir : dist) {
+                FileObject jar = jdkDir.getFileObject(name.toLowerCase().replace("_", "-") + ".jar");
+                if (jar != null) {
+                    return jar;
+                }
+            }
+            FileObject jar = dists.getFileObject(name.toLowerCase().replace("_", "-") + ".jar");
+            if (jar != null) {
+                return jar;
+            }
+            return null;
+        }
+
+        @Override
+        public List<? extends PathResourceImplementation> getResources() {
+            computeTransitiveDeps();
+            FileObject jar = getJar(exists == null);
+            final boolean existsNow = jar != null && jar.isData();
+            if (exists == null) {
+                exists = existsNow;
+            } else {
+                if (exists != existsNow) {
+                    exists = existsNow;
+                    support.firePropertyChange(PROP_FLAGS, !exists, (boolean) exists);
+                }
+            }
+            if (jar != null) {
+                PathResourceImplementation res;
+                try {
+                    res = ClassPathSupport.createResource(getJarRoot());
+                    return Collections.singletonList(res);
+                } catch (MalformedURLException ex) {
+                    // OK
+                }
+            }
+            return Collections.emptyList();
+        }
+
+        private URL getJarRoot() throws MalformedURLException {
+            FileObject jar = getJar(true);
+            if (jar != null) {
+                return new URL("jar:" + jar.toURL() + "!/");
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener pl) {
+            support.addPropertyChangeListener(pl);
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener pl) {
+            support.removePropertyChangeListener(pl);
+        }
+
+        @Override
+        public SuiteSources owner() {
+            return SuiteSources.this;
+        }
+
+        private void computeSourceRoots(Map<String, Dep> collectedDeps) {
+            if (groups != null) {
+                return;
+            }
+            Set<Group> contributingGroups = new LinkedHashSet<>();
+            for (String d : this.dist.dependencies()) {
+                Dep dep = collectedDeps.get(d);
+                if (dep == null || dep.allDeps() == null) {
+                    continue;
+                }
+                for (Dep d2 : dep.allDeps()) {
+                    if (d2 instanceof Group) {
+                        contributingGroups.add((Group) d2);
+                    }
+                }
+            }
+            for (String d : this.dist.distDependencies()) {
+                final Dep anyDep = collectedDeps.get(d);
+                if (anyDep instanceof Dist) {
+                    Dist dep = (Dist) anyDep;
+                    dep.computeSourceRoots(collectedDeps);
+                    contributingGroups.removeAll(dep.getContributingGroups());
+                }
+            }
+            groups = contributingGroups;
+        }
+
+        public Collection<Group> getContributingGroups() {
+            return groups;
+        }
+
+        @Override
+        public String toString() {
+            return "Dist[name=" + name + "]";
+        }
+    }
+
+    final class Group implements SourceGroup, Dep, AnnotationProcessingQuery.Result,
+            Compliance.Provider {
+        private final String mxName;
+        private final MxProject mxPrj;
+        private final FileObject srcDir;
+        private final FileObject srcGenDir;
+        private final FileObject binDir;
+        private final String name;
+        private final String displayName;
+        private final Compliance compliance;
+        private ClassPath sourceCP;
+        private ClassPath cp;
+        private ClassPath processorPath;
+        private Collection<Dep> allDeps;
+
+        Group(String mxName, MxProject mxPrj, FileObject srcDir, FileObject srcGenDir, FileObject binDir, String name, String displayName) {
+            this.mxName = mxName;
+            this.mxPrj = mxPrj;
+            this.srcDir = srcDir;
+            this.srcGenDir = srcGenDir;
+            this.binDir = binDir;
+            this.name = name;
+            this.displayName = displayName;
+            this.compliance = Compliance.parse(mxPrj.javaCompliance());
+        }
+
+        @Override
+        public FileObject getRootFolder() {
+            return srcDir;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getDisplayName() {
+            return displayName;
+        }
+
+        @Override
+        public Icon getIcon(boolean opened) {
+            return null;
+        }
+
+        @Override
+        public Compliance getCompliance() {
+            return compliance;
+        }
+
+        @Override
+        public boolean contains(FileObject file) {
+            if (file == srcDir || file == srcGenDir || FileUtil.isParentOf(srcDir, file) || (srcGenDir != null && FileUtil.isParentOf(srcGenDir, file))) {
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener l) {
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener l) {
+        }
+
+        @Override
+        public String toString() {
+            return "SuiteSources.Group[name=" + name + ",rootFolder=" + srcDir + "]"; // NOI18N
+        }
+
+        ClassPath getSourceCP() {
+            computeTransitiveDeps();
+            return sourceCP;
+        }
+
+        ClassPath getCP() {
+            computeTransitiveDeps();
+            return cp;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            return mxPrj.dependencies();
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            allDeps = set;
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return allDeps;
+        }
+
+        private void computeClassPath(Map<String, Dep> transDeps) {
+            for (Dep d : transDeps.values()) {
+                d.owner().computeTransitiveDeps();
+            }
+
+            List<Group> arr = new ArrayList<>();
+            List<ClassPathImplementation> libs = new ArrayList<>();
+            processTransDep(transDeps.get(mxName), arr, libs);
+            cp = composeClassPath(arr, libs);
+            List<FileObject> roots = new ArrayList<>();
+            if (srcDir != null) {
+                roots.add(srcDir);
+            }
+            if (srcGenDir != null) {
+                roots.add(srcGenDir);
+            }
+            sourceCP = ClassPathSupport.createClassPath(roots.toArray(new FileObject[roots.size()]));
+
+            if (mxPrj.annotationProcessors().isEmpty()) {
+                processorPath = null;
+            } else {
+                List<Group> groups = new ArrayList<>();
+                List<ClassPathImplementation> jars = new ArrayList<>();
+                for (String dep : mxPrj.annotationProcessors()) {
+                    processTransDep(transDeps.get(dep), groups, jars);
+                }
+                processorPath = composeClassPath(groups, jars);
+            }
+        }
+
+        private void processTransDep(Dep dep, List<Group> addGroups, List<ClassPathImplementation> addJars) {
+            if (dep != null) {
+                dep.owner().computeTransitiveDeps();
+                for (Dep d : dep.allDeps()) {
+                    if (d == this) {
+                        continue;
+                    }
+                    d.owner().computeTransitiveDeps();
+                    if (d instanceof Group) {
+                        addGroups.add((Group) d);
+                    } else if (d instanceof ClassPathImplementation) {
+                        addJars.add((ClassPathImplementation) d);
+                    }
+                }
+            }
+        }
+
+        private ClassPath composeClassPath(List<Group> arr, List<ClassPathImplementation> libs) {
+            Set<FileObject> roots = new LinkedHashSet<>();
+            final int depsCount = arr.size();
+            for (int i = 0; i < depsCount; i++) {
+                final Group g = arr.get(i);
+                if (g.binDir != null) {
+                    roots.add(g.binDir);
+                }
+            }
+            ClassPath prjCp = ClassPathSupport.createClassPath(roots.toArray(new FileObject[0]));
+            if (!libs.isEmpty()) {
+                if (libs.size() == 1) {
+                    prjCp = ClassPathSupport.createProxyClassPath(prjCp,
+                                                                  ClassPathFactory.createClassPath(libs.get(0))
+                    );
+                } else {
+                    prjCp = ClassPathSupport.createProxyClassPath(prjCp,
+                                                                  ClassPathFactory.createClassPath(
+                                                                                  ClassPathSupport.createProxyClassPathImplementation(
+                                                                                                  libs.toArray(new ClassPathImplementation[0])
+                                                                                  )
+                                                                  )
+                    );
+                }
+            }
+            return prjCp;
+        }
+
+        ClassPath getProcessorCP() {
+            computeTransitiveDeps();
+            return processorPath;
+        }
+
+        @Override
+        public Set<? extends AnnotationProcessingQuery.Trigger> annotationProcessingEnabled() {
+            return EnumSet.of(AnnotationProcessingQuery.Trigger.ON_SCAN, AnnotationProcessingQuery.Trigger.IN_EDITOR);
+        }
+
+        @Override
+        public Iterable<? extends String> annotationProcessorsToRun() {
+            return null;
+        }
+
+        @Override
+        public URL sourceOutputDirectory() {
+            return srcGenDir == null ? null : srcGenDir.toURL();
+        }
+
+        @Override
+        public Map<? extends String, ? extends String> processorOptions() {
+            return Collections.emptyMap();
+        }
+
+        @Override
+        public void addChangeListener(ChangeListener l) {
+        }
+
+        @Override
+        public void removeChangeListener(ChangeListener l) {
+        }
+
+        @Override
+        public SuiteSources owner() {
+            return SuiteSources.this;
+        }
+    }
+
+    private class Library implements FlaggedClassPathImplementation, Dep {
+        final MxLibrary lib;
+        final PropertyChangeSupport support = new PropertyChangeSupport(this);
+        final String libName;
+        Collection<Dep> allDeps;
+        Boolean exists;
+
+        Library(String libName, MxLibrary lib) {
+            this.libName = libName;
+            this.lib = getOSSLibrary(lib);
+        }
+
+        final MxLibrary getOSSLibrary(MxLibrary lib) {
+            if (lib.sha1() == null && !lib.os_arch().isEmpty()) {
+                Map<String, MxLibrary.Arch> os_dep_libs = lib.os_arch();
+                String os = System.getProperty("os.name").toLowerCase();
+                for (Map.Entry<String, MxLibrary.Arch> entry : os_dep_libs.entrySet()) {
+                    if (os.contains(entry.getKey())) {
+                        return entry.getValue().amd64();

Review comment:
       Uh :) I meant `mx` already supports `aarch64`, maybe even `i386` ?

##########
File path: java/java.mx.project/src/org/netbeans/modules/java/mx/project/SuiteSources.java
##########
@@ -0,0 +1,1195 @@
+/*
+ * 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.mx.project;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.Icon;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.java.mx.project.suitepy.MxDistribution;
+import org.netbeans.modules.java.mx.project.suitepy.MxImports;
+import org.netbeans.modules.java.mx.project.suitepy.MxLibrary;
+import org.netbeans.modules.java.mx.project.suitepy.MxProject;
+import org.netbeans.modules.java.mx.project.suitepy.MxSuite;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.AnnotationProcessingQuery;
+import org.netbeans.api.java.queries.SourceForBinaryQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.SourceGroup;
+import org.netbeans.api.project.Sources;
+import org.netbeans.spi.java.classpath.ClassPathFactory;
+import org.netbeans.spi.java.classpath.ClassPathImplementation;
+import org.netbeans.spi.java.classpath.FlaggedClassPathImplementation;
+import org.netbeans.spi.java.classpath.PathResourceImplementation;
+import org.netbeans.spi.java.classpath.support.ClassPathSupport;
+import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation2;
+import org.netbeans.spi.java.queries.SourceForBinaryQueryImplementation2;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
+import org.openide.util.Exceptions;
+import org.openide.util.Utilities;
+import java.util.stream.Collectors;
+import org.netbeans.api.java.queries.SourceLevelQuery;
+import org.netbeans.spi.java.queries.MultipleRootsUnitTestForSourceQueryImplementation;
+import org.netbeans.spi.java.queries.SourceLevelQueryImplementation2;
+import org.netbeans.spi.project.SubprojectProvider;
+
+final class SuiteSources implements Sources,
+                BinaryForSourceQueryImplementation2<SuiteSources.Group>, SourceForBinaryQueryImplementation2,
+                SourceLevelQueryImplementation2, SubprojectProvider, MultipleRootsUnitTestForSourceQueryImplementation {
+    private static final Logger LOG = Logger.getLogger(SuiteSources.class.getName());
+    private static final SuiteSources CORE;
+
+    static {
+        MxSuite coreSuite = CoreSuite.CORE_5_279_0;
+        CORE = new SuiteSources(null, null, coreSuite);
+    }
+
+    private final MxSuite suite;
+    private final List<Group> groups;
+    private final List<Library> libraries;
+    private final List<Dist> distributions;
+    private final FileObject dir;
+    /**
+     * non-null if the dependencies haven't yet been properly initialized
+     */
+    private Map<String, Dep> transitiveDeps;
+    /**
+     * avoid GC of imported projects
+     */
+    private final SuiteProject prj;
+    private final Map<String, SuiteSources> imported;
+
+    SuiteSources(SuiteProject owner, FileObject dir, MxSuite suite) {
+        final Map<String, Dep> fillDeps = new HashMap<>();
+        this.prj = owner;
+        this.dir = dir;
+        this.groups = findGroups(fillDeps, suite, dir);
+        this.libraries = findLibraries(fillDeps, suite);
+        this.imported = findImportedSuites(dir, suite, fillDeps);
+        this.distributions = findDistributions(suite, this.libraries, this.groups, fillDeps);
+        this.suite = suite;
+        this.transitiveDeps = fillDeps;
+    }
+
+    @Override
+    public String toString() {
+        return "MxSources[" + (dir == null ? "mx" : dir.toURI()) + "]";
+    }
+
+    private List<Group> findGroups(Map<String, Dep> fillDeps, MxSuite s, FileObject dir) {
+        List<Group> arr = new ArrayList<>();
+        for (Map.Entry<String, MxProject> entry : s.projects().entrySet()) {
+            String name = entry.getKey();
+            MxProject mxPrj = entry.getValue();
+            FileObject prjDir = findPrjDir(dir, name, mxPrj);
+            if (prjDir == null) {
+                fillDeps.put(name, new Group(name, mxPrj, null, null, null, name, name));
+                continue;
+            }
+            String prevName = null;
+            Group firstGroup = null;
+            String binPrefix;
+            if (mxPrj.subDir() == null) {
+                binPrefix = "mxbuild/";
+            } else {
+                binPrefix = "mxbuild/" + mxPrj.subDir() + "/";
+            }
+            for (String rel : mxPrj.sourceDirs()) {
+                FileObject srcDir = prjDir.getFileObject(rel);
+                FileObject binDir = getSubDir(dir, binPrefix + name + "/bin");
+                FileObject srcGenDir = getSubDir(dir, binPrefix + name + "/src_gen");
+                if (srcDir != null && binDir != null) {
+                    String prgName = name + "-" + rel;
+                    String displayName;
+                    if (prevName == null) {
+                        displayName = name;
+                    } else {
+                        displayName = name + "[" + rel + "]";
+                    }
+                    Group g = new Group(name, mxPrj, srcDir, srcGenDir, binDir, prgName, displayName);
+                    arr.add(g);
+                    if (firstGroup == null) {
+                        firstGroup = g;
+                    }
+                    prevName = displayName;
+                }
+            }
+            if (firstGroup != null) {
+                fillDeps.put(name, firstGroup);
+            }
+        }
+        return arr;
+    }
+
+    private static FileObject getSubDir(FileObject dir, String relPath) {
+        FileObject subDir = dir.getFileObject(relPath);
+        if (subDir == null) {
+            try {
+                subDir = FileUtil.createFolder(dir, relPath);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return subDir;
+    }
+
+    private List<Library> findLibraries(Map<String, Dep> fillDeps, MxSuite suite) {
+        final Map<String, MxLibrary> allLibraries = new HashMap<>();
+        registerLibs(allLibraries, null, suite.libraries());
+
+        List<Library> arr = new ArrayList<>();
+        for (Map.Entry<String, MxLibrary> entry : allLibraries.entrySet()) {
+            final Library library = new Library(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        for (Map.Entry<String, MxLibrary> entry : suite.jdklibraries().entrySet()) {
+            final JdkLibrary library = new JdkLibrary(entry.getKey(), entry.getValue());
+            arr.add(library);
+            fillDeps.put(library.getName(), library);
+        }
+        return arr;
+    }
+
+    private static Map<String, SuiteSources> findImportedSuites(FileObject dir, MxSuite s, Map<String, Dep> fillDeps) {
+        if (dir == null) {
+            return Collections.emptyMap();
+        }
+        CORE.registerDeps("mx", fillDeps);
+        final MxImports imports = s.imports();
+        if (imports != null) {
+            Map<String, SuiteSources> imported = new LinkedHashMap<>();
+            for (MxImports.Suite imp : imports.suites()) {
+                SuiteSources impSources = findSuiteSources(dir, imp);
+                final String suiteName = imp.name();
+                if (impSources == null) {
+                    LOG.log(Level.INFO, "cannot find imported suite: {0}", suiteName);
+                    continue;
+                }
+                imported.put(suiteName, impSources);
+                impSources.registerDeps(suiteName, fillDeps);
+            }
+            return imported;
+        }
+        return Collections.emptyMap();
+    }
+
+    private List<Dist> findDistributions(MxSuite s, List<Library> libraries, List<Group> groups, Map<String, Dep> fillDeps) {
+        List<Dist> dists = new ArrayList<>();
+        for (Map.Entry<String, MxDistribution> entry : s.distributions().entrySet()) {
+            Dist d = new Dist(entry.getKey(), entry.getValue());
+            dists.add(d);
+            fillDeps.put(d.getName(), d);
+        }
+        return dists;
+    }
+
+    final synchronized void computeTransitiveDeps() {
+        Map<String, Dep> collectedDeps = this.transitiveDeps;
+        if (collectedDeps == null) {
+            return;
+        }
+        this.transitiveDeps = null;
+        for (Library l : this.libraries) {
+            transitiveDeps(l, collectedDeps);
+        }
+        for (Group g : this.groups) {
+            transitiveDeps(g, collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            transitiveDeps(d, collectedDeps);
+        }
+        for (Group g : groups) {
+            g.computeClassPath(collectedDeps);
+        }
+        for (Dist d : this.distributions) {
+            d.computeSourceRoots(collectedDeps);
+        }
+    }
+
+    private static SuiteSources findSuiteSources(FileObject dir, MxImports.Suite imp) throws IllegalArgumentException {
+        SuiteSources sources = findSuiteSources(dir.getParent(), imp.name());
+        if (sources != null) {
+            return sources;
+        }
+        if (imp.subdir()) {
+            for (FileObject subDir : dir.getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+            for (FileObject subDir : dir.getParent().getParent().getChildren()) {
+                sources = findSuiteSources(subDir, imp.name());
+                if (sources != null) {
+                    return sources;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static SuiteSources findSuiteSources(FileObject root, String name) throws IllegalArgumentException {
+        FileObject impDir = root.getFileObject(name);
+        if (impDir != null) {
+            try {
+                Project impPrj = ProjectManager.getDefault().findProject(impDir);
+                return impPrj == null ? null : impPrj.getLookup().lookup(SuiteSources.class);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceGroup[] getSourceGroups(String string) {
+        return groups();
+    }
+
+    Group[] groups() {
+        return groups.toArray(new Group[0]);
+    }
+
+    Group findGroup(FileObject fo) {
+        for (Group g : groups) {
+            if (g.contains(fo)) {
+                return g;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void addChangeListener(ChangeListener cl) {
+    }
+
+    @Override
+    public void removeChangeListener(ChangeListener cl) {
+    }
+
+    private static FileObject findPrjDir(FileObject dir, String prjName, MxProject prj) {
+        if (dir == null) {
+            return null;
+        }
+        if (prj.dir() != null) {
+            return dir.getFileObject(prj.dir());
+        }
+        if (prj.subDir() != null) {
+            dir = dir.getFileObject(prj.subDir());
+            if (dir == null) {
+                return null;
+            }
+        }
+        return dir.getFileObject(prjName);
+    }
+
+    private Collection<Dep> transitiveDeps(Dep current, Map<String, Dep> fill) {
+        current.owner().computeTransitiveDeps();
+        final Collection<Dep> currentAllDeps = current.allDeps();
+        if (currentAllDeps == Collections.<Dep>emptySet()) {
+            throw new IllegalStateException("Cyclic dep on " + current.getName());
+        } else if (currentAllDeps != null) {
+            return currentAllDeps;
+        }
+        current.setAllDeps(Collections.emptySet());
+        TreeSet<Dep> computing = new TreeSet<>();
+        computing.add(current);
+        for (String depName : current.depNames()) {
+            Dep dep = fill.get(depName);
+            if (dep == null) {
+                int colon = depName.lastIndexOf(':');
+                dep = fill.get(depName.substring(colon + 1));
+                if (dep == null) {
+                    LOG.log(Level.INFO, "dep not found: {0}", depName);
+                    continue;
+                }
+            }
+            Collection<Dep> allDeps = transitiveDeps(dep, fill);
+            computing.addAll(allDeps);
+        }
+        current.setAllDeps(computing);
+        return computing;
+    }
+
+    private static void registerLibs(Map<String, MxLibrary> collect, String prefix, Map<String, MxLibrary> libraries) {
+        for (Map.Entry<String, MxLibrary> entry : libraries.entrySet()) {
+            String key = entry.getKey();
+            MxLibrary lib = entry.getValue();
+            if (prefix == null) {
+                collect.put(key, lib);
+            } else {
+                collect.put(prefix + ":" + key, lib);
+            }
+        }
+    }
+
+    private void registerDeps(String prefix, Map<String, Dep> fillDeps) {
+        for (Library library : libraries) {
+            fillDeps.put(prefix + ":" + library.getName(), library);
+        }
+        for (Dist d : distributions) {
+            fillDeps.put(prefix + ":" + d.getName(), d);
+        }
+        for (Map.Entry<String, SuiteSources> s : imported.entrySet()) {
+            s.getValue().registerDeps(s.getKey(), fillDeps);
+        }
+    }
+
+    @Override
+    public Group findBinaryRoots2(URL url) {
+        final FileObject srcFo = URLMapper.findFileObject(url);
+        for (Group group : this.groups) {
+            if (group.contains(srcFo)) {
+                return group;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public URL[] computeRoots(Group group) {
+        if (group.binDir != null) {
+            return new URL[] { group.binDir.toURL() };
+        } else {
+            return new URL[0];
+        }
+    }
+
+    @Override
+    public boolean computePreferBinaries(Group result) {
+        return true;
+    }
+
+    @Override
+    public void computeChangeListener(Group result, boolean bln, ChangeListener cl) {
+    }
+
+    @Override
+    public SourceForBinaryQueryImplementation2.Result findSourceRoots2(URL url) {
+        this.computeTransitiveDeps();
+        for (Dist dist : this.distributions) {
+            URL jar;
+            try {
+                jar = dist.getJarRoot();
+                if (jar == null) {
+                    continue;
+                }
+            } catch (MalformedURLException ok) {
+                continue;
+            }
+            if (jar.equals(url)) {
+                List<FileObject> roots = new ArrayList<>();
+                for (Group d : dist.getContributingGroups()) {
+                    roots.add(d.srcDir);
+                    roots.add(d.srcGenDir);
+                }
+                return new ImmutableResult(roots.toArray(new FileObject[roots.size()]));
+            }
+        }
+        for (Group group : this.groups) {
+            if (group.binDir != null && group.binDir.toURL().equals(url)) {
+                return new ImmutableResult(group.srcDir, group.srcGenDir);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public SourceForBinaryQuery.Result findSourceRoots(URL url) {
+        return findSourceRoots2(url);
+    }
+
+    final Iterable<File> jdks() {
+        Set<File> jdks = new LinkedHashSet<>();
+        String home = System.getProperty("user.home");
+        if (home != null) {
+            File userEnv = new File(new File(new File(home), ".mx"), "env");
+            findJdksInEnv(jdks, userEnv);
+        }
+        FileObject suiteEnv = dir.getFileObject("mx." + dir.getNameExt() + "/env");
+        if (suiteEnv != null) {
+            findJdksInEnv(jdks, FileUtil.toFile(suiteEnv));
+        }
+
+        String javaHomeEnv = System.getenv("JAVA_HOME");
+        if (javaHomeEnv != null) {
+            jdks.add(new File(javaHomeEnv));
+        }
+        String javaHomeProp = System.getProperty("java.home");
+        if (javaHomeProp != null) {
+            jdks.add(new File(javaHomeProp));
+        }
+        return jdks;
+    }
+
+    private void findJdksInEnv(Set<File> jdks, File env) {
+        if (env == null || !env.isFile()) {
+            return;
+        }
+        try (final FileInputStream is = new FileInputStream(env)) {
+            Properties p = new Properties();
+            p.load(is);
+
+            String javaHome = p.getProperty("JAVA_HOME");
+            if (javaHome != null) {
+                jdks.add(new File(javaHome));
+            }
+
+            String extraJavaHomes = p.getProperty("EXTRA_JAVA_HOMES");
+            if (extraJavaHomes != null) {
+                for (String extraHome : extraJavaHomes.split(File.pathSeparator)) {
+                    jdks.add(new File(extraHome));
+                }
+            }
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+
+    @Override
+    public SourceLevelQueryImplementation2.Result getSourceLevel(FileObject fo) {
+        Group g = findGroup(fo);
+        if (g == null) {
+            return null;
+        }
+        return new SourceLevelQueryImplementation2.Result2() {
+            @Override
+            public SourceLevelQuery.Profile getProfile() {
+                return SourceLevelQuery.Profile.DEFAULT;
+            }
+
+            @Override
+            public String getSourceLevel() {
+                return g.getCompliance().getSourceLevel();
+            }
+
+            @Override
+            public void addChangeListener(ChangeListener cl) {
+            }
+
+            @Override
+            public void removeChangeListener(ChangeListener cl) {
+            }
+        };
+    }
+
+    @Override
+    public Set<? extends Project> getSubprojects() {
+        Set<Project> prjs = new HashSet<>();
+        for (SuiteSources imp : imported.values()) {
+            prjs.add(imp.prj);
+        }
+        return prjs;
+    }
+
+    @Override
+    public URL[] findUnitTests(FileObject fo) {
+        return new URL[0];
+    }
+
+    @Override
+    public URL[] findSources(FileObject fo) {
+        Group g = findGroup(fo);
+        return g == null ? new URL[0] : new URL[] { g.getRootFolder().toURL() };
+    }
+
+    static interface Dep extends Comparable<Dep> {
+        String getName();
+
+        Collection<String> depNames();
+
+        Collection<Dep> allDeps();
+
+        void setAllDeps(Collection<Dep> set);
+
+        @Override
+        public default int compareTo(Dep o) {
+            return getName().compareTo(o.getName());
+        }
+
+        SuiteSources owner();
+    }
+
+    final class Dist implements Dep, FlaggedClassPathImplementation {
+        final String name;
+        final MxDistribution dist;
+        Collection<Dep> allDeps;
+        private final PropertyChangeSupport support = new PropertyChangeSupport(this);
+        private Boolean exists;
+        private Collection<Group> groups;
+
+        public Dist(String name, MxDistribution dist) {
+            this.name = name;
+            this.dist = dist;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            Set<String> deps = new TreeSet<>();
+            deps.addAll(dist.distDependencies());
+            deps.addAll(dist.exclude());
+            return deps;
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return this.allDeps;
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            this.allDeps = set;
+        }
+
+        @Override
+        public String getName() {
+            return this.name;
+        }
+
+        @Override
+        public Set<ClassPath.Flag> getFlags() {
+            return exists ? Collections.emptySet() : Collections.singleton(ClassPath.Flag.INCOMPLETE);
+        }
+
+        private FileObject getJar(boolean ignore) {
+            if (SuiteSources.this.dir == null) {
+                return null;
+            }
+            FileObject dists = SuiteSources.this.dir.getFileObject("mxbuild/dists");
+            if (dists == null) {
+                return null;
+            }
+            List<FileObject> dist = Arrays.stream(dists.getChildren()).filter((fo) -> fo.isFolder() && fo.getName().startsWith("jdk")).collect(Collectors.toList());
+            dist.sort((fo1, fo2) -> fo2.getName().compareTo(fo1.getName()));
+            for (FileObject jdkDir : dist) {
+                FileObject jar = jdkDir.getFileObject(name.toLowerCase().replace("_", "-") + ".jar");
+                if (jar != null) {
+                    return jar;
+                }
+            }
+            FileObject jar = dists.getFileObject(name.toLowerCase().replace("_", "-") + ".jar");
+            if (jar != null) {
+                return jar;
+            }
+            return null;
+        }
+
+        @Override
+        public List<? extends PathResourceImplementation> getResources() {
+            computeTransitiveDeps();
+            FileObject jar = getJar(exists == null);
+            final boolean existsNow = jar != null && jar.isData();
+            if (exists == null) {
+                exists = existsNow;
+            } else {
+                if (exists != existsNow) {
+                    exists = existsNow;
+                    support.firePropertyChange(PROP_FLAGS, !exists, (boolean) exists);
+                }
+            }
+            if (jar != null) {
+                PathResourceImplementation res;
+                try {
+                    res = ClassPathSupport.createResource(getJarRoot());
+                    return Collections.singletonList(res);
+                } catch (MalformedURLException ex) {
+                    // OK
+                }
+            }
+            return Collections.emptyList();
+        }
+
+        private URL getJarRoot() throws MalformedURLException {
+            FileObject jar = getJar(true);
+            if (jar != null) {
+                return new URL("jar:" + jar.toURL() + "!/");
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener pl) {
+            support.addPropertyChangeListener(pl);
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener pl) {
+            support.removePropertyChangeListener(pl);
+        }
+
+        @Override
+        public SuiteSources owner() {
+            return SuiteSources.this;
+        }
+
+        private void computeSourceRoots(Map<String, Dep> collectedDeps) {
+            if (groups != null) {
+                return;
+            }
+            Set<Group> contributingGroups = new LinkedHashSet<>();
+            for (String d : this.dist.dependencies()) {
+                Dep dep = collectedDeps.get(d);
+                if (dep == null || dep.allDeps() == null) {
+                    continue;
+                }
+                for (Dep d2 : dep.allDeps()) {
+                    if (d2 instanceof Group) {
+                        contributingGroups.add((Group) d2);
+                    }
+                }
+            }
+            for (String d : this.dist.distDependencies()) {
+                final Dep anyDep = collectedDeps.get(d);
+                if (anyDep instanceof Dist) {
+                    Dist dep = (Dist) anyDep;
+                    dep.computeSourceRoots(collectedDeps);
+                    contributingGroups.removeAll(dep.getContributingGroups());
+                }
+            }
+            groups = contributingGroups;
+        }
+
+        public Collection<Group> getContributingGroups() {
+            return groups;
+        }
+
+        @Override
+        public String toString() {
+            return "Dist[name=" + name + "]";
+        }
+    }
+
+    final class Group implements SourceGroup, Dep, AnnotationProcessingQuery.Result,
+            Compliance.Provider {
+        private final String mxName;
+        private final MxProject mxPrj;
+        private final FileObject srcDir;
+        private final FileObject srcGenDir;
+        private final FileObject binDir;
+        private final String name;
+        private final String displayName;
+        private final Compliance compliance;
+        private ClassPath sourceCP;
+        private ClassPath cp;
+        private ClassPath processorPath;
+        private Collection<Dep> allDeps;
+
+        Group(String mxName, MxProject mxPrj, FileObject srcDir, FileObject srcGenDir, FileObject binDir, String name, String displayName) {
+            this.mxName = mxName;
+            this.mxPrj = mxPrj;
+            this.srcDir = srcDir;
+            this.srcGenDir = srcGenDir;
+            this.binDir = binDir;
+            this.name = name;
+            this.displayName = displayName;
+            this.compliance = Compliance.parse(mxPrj.javaCompliance());
+        }
+
+        @Override
+        public FileObject getRootFolder() {
+            return srcDir;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getDisplayName() {
+            return displayName;
+        }
+
+        @Override
+        public Icon getIcon(boolean opened) {
+            return null;
+        }
+
+        @Override
+        public Compliance getCompliance() {
+            return compliance;
+        }
+
+        @Override
+        public boolean contains(FileObject file) {
+            if (file == srcDir || file == srcGenDir || FileUtil.isParentOf(srcDir, file) || (srcGenDir != null && FileUtil.isParentOf(srcGenDir, file))) {
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener l) {
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener l) {
+        }
+
+        @Override
+        public String toString() {
+            return "SuiteSources.Group[name=" + name + ",rootFolder=" + srcDir + "]"; // NOI18N
+        }
+
+        ClassPath getSourceCP() {
+            computeTransitiveDeps();
+            return sourceCP;
+        }
+
+        ClassPath getCP() {
+            computeTransitiveDeps();
+            return cp;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            return mxPrj.dependencies();
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            allDeps = set;
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return allDeps;
+        }
+
+        private void computeClassPath(Map<String, Dep> transDeps) {
+            for (Dep d : transDeps.values()) {
+                d.owner().computeTransitiveDeps();
+            }
+
+            List<Group> arr = new ArrayList<>();
+            List<ClassPathImplementation> libs = new ArrayList<>();
+            processTransDep(transDeps.get(mxName), arr, libs);
+            cp = composeClassPath(arr, libs);
+            List<FileObject> roots = new ArrayList<>();
+            if (srcDir != null) {
+                roots.add(srcDir);
+            }
+            if (srcGenDir != null) {
+                roots.add(srcGenDir);
+            }
+            sourceCP = ClassPathSupport.createClassPath(roots.toArray(new FileObject[roots.size()]));
+
+            if (mxPrj.annotationProcessors().isEmpty()) {
+                processorPath = null;
+            } else {
+                List<Group> groups = new ArrayList<>();
+                List<ClassPathImplementation> jars = new ArrayList<>();
+                for (String dep : mxPrj.annotationProcessors()) {
+                    processTransDep(transDeps.get(dep), groups, jars);
+                }
+                processorPath = composeClassPath(groups, jars);
+            }
+        }
+
+        private void processTransDep(Dep dep, List<Group> addGroups, List<ClassPathImplementation> addJars) {
+            if (dep != null) {
+                dep.owner().computeTransitiveDeps();
+                for (Dep d : dep.allDeps()) {
+                    if (d == this) {
+                        continue;
+                    }
+                    d.owner().computeTransitiveDeps();
+                    if (d instanceof Group) {
+                        addGroups.add((Group) d);
+                    } else if (d instanceof ClassPathImplementation) {
+                        addJars.add((ClassPathImplementation) d);
+                    }
+                }
+            }
+        }
+
+        private ClassPath composeClassPath(List<Group> arr, List<ClassPathImplementation> libs) {
+            Set<FileObject> roots = new LinkedHashSet<>();
+            final int depsCount = arr.size();
+            for (int i = 0; i < depsCount; i++) {
+                final Group g = arr.get(i);
+                if (g.binDir != null) {
+                    roots.add(g.binDir);
+                }
+            }
+            ClassPath prjCp = ClassPathSupport.createClassPath(roots.toArray(new FileObject[0]));
+            if (!libs.isEmpty()) {
+                if (libs.size() == 1) {
+                    prjCp = ClassPathSupport.createProxyClassPath(prjCp,
+                                                                  ClassPathFactory.createClassPath(libs.get(0))
+                    );
+                } else {
+                    prjCp = ClassPathSupport.createProxyClassPath(prjCp,
+                                                                  ClassPathFactory.createClassPath(
+                                                                                  ClassPathSupport.createProxyClassPathImplementation(
+                                                                                                  libs.toArray(new ClassPathImplementation[0])
+                                                                                  )
+                                                                  )
+                    );
+                }
+            }
+            return prjCp;
+        }
+
+        ClassPath getProcessorCP() {
+            computeTransitiveDeps();
+            return processorPath;
+        }
+
+        @Override
+        public Set<? extends AnnotationProcessingQuery.Trigger> annotationProcessingEnabled() {
+            return EnumSet.of(AnnotationProcessingQuery.Trigger.ON_SCAN, AnnotationProcessingQuery.Trigger.IN_EDITOR);
+        }
+
+        @Override
+        public Iterable<? extends String> annotationProcessorsToRun() {
+            return null;
+        }
+
+        @Override
+        public URL sourceOutputDirectory() {
+            return srcGenDir == null ? null : srcGenDir.toURL();
+        }
+
+        @Override
+        public Map<? extends String, ? extends String> processorOptions() {
+            return Collections.emptyMap();
+        }
+
+        @Override
+        public void addChangeListener(ChangeListener l) {
+        }
+
+        @Override
+        public void removeChangeListener(ChangeListener l) {
+        }
+
+        @Override
+        public SuiteSources owner() {
+            return SuiteSources.this;
+        }
+    }
+
+    private class Library implements FlaggedClassPathImplementation, Dep {
+        final MxLibrary lib;
+        final PropertyChangeSupport support = new PropertyChangeSupport(this);
+        final String libName;
+        Collection<Dep> allDeps;
+        Boolean exists;
+
+        Library(String libName, MxLibrary lib) {
+            this.libName = libName;
+            this.lib = getOSSLibrary(lib);
+        }
+
+        final MxLibrary getOSSLibrary(MxLibrary lib) {
+            if (lib.sha1() == null && !lib.os_arch().isEmpty()) {
+                Map<String, MxLibrary.Arch> os_dep_libs = lib.os_arch();
+                String os = System.getProperty("os.name").toLowerCase();
+                for (Map.Entry<String, MxLibrary.Arch> entry : os_dep_libs.entrySet()) {
+                    if (os.contains(entry.getKey())) {
+                        return entry.getValue().amd64();
+                    }
+                }
+            }
+            return lib;
+        }
+
+        File getJar(boolean dumpIfMissing) {
+            File mxCache;
+            String cache = System.getenv("MX_CACHE_DIR");
+            if (cache != null) {
+                mxCache = new File(cache);
+            } else {
+                mxCache = new File(new File(new File(System.getProperty("user.home")), ".mx"), "cache");
+            }
+            int prefix = libName.indexOf(':');
+            final String simpleName = libName.substring(prefix + 1);
+
+            File simpleJar = new File(mxCache, simpleName + "_" + lib.sha1() + ".jar");
+            if (simpleJar.exists()) {
+                return simpleJar;
+            }
+            File dir = new File(mxCache, simpleName + "_" + lib.sha1());
+            File jar = new File(dir, simpleName.replace('_', '-').toLowerCase(Locale.ENGLISH) + ".jar");
+
+            if (dumpIfMissing && !jar.exists()) {
+                for (File f = jar;; f = f.getParentFile()) {
+                    if (!f.exists()) {
+                        LOG.log(Level.WARNING, "{0} does not exist", f);
+                    } else {
+                        StringBuilder sb = new StringBuilder();
+                        sb.append(f).append(" exists:\n");
+                        String[] kids = f.list();
+                        if (kids != null) {
+                            for (String n : kids) {
+                                sb.append("  ").append(n).append("\n");
+                            }
+                        }
+                        LOG.log(Level.INFO, sb.toString());
+                        break;
+                    }
+                }
+            }
+            return jar;
+        }
+
+        @Override
+        public String getName() {
+            return libName;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            return lib.dependencies();
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return allDeps;
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            this.allDeps = set;
+        }
+
+        @Override
+        public Set<ClassPath.Flag> getFlags() {
+            return exists ? Collections.emptySet() : Collections.singleton(ClassPath.Flag.INCOMPLETE);
+        }
+
+        @Override
+        public List<? extends PathResourceImplementation> getResources() {
+            File jar = getJar(exists == null);
+            if (exists == null) {
+                exists = jar.exists();
+            } else {
+                if (exists != jar.exists()) {
+                    exists = jar.exists();
+                    support.firePropertyChange(PROP_FLAGS, !exists, (boolean) exists);
+                }
+            }
+            PathResourceImplementation res;
+            try {
+                res = ClassPathSupport.createResource(new URL("jar:" + Utilities.toURI(jar).toURL() + "!/"));
+                return Collections.singletonList(res);
+            } catch (MalformedURLException ex) {
+                return Collections.emptyList();
+            }
+        }
+
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener pl) {
+            support.addPropertyChangeListener(pl);
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener pl) {
+            support.removePropertyChangeListener(pl);
+        }
+
+        @Override
+        public SuiteSources owner() {
+            return SuiteSources.this;
+        }
+    }
+
+    private class JdkLibrary extends Library {
+        JdkLibrary(String libName, MxLibrary lib) {
+            super(libName, lib);
+        }
+
+        @Override
+        File getJar(boolean dumpIfMissing) {
+            File first = null;
+            for (File jdk : jdks()) {
+                File jre = new File(jdk, "jre");
+                File jrePath = new File(jre, lib.path().replace('/', File.separatorChar));
+                if (jrePath.exists()) {
+                    return jrePath;
+                }
+
+                if (first == null) {
+                    first = jrePath;
+                }
+
+                File jdkPath = new File(jdk, lib.path().replace('/', File.separatorChar));
+
+                if (jdkPath.exists()) {
+                    return jdkPath;
+                }
+
+            }
+
+            if (dumpIfMissing) {
+                for (File jdk : jdks()) {
+                    File libPath = new File(jdk, lib.path().replace('/', File.separatorChar));
+                    if (!libPath.exists()) {
+                        LOG.log(Level.WARNING, "{0} does not exist", libPath);
+                    } else {
+                        StringBuilder sb = new StringBuilder();
+                        sb.append(libPath).append(" exists:\n");
+                        String[] kids = libPath.list();
+                        if (kids != null) {
+                            for (String n : kids) {
+                                sb.append("  ").append(n).append("\n");
+                            }
+                        }
+                        LOG.log(Level.INFO, sb.toString());
+                        break;
+                    }
+                }
+            }
+            return first;
+        }
+
+        @Override
+        public String getName() {
+            return libName;
+        }
+
+        @Override
+        public Collection<String> depNames() {
+            return lib.dependencies();
+        }
+
+        @Override
+        public Collection<Dep> allDeps() {
+            return allDeps;
+        }
+
+        @Override
+        public void setAllDeps(Collection<Dep> set) {
+            this.allDeps = set;
+        }
+
+        @Override
+        public Set<ClassPath.Flag> getFlags() {
+            return exists ? Collections.emptySet() : Collections.singleton(ClassPath.Flag.INCOMPLETE);
+        }
+
+        @Override
+        public List<? extends PathResourceImplementation> getResources() {

Review comment:
       Can be somehow shared with `Library` implementation ?




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@netbeans.apache.org
For additional commands, e-mail: notifications-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists