package bluej.utility;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import bluej.Config;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ScanResult;
import javafx.application.Platform;
import nu.xom.Attribute;
import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.ParsingException;
import bluej.Boot;
import bluej.parser.ImportedTypeCompletion;
import bluej.pkgmgr.JavadocResolver;
import bluej.pkgmgr.Project;
import bluej.stride.generic.AssistContentThreadSafe;
import threadchecker.OnThread;
import threadchecker.Tag;
| A class which manages scanning the classpath for available imports.
|
public class ImportScanner
{
private final Object monitor = new Object();
private CompletableFuture<RootPackageInfo> root;
private final Project project;
public ImportScanner(Project project)
{
this.project = project;
}
| For each package that we scan, we hold one PackgeInfo with details on the items
| in that package, and links to any subpackages. Thus the root of this tree
| is a single PackageInfo representing the unnamed package (held in this.root).
|
private class PackageInfo
{
public final HashMap<String, AssistContentThreadSafe> types = new HashMap<>();
public final HashMap<String, PackageInfo> subPackages = new HashMap<>();
protected void addClass(Iterator<String> packageIdents, String name)
{
if (packageIdents.hasNext())
{
String ident = packageIdents.next();
PackageInfo subPkg = subPackages.get(ident);
if (subPkg == null)
{
subPkg = new PackageInfo();
subPackages.put(ident, subPkg);
}
subPkg.addClass(packageIdents, name);
}
else
{
types.put(name, null);
}
}
| Gets the type for the given name from this package, either using cached copy
| or by calculating it on demand.
|
| @param prefix The package name, ending in ".", e.g. "java.lang."
|* @param name The unqualified type name, e.g. "String".
*/
@OnThread(Tag.Worker)
private AssistContentThreadSafe getType(String prefix, String name, JavadocResolver javadocResolver)
{
return types.computeIfAbsent(name, s -> {
|
|// To safely get an AssistContentThreadSafe, we must create one from the FXPlatform thread.
|
|// So we need to hop across to the FXPlatform thread. Because we are an arbitrary background
|
|// worker thread, it is safe to use wait afterwards; without risk of deadlock:
|
|try
|
|{
|
|CompletableFuture<AssistContentThreadSafe> f = new CompletableFuture<>();
|
|Platform.runLater(() -> {
|
|Class<?> c = project.loadClass(prefix + s);
|
|// This happens reasonably often while the user is typing in an import in Stride,
|
|// so it's not necessarily a bug:
|
|if (c == null)
|
|{
|
|f.complete(null);
|
|}
|
|else
|
|{
|
|f.complete(new AssistContentThreadSafe(new ImportedTypeCompletion(c, javadocResolver)));
|
|}
|
|});
|
|return f.get();
|
|}
|
|catch (Exception e)
|
|{
|
|Debug.reportError(e);
|
|return null;
|
|}
|
|});
|
|}
|
|/**
| Gets types arising from a given import directive in the source code.
|
| @param prefix The prefix of this package, ending in ".". E.g. for the java
|* package, we would be passed "java."
* @param idents The next in the sequence of identifiers. E.g. if we are the java package
* we might be passed {}lang", "String"}. The final item may be an asterisk,
* e.g. {}lang", "*"}, in which case we return all types. Otherwise we will
* return an empty list (if the type is not found), or a singleton list.
* @return The
*/
@OnThread(Tag.Worker)
public List<AssistContentThreadSafe> getImportedTypes(String prefix, Iterator<String> idents, JavadocResolver javadocResolver)
|
|{
|
|if (!idents.hasNext())
|
|return Collections.emptyList();
|
|String s = idents.next();
|
|if (s.equals("*"))
{
// Return all types:
// Take a copy in case it causes problems that getType modifies the collection
Collection<String> typeNames = new ArrayList<>(types.keySet());
|
|return typeNames.stream().map(t -> getType(prefix, t, javadocResolver)).filter(ac -> ac != null).collect(Collectors.toList());
|
|}
|
|else if (idents.hasNext())
|
|{
|
|// Still more identifiers to follow. Look for package:
|
|if (subPackages.containsKey(s))
|
|return subPackages.get(s).getImportedTypes(prefix + s + ".", idents, javadocResolver);
else{ return Collections.emptyList();
}
}
else
{
// Final identifier, not an asterisk, look for class:
|
|AssistContentThreadSafe ac = getType(prefix, s, javadocResolver);
|
|if (ac != null)
|
|return Collections.singletonList(ac);
|
|else{ return Collections.emptyList();
|
|}
|
|}
|
|}
|
|public void addTypes(PackageInfo from)
|
|{
|
|types.putAll(from.types);
|
|from.subPackages.forEach((name, pkg) -> {
|
|subPackages.putIfAbsent(name, new PackageInfo());
|
|subPackages.get(name).addTypes(pkg);
|
|});
|
|}
|
|}
|
|// PackageInfo, but for the root type.
|
|private class RootPackageInfo extends PackageInfo
|
|{
|
|// Adds fully qualified class name to type list.
|
|public void addClass(String name)
|
|{
|
|String[] splitParts = name.split("\\.", -1);
addClass(Arrays.asList(Arrays.copyOf(splitParts, splitParts.length - 1)).iterator(), splitParts[splitParts.length - 1]);
}
}
@OnThread(Tag.Any)
private CompletableFuture<? extends PackageInfo> getRoot()
|
|{
|
|synchronized (monitor)
|
|{
|
|// Already started calculating:
|
|if (root != null)
|
|{
|
|return root;
|
|}
|
|else
|
|{
|
|// Start calculating:
|
|root = new CompletableFuture<>();
|
|// We don't use runBackground because we don't want to end up
|
|// behind other callers of getRoot in the queue (this can
|
|// cause a deadlock because there are no background threads
|
|// available, as they are all blocked waiting for this
|
|// future to complete):
|
|new Thread() { public void run()
|
|{
|
|RootPackageInfo rootPkg = findAllTypes();
|
|try
|
|{
|
|loadCachedImports(rootPkg);
|
|}
|
|finally
|
|{
|
|root.complete(rootPkg);
|
|}
|
|}
|
|}.start();
|
|return root;
|
|}
|
|}
|
|}
|
|/**
| Given an import source (e.g. "java.lang.String", "java.util.*"), finds all the
|* types that will be imported.
*
* If the one-time on-load import scanning has not finished yet, this method will
* wait until it has. Hence you should call it from a worker thread, not from a
| GUI thread where it could block the GUI for a long time.
|
@OnThread(Tag.Worker)
public List getImportedTypes(String importSrc)
{
try
{
return getRoot().get().getImportedTypes("", Arrays.asList(importSrc.split("\\.", -1)).iterator(), project.getJavadocResolver());
}
catch (InterruptedException | ExecutionException e)
{
Debug.reportError("Exception in getImportedTypes", e);
return Collections.emptyList();
}
}
| Gets a list of ClassGraph items which can be used to find available classes.
|
| Because of the way ClassGraph works, one item is not enough for all classes;
| we use one for system classes and one for user classes.
|
@OnThread(Tag.Worker)
private List getClassloaderConfig()
{
ArrayList<ClassLoader> cl = new ArrayList<>();
try
{
CompletableFuture<ClassLoader> projectClassLoader = new CompletableFuture<>();
Platform.runLater(() -> {
projectClassLoader.complete(project.getClassLoader());
});
cl.add(projectClassLoader.get());
}
catch (InterruptedException | ExecutionException e)
{
Debug.reportError(e);
}
cl.add(new URLClassLoader(Boot.getInstance().getRuntimeUserClassPath()));
ClassGraph userClassGraph = new ClassGraph()
.overrideClassLoaders(cl.toArray(new ClassLoader[0]))
.blacklistPackages("bluej.*");
ClassGraph systemClassGraph = new ClassGraph()
.enableSystemPackages()
.whitelistPackages("java.*", "javax.*", "javafx.*");
return List.of(
userClassGraph.enableClassInfo(),
systemClassGraph.enableClassInfo()
);
}
| Gets a package-tree structure which includes all packages and class-names
| on the current class-path (by scanning all JARs and class-files on the path).
|
| @return A package-tree structure with all class names present, but not any further
| details about the classes.
|
@OnThread(Tag.Worker)
private RootPackageInfo findAllTypes()
{
List<ClassGraph> classGraphs = getClassloaderConfig();
RootPackageInfo r = new RootPackageInfo();
if (classGraphs != null)
{
final int threads = Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
for (ClassGraph classGraph : classGraphs)
{
try (ScanResult result = classGraph.scan(threads))
{
for (ClassInfo c : result.getAllClasses())
{
r.addClass(c.getName());
}
}
catch (Throwable t)
{
Debug.reportError(t);
}
}
}
return r;
}
| Starts scanning for available importable types from the classpath.
| Will operate in a background thread.
|
public void startScanning()
{
getRoot();
}
| Saves all java.** type information to a cache
|
public void saveCachedImports()
{
if (getRoot().isDone())
{
Element cache = new Element("packages");
cache.addAttribute(new Attribute("javaHome", getJavaHome()));
cache.addAttribute(new Attribute("version", getVersion()));
try
{
PackageInfo javaPkg = getRoot().get().subPackages.get("java");
if (javaPkg != null)
{
cache.appendChild(toXML(javaPkg, "java"));
FileOutputStream os = new FileOutputStream(getImportCachePath());
Utility.serialiseCodeTo(cache, os);
os.close();
}
}
catch (InterruptedException | ExecutionException | IOException e)
{
Debug.reportError(e);
}
}
}
| Version of the currently running software
|
private static String getVersion()
{
return Config.isGreenfoot() ? Boot.GREENFOOT_VERSION : Boot.BLUEJ_VERSION;
}
| Java home directory
|
private static String getJavaHome()
{
return Boot.getInstance().getJavaHome().getAbsolutePath();
}
| Import cache path to save to/load from
|
private static File getImportCachePath()
{
return new File(Config.getUserConfigDir(), "import-cache.xml");
}
| Loads cached (java.**) imports into the given root package, if possible.
|
public void loadCachedImports(PackageInfo rootPkg)
{
try {
Document xml = new Builder().build(getImportCachePath());
Element packagesEl = xml.getRootElement();
if (!packagesEl.getLocalName().equals("packages"))
return;
if (!getJavaHome().equals(packagesEl.getAttributeValue("javaHome")) || !getVersion().equals(packagesEl.getAttributeValue("version")))
return;
for (int i = 0; i < packagesEl.getChildElements().size(); i++)
{
fromXML(packagesEl.getChildElements().get(i), rootPkg);
}
}
catch (ParsingException | IOException e) {
Debug.message(e.getClass().getName() + " while reading import cache: " + e.getMessage());
}
}
| Loads the given XML package item and puts it into the given parent package.
|
private void fromXML(Element pkgEl, PackageInfo addToParent)
{
String name = pkgEl.getAttributeValue("name");
if (name == null)
return;
PackageInfo loadPkg = new PackageInfo();
for (int i = 0; i < pkgEl.getChildElements().size(); i++)
{
Element el = pkgEl.getChildElements().get(i);
if (el.getLocalName().equals("package"))
{
fromXML(el, loadPkg);
}
else
{
AssistContentThreadSafe acts = new AssistContentThreadSafe(el);
String nameWithoutPackage = (acts.getDeclaringClass() == null ? "" : acts.getDeclaringClass() + "$") + acts.getName();
loadPkg.types.put(nameWithoutPackage, acts);
}
}
addToParent.subPackages.putIfAbsent(name, new PackageInfo());
addToParent.subPackages.get(name).addTypes(loadPkg);
}
| Save the given PackageInfo item (with package name) to XML
|
private static Element toXML(PackageInfo pkg, String name)
{
Element el = new Element("package");
el.addAttribute(new Attribute("name", name));
pkg.types.values().forEach(acts -> {if (acts != null) el.appendChild(acts.toXML());
});
pkg.subPackages.forEach((subName, subPkg) -> el.appendChild(toXML(subPkg, subName)));
return el;
}
}
top,
use,
map,
class ImportScanner
. ImportScanner
top,
use,
map,
class ImportScanner . PackageInfo
. addClass
. getImportedTypes
. getClassloaderConfig
. findAllTypes
. startScanning
. saveCachedImports
. getVersion
. getJavaHome
. getImportCachePath
. loadCachedImports
. fromXML
. toXML
360 neLoCode
+ 126 LoComm