8134255: Implement tab-completion for java package prefixes and package names
authorsundar
Sun, 23 Aug 2015 10:02:14 +0530
changeset 32314 8f7d23d3b1ad
parent 32313 11b284f8c4c6
child 32315 aa64591f534b
8134255: Implement tab-completion for java package prefixes and package names Reviewed-by: attila, mhaupt
nashorn/samples/classes.js
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/EditObject.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/NashornCompleter.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PackagesHelper.java
nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PropertiesHelper.java
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptEnvironment.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/samples/classes.js	Sun Aug 23 10:02:14 2015 +0530
@@ -0,0 +1,47 @@
+// Usage: jjs classes.js [ -- <java_package_name > ]
+
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ *   - Neither the name of Oracle nor the names of its
+ *     contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// print all Java classes of the given package and its subpackages.
+var pkg = arguments.length > 0? arguments[0] : "java.lang";
+
+with (new JavaImporter(javax.tools, java.util)) {
+    var compiler = ToolProvider.systemJavaCompiler;
+    var fm = compiler.getStandardFileManager(null, null, null);
+    var kinds = EnumSet.of(JavaFileObject.Kind.CLASS);
+    var loc = StandardLocation.PLATFORM_CLASS_PATH;
+    var itr = fm.list(loc, pkg, kinds, true).iterator();
+    while(itr.hasNext()) {
+        print(fm.inferBinaryName(loc, itr.next()));
+    }
+    fm.close();
+}
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/EditObject.java	Fri Aug 21 18:01:23 2015 +0530
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/EditObject.java	Sun Aug 23 10:02:14 2015 +0530
@@ -25,6 +25,9 @@
 
 package jdk.nashorn.tools.jjs;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.function.Consumer;
 import jdk.nashorn.api.scripting.AbstractJSObject;
 import jdk.nashorn.internal.runtime.JSType;
@@ -35,6 +38,13 @@
  * for editing and evaluating scripts from it.
  */
 final class EditObject extends AbstractJSObject {
+    private static final Set<String> props;
+    static {
+        final HashSet<String> s = new HashSet<>();
+        s.add("editor");
+        props = Collections.unmodifiableSet(s);
+    }
+
     private final Consumer<String> errorHandler;
     private final Consumer<String> evaluator;
     private final Console console;
@@ -61,6 +71,11 @@
     }
 
     @Override
