PackagesHelper.java revision 1790:785843878cf7
1/*
2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.tools.jjs;
27
28import java.lang.reflect.Modifier;
29import java.io.IOException;
30import java.io.File;
31import java.net.URI;
32import java.nio.file.DirectoryStream;
33import java.nio.file.Files;
34import java.nio.file.FileSystem;
35import java.nio.file.FileSystems;
36import java.nio.file.Path;
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.EnumSet;
40import java.util.HashSet;
41import java.util.LinkedHashMap;
42import java.util.List;
43import java.util.Map;
44import java.util.Set;
45import java.util.stream.Collectors;
46import java.util.stream.Stream;
47import javax.tools.JavaCompiler;
48import javax.tools.JavaFileManager.Location;
49import javax.tools.JavaFileObject;
50import javax.tools.StandardJavaFileManager;
51import javax.tools.StandardLocation;
52import javax.tools.ToolProvider;
53import jdk.nashorn.internal.runtime.Context;
54
55/**
56 * A helper class to compute properties of a Java package object. Properties of
57 * package object are (simple) top level class names in that java package and
58 * immediate subpackages of that package.
59 */
60final class PackagesHelper {
61    // JavaCompiler may be null on certain platforms (eg. JRE)
62    private static final JavaCompiler compiler;
63    static {
64        // Use javac only if security manager is not around!
65        compiler = System.getSecurityManager() == null? ToolProvider.getSystemJavaCompiler() : null;
66    }
67
68    /**
69     * Is javac available?
70     *
71     * @return true if javac is available
72     */
73    private static boolean isJavacAvailable() {
74        return compiler != null;
75    }
76
77    private final Context context;
78    private final boolean modulePathSet;
79    private final StandardJavaFileManager fm;
80    private final Set<JavaFileObject.Kind> fileKinds;
81    private final FileSystem jrtfs;
82
83    /**
84     * Construct a new PackagesHelper.
85     *
86     * @param context the current Nashorn Context
87     */
88    PackagesHelper(final Context context) throws IOException {
89        this.context = context;
90        final String modulePath = context.getEnv()._module_path;
91        this.modulePathSet = modulePath != null && !modulePath.isEmpty();
92        if (isJavacAvailable()) {
93            final String classPath = context.getEnv()._classpath;
94            fm = compiler.getStandardFileManager(null, null, null);
95            fileKinds = EnumSet.of(JavaFileObject.Kind.CLASS);
96
97            if (this.modulePathSet) {
98                fm.setLocation(StandardLocation.MODULE_PATH, getFiles(modulePath));
99            }
100
101            if (classPath != null && !classPath.isEmpty()) {
102                fm.setLocation(StandardLocation.CLASS_PATH, getFiles(classPath));
103            } else {
104                // no classpath set. Make sure that it is empty and not any default like "."
105                fm.setLocation(StandardLocation.CLASS_PATH, Collections.<File>emptyList());
106            }
107            jrtfs = null;
108        } else {
109            // javac is not available - directly use jrt fs
110            // to support at least platform classes.
111            fm = null;
112            fileKinds = null;
113            jrtfs = FileSystems.getFileSystem(URI.create("jrt:/"));
114        }
115    }
116
117    // LRU cache for java package properties lists
118    private final LinkedHashMap<String, List<String>> propsCache =
119        new LinkedHashMap<String, List<String>>(32, 0.75f, true) {
120            private static final int CACHE_SIZE = 100;
121            private static final long serialVersionUID = 1;
122
123            @Override
124            protected boolean removeEldestEntry(final Map.Entry<String, List<String>> eldest) {
125                return size() > CACHE_SIZE;
126            }
127        };
128
129    /**
130     * Return the list of properties of the given Java package or package prefix
131     *
132     * @param pkg Java package name or package prefix name
133     * @return the list of properties of the given Java package or package prefix
134     */
135    List<String> getPackageProperties(final String pkg) {
136        // check the cache first
137        if (propsCache.containsKey(pkg)) {
138            return propsCache.get(pkg);
139        }
140
141        try {
142            // make sorted list of properties
143            final List<String> props = new ArrayList<>(listPackage(pkg));
144            Collections.sort(props);
145            propsCache.put(pkg, props);
146            return props;
147        } catch (final IOException exp) {
148            if (Main.DEBUG) {
149                exp.printStackTrace();
150            }
151            return Collections.<String>emptyList();
152        }
153    }
154
155    public void close() throws IOException {
156        if (fm != null) {
157            fm.close();
158        }
159    }
160
161    private Set<String> listPackage(final String pkg) throws IOException {
162        final Set<String> props = new HashSet<>();
163        if (fm != null) {
164            listPackage(StandardLocation.PLATFORM_CLASS_PATH, pkg, props);
165            if (this.modulePathSet) {
166                for (Set<Location> locs : fm.listModuleLocations(StandardLocation.MODULE_PATH)) {
167                    for (Location loc : locs) {
168                        listPackage(loc, pkg, props);
169                    }
170                }
171            }
172            listPackage(StandardLocation.CLASS_PATH, pkg, props);
173        } else if (jrtfs != null) {
174            // look for the /packages/<package_name> directory
175            Path pkgDir = jrtfs.getPath("/packages/" + pkg);
176            if (Files.exists(pkgDir)) {
177                String pkgSlashName = pkg.replace('.', '/');
178                try (DirectoryStream<Path> ds = Files.newDirectoryStream(pkgDir)) {
179                    // it has module links under which this package occurs
180                    for (Path mod : ds) {
181                        // get the package directory under /modules
182                        Path pkgUnderMod = jrtfs.getPath(mod.toString() + "/" + pkgSlashName);
183                        try (DirectoryStream<Path> ds2 = Files.newDirectoryStream(pkgUnderMod)) {
184                            for (Path p : ds2) {
185                                String str = p.getFileName().toString();
186                                // get rid of ".class", if any
187                                if (str.endsWith(".class")) {
188                                    final String clsName = str.substring(0, str.length() - ".class".length());
189                                    if (clsName.indexOf('$') == -1 && isClassAccessible(pkg + "." + clsName)) {
190                                        props.add(str);
191                                    }
192                                } else if (isPackageAccessible(pkg + "." + str)) {
193                                    props.add(str);
194                                }
195                            }
196                        }
197                    }
198                }
199            }
200        }
201        return props;
202    }
203
204    private void listPackage(final Location loc, final String pkg, final Set<String> props)
205            throws IOException {
206        for (JavaFileObject file : fm.list(loc, pkg, fileKinds, true)) {
207            final String binaryName = fm.inferBinaryName(loc, file);
208            // does not start with the given package prefix
209            if (!binaryName.startsWith(pkg + ".")) {
210                continue;
211            }
212
213            final int nextDot = binaryName.indexOf('.', pkg.length() + 1);
214            final int start = pkg.length() + 1;
215
216            if (nextDot != -1) {
217                // subpackage - eg. "regex" for "java.util"
218                final String pkgName = binaryName.substring(start, nextDot);
219                if (isPackageAccessible(binaryName.substring(0, nextDot))) {
220                    props.add(binaryName.substring(start, nextDot));
221                }
222            } else {
223                // class - filter out nested, inner, anonymous, local classes.
224                // Dynalink supported public nested classes as properties of
225                // StaticClass object anyway. We don't want to expose those
226                // "$" internal names as properties of package object.
227
228                final String clsName = binaryName.substring(start);
229                if (clsName.indexOf('$') == -1 && isClassAccessible(binaryName)) {
230                    props.add(clsName);
231                }
232            }
233        }
234    }
235
236    // return list of File objects for the given class path
237    private static List<File> getFiles(final String classPath) {
238        return Stream.of(classPath.split(File.pathSeparator))
239                    .map(File::new)
240                    .collect(Collectors.toList());
241    }
242
243    private boolean isClassAccessible(final String className) {
244        try {
245            final Class<?> clz = context.findClass(className);
246            return Modifier.isPublic(clz.getModifiers());
247        } catch (final ClassNotFoundException cnfe) {
248        }
249        return false;
250    }
251
252    private boolean isPackageAccessible(final String pkgName) {
253        try {
254            Context.checkPackageAccess(pkgName);
255            return true;
256        } catch (final SecurityException se) {
257            return false;
258        }
259    }
260}
261