org.codehaus.groovy.tools.shell.util.PackageHelperImpl.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of groovy-all Show documentation
Show all versions of groovy-all Show documentation
Groovy: A powerful, dynamic language for the JVM
/*
* 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.codehaus.groovy.tools.shell.util
import groovy.transform.CompileStatic
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.prefs.PreferenceChangeEvent
import java.util.prefs.PreferenceChangeListener
import java.util.regex.Pattern
import java.util.zip.ZipException
/**
* Helper class that crawls all items of the classpath for packages.
* Retrieves from those sources the list of subpackages and classes on demand.
*/
class PackageHelperImpl implements PreferenceChangeListener, PackageHelper {
// Pattern for regular Classnames
public static final Pattern NAME_PATTERN = ~('^[A-Z][^.\$_]+\$')
private static final String CLASS_SUFFIX = '.class'
protected static final Logger LOG = Logger.create(PackageHelperImpl)
Map rootPackages = null
final ClassLoader groovyClassLoader
PackageHelperImpl(final ClassLoader groovyClassLoader=null) {
this.groovyClassLoader = groovyClassLoader
if (! Boolean.valueOf(Preferences.get(IMPORT_COMPLETION_PREFERENCE_KEY))) {
rootPackages = initializePackages(groovyClassLoader)
}
Preferences.addChangeListener(this)
}
@Override
void preferenceChange(final PreferenceChangeEvent evt) {
if (evt.key == IMPORT_COMPLETION_PREFERENCE_KEY) {
if (Boolean.valueOf(evt.getNewValue())) {
rootPackages = null
} else if (rootPackages == null) {
rootPackages = initializePackages(groovyClassLoader)
}
}
}
static Map initializePackages(final ClassLoader groovyClassLoader) throws IOException {
Map rootPackages = new HashMap()
Set urls = new HashSet()
// classes in CLASSPATH
for (ClassLoader loader = groovyClassLoader; loader != null; loader = loader.parent) {
if (!(loader instanceof URLClassLoader)) {
LOG.debug('Ignoring classloader for completion: ' + loader)
continue
}
urls.addAll(((URLClassLoader)loader).URLs)
}
// System classes
Class[] systemClasses = [String, javax.swing.JFrame, GroovyObject] as Class[]
boolean jigsaw = false
systemClasses.each { Class systemClass ->
// normal slash even in Windows
String classfileName = systemClass.name.replace('.', '/') + '.class'
URL classURL = systemClass.getResource(classfileName)
if (classURL == null) {
// this seems to work on Windows better than the earlier approach
classURL = Thread.currentThread().contextClassLoader.getResource(classfileName)
}
if (classURL != null) {
URLConnection uc = classURL.openConnection()
if (uc instanceof JarURLConnection) {
urls.add(((JarURLConnection) uc).getJarFileURL())
} else if (uc.getClass().getSimpleName().equals("JavaRuntimeURLConnection")) {
// Java 9 Jigsaw detected
jigsaw = true
} else {
String filepath = classURL.toExternalForm()
String rootFolder = filepath.substring(0, filepath.length() - classfileName.length() - 1)
urls.add(new URL(rootFolder))
}
}
}
for (URL url : urls) {
Collection packageNames = getPackageNames(url)
if (packageNames) {
mergeNewPackages(packageNames, url, rootPackages)
}
}
if (jigsaw) {
URL jigsawURL = URI.create("jrt:/").toURL()
Set jigsawPackages = getPackagesAndClassesFromJigsaw(jigsawURL) { isPackage, name -> isPackage && name }
mergeNewPackages(jigsawPackages, jigsawURL, rootPackages)
}
return rootPackages
}
/**
* This method returns packages or classes listed from Jigsaw modules.
* It makes use of a GroovyShell in order to avoid a hard dependency
* to JDK 7+ when building the Groovysh module (uses nio2)
* @return
*/
private static Set getPackagesAndClassesFromJigsaw(URL jigsawURL, Closure predicate) {
def shell = new GroovyShell()
shell.setProperty('predicate', predicate)
String jigsawURLString = jigsawURL.toString()
shell.setProperty('jigsawURLString', jigsawURLString)
shell.evaluate '''import java.nio.file.*
def fs = FileSystems.newFileSystem(URI.create(jigsawURLString), [:])
result = [] as Set
def filterPackageName(Path path) {
def elems = "$path".split('/')
if (elems && elems.length > 2) {
// remove e.g. 'modules/java.base/
elems = elems[2.. 2) {
// remove e.g. 'modules/java.base/
elems = elems[2.. filterPackageName(dir); FileVisitResult.CONTINUE },
visitFile: { file, attrs -> filterClassName(file); FileVisitResult.CONTINUE}
]
as SimpleFileVisitor)
'''
Set jigsawPackages = (Set) shell.getProperty('result')
jigsawPackages
}
static mergeNewPackages(final Collection packageNames, final URL url,
final Map rootPackages) {
StringTokenizer tokenizer
packageNames.each { String packname ->
tokenizer = new StringTokenizer(packname, '.')
if (!tokenizer.hasMoreTokens()) {
return
}
String rootname = tokenizer.nextToken()
CachedPackage cp
CachedPackage childp
cp = rootPackages.get(rootname, null) as CachedPackage
if (cp == null) {
cp = new CachedPackage(rootname, [url] as Set)
rootPackages.put(rootname, cp)
}
while(tokenizer.hasMoreTokens()) {
String packbasename = tokenizer.nextToken()
if (cp.childPackages == null) {
// small initial size, to save memory
cp.childPackages = new HashMap(1)
}
childp = cp.childPackages.get(packbasename, null) as CachedPackage
if (childp == null) {
// start with small arraylist, to save memory
Set urllist = new HashSet(1)
urllist.add(url)
childp = new CachedPackage(packbasename, urllist)
cp.childPackages.put(packbasename, childp)
} else {
childp.sources.add(url)
}
cp = childp
}
}
}
/**
* Returns all packagenames found at URL, accepts jar files and folders
* @param url
* @return
*/
static Collection getPackageNames(final URL url) {
//log.debug(url)
String path = URLDecoder.decode(url.getFile(), 'UTF-8')
File urlfile = new File(path)
if (urlfile.isDirectory()) {
Set packnames = new HashSet()
collectPackageNamesFromFolderRecursive(urlfile, '', packnames)
return packnames
}
if (urlfile.path.endsWith('.jar')) {
try {
JarFile jf = new JarFile(urlfile)
return getPackageNamesFromJar(jf)
} catch(ZipException ze) {
if (LOG.debugEnabled) {
ze.printStackTrace()
}
LOG.debug("Error opening zipfile : '${url.getFile()}', ${ze.toString()}")
} catch (FileNotFoundException fnfe) {
LOG.debug("Error opening file : '${url.getFile()}', ${fnfe.toString()}")
}
}
return []
}
/**
* Crawls a folder, iterates over subfolders, looking for class files.
* @param directory
* @param prefix
* @param packnames
* @return
*/
static Collection collectPackageNamesFromFolderRecursive(final File directory, final String prefix,
final Set packnames) {
//log.debug(directory)
File[] files = directory.listFiles()
boolean packageAdded = false
for (int i = 0; (files != null) && (i < files.length); i++) {
if (files[i].isDirectory()) {
if (files[i].name.startsWith('.')) {
return
}
String optionalDot = prefix ? '.' : ''
collectPackageNamesFromFolderRecursive(files[i], prefix + optionalDot + files[i].name, packnames)
} else if (! packageAdded) {
if (files[i].name.endsWith(CLASS_SUFFIX)) {
packageAdded = true
if (prefix) {
packnames.add(prefix)
}
}
}
}
}
static Collection getPackageNamesFromJar(final JarFile jf) {
Set packnames = new HashSet()
for (Enumeration e = jf.entries(); e.hasMoreElements();) {
JarEntry entry = (JarEntry) e.nextElement()
if (entry == null) {
continue
}
String name = entry.name
if (!name.endsWith(CLASS_SUFFIX)) {
// only use class files
continue
}
// normal slashes also on Windows
String fullname = name.replace('/', '.').substring(0, name.length() - CLASS_SUFFIX.length())
// Discard classes in the default package
if (fullname.lastIndexOf('.') > -1) {
packnames.add(fullname.substring(0, fullname.lastIndexOf('.')))
}
}
return packnames
}
// following block does not work, because URLClassLoader.packages only ever returns SystemPackages
/*static Collection getPackageNames(URL url) {
URLClassLoader urlLoader = new URLClassLoader([url] as URL[])
//log.debug(urlLoader.packages.getClass())
urlLoader.getPackages().collect {Package pack ->
pack.name
}
}*/
/**
* returns the names of Classes and direct subpackages contained in a package
* @param packagename
* @return
*/
@CompileStatic
Set getContents(final String packagename) {
if (! rootPackages) {
return [] as Set
}
if (! packagename) {
return rootPackages.collect { String key, CachedPackage v -> key } as Set
}
String sanitizedPackageName
if (packagename.endsWith('.*')) {
sanitizedPackageName = packagename[0..-3]
} else {
sanitizedPackageName = packagename
}
StringTokenizer tokenizer = new StringTokenizer(sanitizedPackageName, '.')
CachedPackage cp = rootPackages.get(tokenizer.nextToken())
while (cp != null && tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken()
if (cp.childPackages == null) {
// no match for taken,no subpackages known
return [] as Set
}
cp = cp.childPackages.get(token) as CachedPackage
}
if (cp == null) {
return [] as Set
}
// TreeSet for ordering
Set children = new TreeSet()
if (cp.childPackages) {
children.addAll(cp.childPackages.collect { String key, CachedPackage v -> key })
}
if (cp.checked && !cp.containsClasses) {
return children
}
Set classnames = getClassnames(cp.sources, sanitizedPackageName)
cp.checked = true
if (classnames) {
cp.containsClasses = true
children.addAll(classnames)
}
return children
}
/**
* Copied from JLine 1.0 ClassNameCompletor
* @param urls
* @param packagename
* @return
*/
@CompileStatic
static Set getClassnames(final Set urls, final String packagename) {
Set classes = new TreeSet()
// normal slash even in Windows
String pathname = packagename.replace('.', '/')
for (Iterator it = urls.iterator(); it.hasNext();) {
URL url = (URL) it.next()
if (url.protocol=='jrt') {
getPackagesAndClassesFromJigsaw(url) { boolean isPackage, String name ->
!isPackage && name.startsWith(packagename)
}.collect(classes) { it - "${packagename}." }
} else {
File file = new File(URLDecoder.decode(url.getFile(), 'UTF-8'))
if (file == null) {
continue
}
if (file.isDirectory()) {
File packFolder = new File(file, pathname)
if (!packFolder.isDirectory()) {
continue
}
File[] files = packFolder.listFiles()
for (int i = 0; (files != null) && (i < files.length); i++) {
if (files[i].isFile()) {
String filename = files[i].name
if (filename.endsWith(CLASS_SUFFIX)) {
String name = filename.substring(0, filename.length() - CLASS_SUFFIX.length())
if (!name.matches(NAME_PATTERN)) {
continue
}
classes.add(name)
}
}
}
continue
}
if (!file.toString().endsWith('.jar')) {
continue
}
JarFile jf = new JarFile(file)
for (Enumeration e = jf.entries(); e.hasMoreElements();) {
JarEntry entry = (JarEntry) e.nextElement()
if (entry == null) {
continue
}
String name = entry.name
// only use class files
if (!name.endsWith(CLASS_SUFFIX)) {
continue
}
// normal slash inside jars even on windows
int lastslash = name.lastIndexOf('/')
if (lastslash == -1 || name.substring(0, lastslash) != pathname) {
continue
}
name = name.substring(lastslash + 1, name.length() - CLASS_SUFFIX.length())
if (!name.matches(NAME_PATTERN)) {
continue
}
classes.add(name)
}
}
}
return classes
}
}
class CachedPackage {
String name
boolean containsClasses
boolean checked
Map childPackages
Set sources
CachedPackage(String name, Set sources) {
this.sources = sources
this.name = name
}
}