+    public Set<String> keySet() {
+        return props;
+    }
+
+    @Override
     public Object getMember(final String name) {
         if (name.equals("editor")) {
             return editor;
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java	Fri Aug 21 18:01:23 2015 +0530
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/Main.java	Sun Aug 23 10:02:14 2015 +0530
@@ -49,6 +49,8 @@
 public final class Main extends Shell {
     private Main() {}
 
+    static final boolean DEBUG = Boolean.getBoolean("nashorn.jjs.debug");
+
     // file where history is persisted.
     private static final File HIST_FILE = new File(new File(System.getProperty("user.home")), ".jjs.history");
 
@@ -100,7 +102,8 @@
         final PrintWriter err = context.getErr();
         final Global oldGlobal = Context.getGlobal();
         final boolean globalChanged = (oldGlobal != global);
-        final Completer completer = new NashornCompleter(context, global, this);
+        final PropertiesHelper propsHelper = new PropertiesHelper(env._classpath);
+        final Completer completer = new NashornCompleter(context, global, this, propsHelper);
 
         try (final Console in = new Console(System.in, System.out, HIST_FILE, completer)) {
             if (globalChanged) {
@@ -161,6 +164,13 @@
             if (globalChanged) {
                 Context.setGlobal(oldGlobal);
             }
+            try {
+                propsHelper.close();
+            } catch (final Exception exp) {
+                if (DEBUG) {
+                    exp.printStackTrace();
+                }
+            }
         }
 
         return SUCCESS;
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/NashornCompleter.java	Fri Aug 21 18:01:23 2015 +0530
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/NashornCompleter.java	Sun Aug 23 10:02:14 2015 +0530
@@ -55,12 +55,15 @@
     private final Context context;
     private final Global global;
     private final PartialParser partialParser;
+    private final PropertiesHelper propsHelper;
     private final Parser parser;
 
-    NashornCompleter(final Context context, final Global global, final PartialParser partialParser) {
+    NashornCompleter(final Context context, final Global global,
+            final PartialParser partialParser, final PropertiesHelper propsHelper) {
         this.context = context;
         this.global = global;
         this.partialParser = partialParser;
+        this.propsHelper = propsHelper;
         this.parser = Parser.create();
     }
 
@@ -122,19 +125,22 @@
         Object obj = null;
         try {
             obj = context.eval(global, objExprCode, global, "<suggestions>");
-        } catch (Exception ignored) {
-            // throw the exception - this is during tab-completion
+        } catch (Exception exp) {
+            // throw away the exception - this is during tab-completion
+            if (Main.DEBUG) {
+                exp.printStackTrace();
+            }
         }
 
         if (obj != null && obj != ScriptRuntime.UNDEFINED) {
             if (endsWithDot) {
                 // no user specified "prefix". List all properties of the object
-                result.addAll(PropertiesHelper.getProperties(obj));
+                result.addAll(propsHelper.getProperties(obj));
                 return cursor;
             } else {
                 // list of properties matching the user specified prefix
                 final String prefix = select.getIdentifier();
-                result.addAll(PropertiesHelper.getProperties(obj, prefix));
+                result.addAll(propsHelper.getProperties(obj, prefix));
                 return cursor - prefix.length();
             }
         }
@@ -145,7 +151,7 @@
     private int completeIdentifier(final String test, final int cursor, final List<CharSequence> result,
                 final IdentifierTree ident) {
         final String name = ident.getName();
-        result.addAll(PropertiesHelper.getProperties(global, name));
+        result.addAll(propsHelper.getProperties(global, name));
         return cursor - name.length();
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PackagesHelper.java	Sun Aug 23 10:02:14 2015 +0530
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.nashorn.tools.jjs;
+
+import java.io.IOException;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileManager.Location;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+/**
+ * A helper class to compute properties of a Java package object. Properties of
+ * package object are (simple) top level class names in that java package and
+ * immediate subpackages of that package.
+ */
+final class PackagesHelper {
+    // JavaCompiler may be null on certain platforms (eg. JRE)
+    private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+
+    /**
+     * Is Java package properties helper available?
+     *
+     * @return true if package properties support is available
+     */
+    static boolean isAvailable() {
+        return compiler != null;
+    }
+
+    private final StandardJavaFileManager fm;
+    private final Set<JavaFileObject.Kind> fileKinds;
+
+    /**
+     * Construct a new PackagesHelper.
+     *
+     * @param classPath Class path to compute properties of java package objects
+     */
+    PackagesHelper(final String classPath) throws IOException {
+        assert isAvailable() : "no java compiler found!";
+
+        fm = compiler.getStandardFileManager(null, null, null);
+        fileKinds = EnumSet.of(JavaFileObject.Kind.CLASS);
+
+        if (classPath != null && !classPath.isEmpty()) {
+            fm.setLocation(StandardLocation.CLASS_PATH, getFiles(classPath));
+        } else {
+            // no classpath set. Make sure that it is empty and not any default like "."
+            fm.setLocation(StandardLocation.CLASS_PATH, Collections.<File>emptyList());
+        }
+    }
+
+    // LRU cache for java package properties lists
+    private final LinkedHashMap<String, List<String>> propsCache =
+        new LinkedHashMap<>(32, 0.75f, true) {
+            private static final int CACHE_SIZE = 100;
+            private static final long serialVersionUID = 1;
+
+            @Override
+            protected boolean removeEldestEntry(final Map.Entry<String, List<String>> eldest) {
+                return size() > CACHE_SIZE;
+            }
+        };
+
+    /**
+     * Return the list of properties of the given Java package or package prefix
+     *
+     * @param pkg Java package name or package prefix name
+     * @return the list of properties of the given Java package or package prefix
+     */
+    List<String> getPackageProperties(final String pkg) {
+        // check the cache first
+        if (propsCache.containsKey(pkg)) {
+            return propsCache.get(pkg);
+        }
+
+        try {
+            // make sorted list of properties
+            final List<String> props = new ArrayList<>(listPackage(pkg));
+            Collections.sort(props);
+            propsCache.put(pkg, props);
+            return props;
+        } catch (final IOException exp) {
+            if (Main.DEBUG) {
+                exp.printStackTrace();
+            }
+            return Collections.<String>emptyList();
+        }
+    }
+
+    public void close() throws IOException {
+        fm.close();
+    }
+
+    private Set<String> listPackage(final String pkg) throws IOException {
+        final Set<String> props = new HashSet<>();
+        listPackage(StandardLocation.PLATFORM_CLASS_PATH, pkg, props);
+        listPackage(StandardLocation.CLASS_PATH, pkg, props);
+        return props;
+    }
+
+    private void listPackage(final Location loc, final String pkg, final Set<String> props)
+            throws IOException {
+        for (JavaFileObject file : fm.list(loc, pkg, fileKinds, true)) {
+            final String binaryName = fm.inferBinaryName(loc, file);
+            // does not start with the given package prefix
+            if (!binaryName.startsWith(pkg + ".")) {
+                continue;
+            }
+
+            final int nextDot = binaryName.indexOf('.', pkg.length() + 1);
+            final int start = pkg.length() + 1;
+
+            if (nextDot != -1) {
+                // subpackage - eg. "regex" for "java.util"
+                props.add(binaryName.substring(start, nextDot));
+            } else {
+                // class - filter out nested, inner, anonymous, local classes.
+                // Dynalink supported public nested classes as properties of
+                // StaticClass object anyway. We don't want to expose those
+                // "$" internal names as properties of package object.
+
+                final String clsName = binaryName.substring(start);
+                if (clsName.indexOf('$') == -1) {
+                    props.add(clsName);
+                }
+            }
+        }
+    }
+
+    // return list of File objects for the given class path
+    private static List<File> getFiles(final String classPath) {
+        return Stream.of(classPath.split(File.pathSeparator))
+                    .map(File::new)
+                    .collect(Collectors.toList());
+    }
+}
--- a/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PropertiesHelper.java	Fri Aug 21 18:01:23 2015 +0530
+++ b/nashorn/src/jdk.scripting.nashorn.shell/share/classes/jdk/nashorn/tools/jjs/PropertiesHelper.java	Sun Aug 23 10:02:14 2015 +0530
@@ -25,6 +25,7 @@
 
 package jdk.nashorn.tools.jjs;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -32,6 +33,7 @@
 import java.util.WeakHashMap;
 import java.util.stream.Collectors;
 import jdk.nashorn.internal.runtime.JSType;
+import jdk.nashorn.internal.runtime.NativeJavaPackage;
 import jdk.nashorn.internal.runtime.PropertyMap;
 import jdk.nashorn.internal.runtime.ScriptObject;
 import jdk.nashorn.internal.runtime.ScriptRuntime;
@@ -41,19 +43,59 @@
  * A helper class to get properties of a given object for source code completion.
  */
 final class PropertiesHelper {
-    private PropertiesHelper() {}
-
+    // Java package properties helper, may be null
+    private PackagesHelper pkgsHelper;
     // cached properties list
-    private static final WeakHashMap<Object, List<String>> propsCache = new WeakHashMap<>();
+    private final WeakHashMap<Object, List<String>> propsCache = new WeakHashMap<>();
 
-    // returns the list of properties of the given object
-    static List<String> getProperties(final Object obj) {
+    /**
+     * Construct a new PropertiesHelper.
+     *
+     * @param classPath Class path to compute properties of java package objects
+     */
+    PropertiesHelper(final String classPath) {
+        if (PackagesHelper.isAvailable()) {
+            try {
+                this.pkgsHelper = new PackagesHelper(classPath);
+            } catch (final IOException exp) {
+                if (Main.DEBUG) {
+                    exp.printStackTrace();
+                }
+                this.pkgsHelper = null;
+            }
+        }
+    }
+
+    void close() throws Exception {
+        propsCache.clear();
+        pkgsHelper.close();
+    }
+
+    /**
+     * returns the list of properties of the given object.
+     *
+     * @param obj object whose property list is returned
+     * @return the list of properties of the given object
+     */
+    List<String> getProperties(final Object obj) {
         assert obj != null && obj != ScriptRuntime.UNDEFINED;
 
+        // wrap JS primitives as objects before gettting properties
         if (JSType.isPrimitive(obj)) {
             return getProperties(JSType.toScriptObject(obj));
         }
 
+        // Handle Java package prefix case first. Should do it before checking
+        // for its super class ScriptObject!
+        if (obj instanceof NativeJavaPackage) {
+            if (pkgsHelper != null) {
+                return pkgsHelper.getPackageProperties(((NativeJavaPackage)obj).getName());
+            } else {
+                return Collections.<String>emptyList();
+            }
+        }
+
+        // script object - all inherited and non-enumerable, non-index properties
         if (obj instanceof ScriptObject) {
             final ScriptObject sobj = (ScriptObject)obj;
             final PropertyMap pmap = sobj.getMap();
@@ -71,6 +113,7 @@
             return props;
         }
 
+        // java class case - don't refer to StaticClass directly
         if (NativeJava.isType(ScriptRuntime.UNDEFINED, obj)) {
             if (propsCache.containsKey(obj)) {
                 return propsCache.get(obj);
@@ -82,6 +125,7 @@
             return props;
         }
 
+        // any other Java object
         final Class<?> clazz = obj.getClass();
         if (propsCache.containsKey(clazz)) {
             return propsCache.get(clazz);
@@ -94,8 +138,14 @@
         return props;
     }
 
-    // returns the list of properties of the given object that start with the given prefix
-    static List<String> getProperties(final Object obj, final String prefix) {
+    /**
+     * Returns the list of properties of the given object that start with the given prefix.
+     *
+     * @param obj object whose property list is returned
+     * @param prefix property prefix to be matched
+     * @return the list of properties of the given object
+     */
+    List<String> getProperties(final Object obj, final String prefix) {
         assert prefix != null && !prefix.isEmpty();
         return getProperties(obj).stream()
                    .filter(s -> s.startsWith(prefix))
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptEnvironment.java	Fri Aug 21 18:01:23 2015 +0530
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptEnvironment.java	Sun Aug 23 10:02:14 2015 +0530
@@ -61,6 +61,9 @@
     /** Size of the per-global Class cache size */
     public final int     _class_cache_size;
 
+    /** -classpath value. */
+    public final String  _classpath;
+
     /** Only compile script, do not run it or generate other ScriptObjects */
     public final boolean _compile_only;
 
@@ -220,6 +223,7 @@
         this.options = options;
 
         _class_cache_size     = options.getInteger("class.cache.size");
+        _classpath            = options.getString("classpath");
         _compile_only         = options.getBoolean("compile.only");
         _const_as_var         = options.getBoolean("const.as.var");
         _debug_lines          = options.getBoolean("debug.lines");