From 92a63e1494d7e24a9f36cf639257b181080a535f Mon Sep 17 00:00:00 2001 From: mattirn Date: Thu, 8 Apr 2021 18:31:19 +0200 Subject: [PATCH] Groovy REPL: manage tab completions of dynamically loaded jars --- .../java/org/jline/script/GroovyEngine.java | 103 ++++++++-------- .../java/org/jline/script/PackageHelper.java | 112 ++++++++++++++---- 2 files changed, 140 insertions(+), 75 deletions(-) diff --git a/groovy/src/main/java/org/jline/script/GroovyEngine.java b/groovy/src/main/java/org/jline/script/GroovyEngine.java index 1addd9d81..c60536e80 100644 --- a/groovy/src/main/java/org/jline/script/GroovyEngine.java +++ b/groovy/src/main/java/org/jline/script/GroovyEngine.java @@ -78,7 +78,8 @@ public enum Format {JSON, GROOVY, NONE} , Pattern.DOTALL); private static final Pattern PATTERN_TRAIT_DEF = Pattern.compile("^trait\\s+(" + REGEX_VAR + ")\\s*(\\{.*?})(|\n)$" , Pattern.DOTALL); - private static final Pattern PATTERN_CLASS = Pattern.compile("(.*?)\\.([A-Z_].*)"); + private static final String REGEX_CLASS = "(.*?)\\.([A-Z_].*)"; + private static final Pattern PATTERN_CLASS = Pattern.compile(REGEX_CLASS); private static final String REGEX_PACKAGE = "([a-z][a-z_0-9]*\\.)*"; private static final String REGEX_CLASS_NAME = "[A-Z_](\\w)*"; private static final Pattern PATTERN_LOAD_CLASS = Pattern.compile("(import\\s+|new\\s+|\\s*)?(" @@ -106,7 +107,7 @@ public GroovyEngine() { this.sharedData = new Binding(); shell = new GroovyShell(sharedData); for (String s : DEFAULT_IMPORTS) { - addToNameClass(s, defaultNameClass, shell); + addToNameClass(s, defaultNameClass); } nameClass = new HashMap<>(defaultNameClass); } @@ -218,10 +219,6 @@ public Object execute(File script, Object[] args) throws Exception { return s.run(); } - private static Set> classesForPackage(String pckgname) throws ClassNotFoundException { - return classesForPackage(pckgname, null); - } - private static Set> classesForPackage(String pckgname, GroovyShell shell) throws ClassNotFoundException { String name = pckgname; Matcher matcher = PATTERN_CLASS.matcher(name); @@ -232,27 +229,43 @@ private static Set> classesForPackage(String pckgname, GroovyShell shel if (out.isEmpty()) { out.addAll(JrtJavaBasePackages.getClassesForPackage(name)); } - if (out.isEmpty() && shell != null && name.endsWith(".*")) { - name = name.substring(0, name.length() - 1); - Set classNames = Helpers.nextFileDomain(name); - for (String c : classNames) { - if (Character.isUpperCase(c.charAt(0))) { - try { - out.add((Class)executeStatement(shell, new HashMap<>(), name + c + ".class")); - } catch (Exception ignore) { + if (out.isEmpty() && shell != null) { + if (name.endsWith(".*")) { + name = name.substring(0, name.length() - 1); + Set classNames = Helpers.nextFileDomain(name); + for (String c : classNames) { + if (Character.isUpperCase(c.charAt(0))) { + try { + out.add((Class) executeStatement(shell, new HashMap<>(), name + c + ".class")); + } catch (Exception ignore) { + } } } + } else if (name.endsWith(".**")) { + out.addAll(new HashSet<>(PackageHelper.getClassesForPackage(name, shell.getClassLoader() + , n -> + { + if (n.contains("-")) { + return null; + } + Class o = null; + try { + o = (Class) shell.evaluate(n + ".class"); + } catch (Exception | Error ignore) { + } + return o; + }))); } } return out; } private void addToNameClass(String name) { - addToNameClass(name, nameClass, shell); + addToNameClass(name, nameClass); } - private static void addToNameClass(String name, Map> nameClass, GroovyShell shell) { + private void addToNameClass(String name, Map> nameClass) { try { if (name.endsWith(".*")) { for (Class c : classesForPackage(name, shell)) { @@ -303,6 +316,7 @@ public Object execute(String statement) throws Exception { Matcher matcher = PATTERN_CLASS_DEF.matcher(statement); matcher.matches(); classes.put(matcher.group(1), matcher.group(2)); + addToNameClass(matcher.group(1)); } else if (PATTERN_TRAIT_DEF.matcher(statement).matches()) { Matcher matcher = PATTERN_TRAIT_DEF.matcher(statement); matcher.matches(); @@ -409,6 +423,9 @@ private void refreshNameClass() { for (String name : imports.keySet()) { addToNameClass(name); } + for (String name : classes.keySet()) { + addToNameClass(name); + } } private void del(String var) { @@ -564,9 +581,9 @@ private static Set loadedPackages() { return out; } - private static Set names(String domain) { + private static Set names(String domain, Collection packages) { Set out = new HashSet<>(); - for (String p : loadedPackages()) { + for (String p : packages) { if (p.startsWith(domain)) { int idx = p.indexOf('.', domain.length()); if (idx < 0) { @@ -674,11 +691,11 @@ private static Set nextFileDomain(String domain) { return out; } - public static Set nextDomain(String domain, CandidateType type) { - return nextDomain(domain, new AccessRules(), type); + public static Set nextDomain(String domain, CandidateType type, GroovyShell shell) { + return nextDomain(domain, new AccessRules(), type, shell); } - public static Set nextDomain(String domain, AccessRules access, CandidateType type) { + public static Set nextDomain(String domain, AccessRules access, CandidateType type, GroovyShell shell) { Set out = new HashSet<>(); if (domain.isEmpty()) { for (String p : loadedPackages()) { @@ -686,11 +703,15 @@ public static Set nextDomain(String domain, AccessRules access, Candidat } out.addAll(nextFileDomain(null)); } else if ((domain.split("\\.")).length < 2) { - out = names(domain); + out = names(domain, loadedPackages()); + out.addAll(names(domain, PackageHelper.getClassNamesForPackage(domain, shell.getClassLoader()))); out.addAll(nextFileDomain(domain)); } else { try { - for (Class c : classesForPackage(domain)) { + if (!domain.matches(REGEX_CLASS)) { + out = names(domain, PackageHelper.getClassNamesForPackage(domain, shell.getClassLoader())); + } + for (Class c : classesForPackage(domain, shell)) { try { if ((!Modifier.isPublic(c.getModifiers()) && !access.allClasses) || c.getCanonicalName() == null) { continue; @@ -723,7 +744,8 @@ && noStaticFields(c, access.allFields))) { if (Log.isDebugEnabled()) { e.printStackTrace(); } - out = names(domain); + out.addAll(names(domain, loadedPackages())); + out.addAll(names(domain, PackageHelper.getClassNamesForPackage(domain, shell.getClassLoader()))); } } return out; @@ -862,7 +884,7 @@ public void complete(LineReader reader, ParsedLine commandLine, List curBuf = buffer.substring(0, lastDelim + 1); } Helpers.doCandidates(candidates - , Helpers.nextDomain(curBuf, new AccessRules(groovyEngine.groovyOptions()), type) + , Helpers.nextDomain(curBuf, new AccessRules(groovyEngine.groovyOptions()), type, groovyEngine.shell) , curBuf, type); } @@ -931,7 +953,7 @@ public void complete(LineReader reader, ParsedLine commandLine, List } catch (Exception e) { String param = wordbuffer.substring(0, idx + 1); Helpers.doCandidates(candidates - , Helpers.nextDomain(param, CandidateType.CONSTRUCTOR) + , Helpers.nextDomain(param, CandidateType.CONSTRUCTOR, inspector.shell) , param, CandidateType.CONSTRUCTOR); } } else { @@ -992,7 +1014,7 @@ public void complete(LineReader reader, ParsedLine commandLine, List } finally { param = wordbuffer.substring(eqsep + 1, varsep + 1); Helpers.doCandidates(candidates - , Helpers.nextDomain(param, CandidateType.STATIC_METHOD) + , Helpers.nextDomain(param, CandidateType.STATIC_METHOD, inspector.shell) , curBuf, CandidateType.PACKAGE); } } @@ -1186,7 +1208,7 @@ private static class Inspector { public Inspector(GroovyEngine groovyEngine) { this.imports = groovyEngine.imports; - this.nameClass = new HashMap<>(groovyEngine.nameClass); + this.nameClass = groovyEngine.nameClass; this.canonicalNames = groovyEngine.groovyOption(CANONICAL_NAMES, false); this.nanorcSyntax = groovyEngine.groovyOption(NANORC_SYNTAX, DEFAULT_NANORC_SYNTAX); this.noSyntaxCheck = groovyEngine.groovyOption(NO_SYNTAX_CHECK, false); @@ -1201,7 +1223,7 @@ public Inspector(GroovyEngine groovyEngine) { sharedData.setVariable(entry.getKey(), obj); } groovyEngine.getObjectCloner().purgeCache(); - shell = new GroovyShell(sharedData); + shell = new GroovyShell(groovyEngine.shell.getClassLoader(), sharedData); try { File file = OSUtils.IS_WINDOWS ? new File("NUL") : new File("/dev/null"); OutputStream outputStream = new FileOutputStream(file); @@ -1216,29 +1238,6 @@ public Inspector(GroovyEngine groovyEngine) { sharedData.setVariable(entry.getKey(), execute("{" + m.group(1) + "->" + m.group(2) + "}")); } } - for (Map.Entry entry : imports.entrySet()) { - try { - executeStatement(shell, new HashMap<>(), entry.getValue()); - addToNameClass(entry.getKey(), nameClass, shell); - } catch (Exception e) { - if (Log.isDebugEnabled()) { - e.printStackTrace(); - } - } - } - for (Map.Entry entry : groovyEngine.traits.entrySet()) { - try { - executeStatement(shell, imports, "trait " + entry.getKey() + " " + entry.getValue()); - } catch (Exception ignore) { - } - } - for (Map.Entry entry : groovyEngine.classes.entrySet()) { - try { - executeStatement(shell, imports, "class " + entry.getKey() + " " + entry.getValue()); - addToNameClass(entry.getKey(), nameClass, shell); - } catch (Exception ignore) { - } - } } public Object getInvolvedObject() { diff --git a/groovy/src/main/java/org/jline/script/PackageHelper.java b/groovy/src/main/java/org/jline/script/PackageHelper.java index d307b5287..be99379fd 100644 --- a/groovy/src/main/java/org/jline/script/PackageHelper.java +++ b/groovy/src/main/java/org/jline/script/PackageHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2020, the original author or authors. + * Copyright (c) 2002-2021, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; +import java.util.function.Function; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** @@ -26,7 +27,7 @@ * */ public class PackageHelper { - private enum ClassesToScann {ALL, PACKAGE_ALL, PACKAGE_CLASS}; + private enum ClassesToScann {ALL, PACKAGE_ALL, PACKAGE_CLASS} /** * Private helper method * @@ -40,8 +41,7 @@ private enum ClassesToScann {ALL, PACKAGE_ALL, PACKAGE_CLASS}; * @param scann * determinate which classes will be added */ - private static void checkDirectory(File directory, String pckgname, - List> classes, ClassesToScann scann) { + private static void checkDirectory(File directory, String pckgname, List> classes, ClassesToScann scann) { File tmpDirectory; if (directory.exists() && directory.isDirectory()) { @@ -61,8 +61,8 @@ private static void checkDirectory(File directory, String pckgname, /** * Private helper method. * - * @param connection - * the connection to the jar + * @param jarFile + * the jar file * @param pckgname * the package name to search for * @param classes @@ -73,10 +73,9 @@ private static void checkDirectory(File directory, String pckgname, * @throws IOException * if it can't correctly read from the jar file. */ - private static void checkJarFile(JarURLConnection connection, - String pckgname, List> classes, ClassesToScann scann) + private static void checkJarFile(final JarFile jarFile, String pckgname, List> classes, List names + , ClassesToScann scann, Function> classResolver) throws IOException { - final JarFile jarFile = connection.getJarFile(); final Enumeration entries = jarFile.entries(); String name; @@ -88,21 +87,77 @@ private static void checkJarFile(JarURLConnection connection, String namepckg = name.substring(0, name.lastIndexOf(".")); if (pckgname.equals(namepckg) && ((scann == ClassesToScann.PACKAGE_CLASS && !name.contains("$")) || scann == ClassesToScann.PACKAGE_ALL)) { - addClass(name, classes); + if (names == null) { + addClass(name, classes, classResolver); + } else { + names.add(name); + } } } else if (name.contains(pckgname)) { - addClass(name, classes); + if (names == null) { + addClass(name, classes, classResolver); + } else { + names.add(name); + } } } } } private static void addClass(String className, List> classes) { + addClass(className, classes, PackageHelper::classResolver); + } + + private static void addClass(String className, List> classes, Function> classResolver) { + Class clazz = classResolver.apply(className); + if (clazz != null) { + classes.add(clazz); + } + } + + private static Class classResolver(String name) { + Class out = null; try { - classes.add(Class.forName(className)); - } catch (Exception|Error e) { - // ignore + out = Class.forName(name); + } catch (Exception|Error ignore) { + } + return out; + } + + /** + * Attempts to list all the class names in the specified package as determined + * by the Groovy class loader classpath + * + * @param pckgname + * the package name to search + * @param classLoader Groovy class loader + * @return a list of class names that exist within that package + */ + public static List getClassNamesForPackage(String pckgname, ClassLoader classLoader) { + List out = new ArrayList<>(); + try { + getClassesForPackage(pckgname, classLoader, out, null); + } catch (Exception ignore) { + } + return out; + } + + /** + * Attempts to list all the classes in the specified package as determined + * by the Groovy class loader classpath + * + * @param pckgname + * the package name to search + * @param classLoader Groovy class loader + * @param classResolver resolve class from class name + * @return a list of classes that exist within that package + * @throws ClassNotFoundException + * if something went wrong + */ + public static List> getClassesForPackage(String pckgname, ClassLoader classLoader + , Function> classResolver) throws ClassNotFoundException { + return getClassesForPackage(pckgname, classLoader, null, classResolver); } /** @@ -116,6 +171,15 @@ private static void addClass(String className, List> classes) { * if something went wrong */ public static List> getClassesForPackage(String pckgname) throws ClassNotFoundException { + final ClassLoader cld = Thread.currentThread().getContextClassLoader(); + if (cld == null) { + throw new ClassNotFoundException("Can't get class loader."); + } + return getClassesForPackage(pckgname, cld, null, PackageHelper::classResolver); + } + + private static List> getClassesForPackage(String pckgname, final ClassLoader classLoader, List names + , Function> classResolver) throws ClassNotFoundException { List> classes = new ArrayList<>(); ClassesToScann scann = ClassesToScann.ALL; if (pckgname.endsWith(".*")) { @@ -127,24 +191,26 @@ public static List> getClassesForPackage(String pckgname) throws ClassN } try { - final ClassLoader cld = Thread.currentThread().getContextClassLoader(); - if (cld == null) { - throw new ClassNotFoundException("Can't get class loader."); - } - final Enumeration resources = cld.getResources(pckgname.replace('.', '/')); + final Enumeration resources = classLoader.getResources(pckgname.replace('.', '/')); URLConnection connection; for (URL url; resources.hasMoreElements() - && ((url = resources.nextElement()) != null);) { + && ((url = resources.nextElement()) != null); ) { try { connection = url.openConnection(); if (connection instanceof JarURLConnection) { - checkJarFile((JarURLConnection) connection, pckgname, classes, scann); + checkJarFile(((JarURLConnection) connection).getJarFile(), pckgname, classes, names, scann, classResolver); } else if (connection.getClass().getCanonicalName().equals("sun.net.www.protocol.file.FileURLConnection")) { try { - checkDirectory( - new File(URLDecoder.decode(url.getPath(), "UTF-8")), pckgname, classes, scann); + File file = new File(URLDecoder.decode(url.getPath(), "UTF-8")); + if (file.exists()) { + if (file.isDirectory()) { + checkDirectory(file, pckgname, classes, scann); + } else if (file.getName().endsWith(".jar")) { + checkJarFile(new JarFile(file), pckgname, classes, names, scann, classResolver); + } + } } catch (final UnsupportedEncodingException ex) { throw new ClassNotFoundException( pckgname