
org.clapper.classutil.ClassFinder.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of classutil_2.12.0-M4 Show documentation
Show all versions of classutil_2.12.0-M4 Show documentation
A library for fast runtime class-querying, and more
/*
---------------------------------------------------------------------------
This software is released under a BSD license, adapted from
http://opensource.org/licenses/bsd-license.php
Copyright (c) 2010, Brian M. Clapper
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 names "clapper.org", "ClassUtil", 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 HOLDER 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.
---------------------------------------------------------------------------
*/
/** This library provides methods for locating and filtering classes
* quickly--faster, in fact, than can be done with Java or Scala runtime
* reflection. Under the covers, it uses the ASM bytecode library, though
* it can easily be extended to use other bytecode libraries. ClassUtil
* loads and returns information about classes using an efficient lazy
* iterator approach, which offers minimal startup penalty and the ability
* to cut the traversal short.
*/
package org.clapper.classutil
import scala.annotation.tailrec
import scala.language.reflectiveCalls
import java.util.jar.{JarFile, Manifest => JarManifest}
import java.util.zip.{ZipFile, ZipEntry}
import java.io.{File, InputStream}
/** An enumerated high-level view of the modifiers that can be attached
* to a method, class or field.
*/
object Modifier extends Enumeration {
type Modifier = Value
val Abstract = Value("abstract")
val Final = Value("final")
val Interface = Value("interface")
val Native = Value("native")
val Private = Value("private")
val Protected = Value("protected")
val Public = Value("public")
val Static = Value("static")
val Strict = Value("strict")
val Synchronized = Value("synchronized")
val Synthetic = Value("synthetic")
val Transient = Value("transient")
val Volatile = Value("volatile")
}
/** Base trait for method, field and class info.
*/
private[classutil] trait BaseInfo {
/** The name of the entity.
*/
val name: String
/** The entity's modifiers.
*/
val modifiers: Set[Modifier.Modifier]
/** A printable version of the field. Currently, the string version is
* the entity name.
*/
override def toString = name
/** Convenience method that determines whether the class implements an
* interface. This method is just shorthand for:
* {{{
* modifiers contains Modifier.Interface
* }}}
*/
def isInterface = modifiers contains Modifier.Interface
/** Convenience method that determines whether the class is abstract
* This method is just shorthand for:
* {{{
* modifiers contains Modifier.Abstract
* }}}
*/
def isAbstract = modifiers contains Modifier.Abstract
/** Convenience method that determines whether the class is private.
* This method is just shorthand for:
* {{{
* modifiers contains Modifier.Private
* }}}
*/
def isPrivate = modifiers contains Modifier.Private
/** Convenience method that determines whether the class is protected.
* This method is just shorthand for:
* {{{
* modifiers contains Modifier.Protected
* }}}
*/
def isProtected = modifiers contains Modifier.Protected
/** Convenience methods that determines whether the class is public.
* This method is just shorthand for:
* {{{
* modifiers contains Modifier.Public
* }}}
*/
def isPublic = modifiers contains Modifier.Public
/** Convenience methods that determines whether the class is final.
* This method is just shorthand for:
* {{{
* modifiers contains Modifier.Final
* }}}
*/
def isFinal = modifiers contains Modifier.Final
/** Convenience methods that determines whether the class is static.
* This method is just shorthand for:
* {{{
* modifiers contains Modifier.Static
* }}}
*/
def isStatic = modifiers contains Modifier.Static
/** Convenience methods that determines whether the class is synchronized.
* This method is just shorthand for:
* {{{
* modifiers contains Modifier.Synchronized
* }}}
*/
def isSynchronized = modifiers contains Modifier.Synchronized
/** Convenience methods that determines whether the class is synthetic.
* This method is just shorthand for:
* {{{
* modifiers contains Modifier.Synthetic
* }}}
*/
def isSynthetic = modifiers contains Modifier.Synthetic
/** Convenience method to determine whether the class is concrete (i.e.,
* isn't abstract and isn't an interface).
*/
def isConcrete = ! ((modifiers contains Modifier.Abstract) ||
(modifiers contains Modifier.Interface))
}
/** Information about a method, as read from a class file.
*/
trait MethodInfo extends BaseInfo {
/** The method's JVM signature (only available with generics).
* Ex: java.util.List.iterator ()Ljava/util/Iterator;
*/
val signature: String
/** The method's descriptor which describes it's arg types
* and return type.
* Ex: (ILjava/lang/String;)[I
*/
val descriptor: String
/** A list of the checked exceptions (as class names) that the method
* throws, or an empty list if it throws no known checked exceptions.
*/
val exceptions: List[String]
/** A printable version of the method. Currently, the string is
* the method name plus descriptor.
*/
override def toString = name + descriptor
override def hashCode = toString.hashCode
override def equals(o: Any) = o match {
case m: MethodInfo => m.toString == toString
case _ => false
}
}
/** Information about a field, as read from a class file.
*/
trait FieldInfo extends BaseInfo {
/** The field's JVM signature (only available with generics).
*/
val signature: String
/** The field's descriptor which describes it's type
* Ex: Ljava/lang/String;
*/
val descriptor: String
/** The field's default value, only available when the field
* is a static field that is a primitive or a String type.
*/
val value: Option[java.lang.Object]
override def hashCode = name.hashCode
override def equals(o: Any) = o match {
case m: FieldInfo => m.name == name
case _ => false
}
}
/** Information about a field, as read from a class file.
*/
trait AnnotationInfo {
/** The annotations's descriptor which describes it's type
* Ex: Lscala/reflect/ScalaSignature;
*/
val descriptor: String
val visible: Boolean
def params: Map[String, Any]
override def hashCode = (descriptor, params).hashCode
override def equals(o: Any) = o match {
case m: AnnotationInfo => m.descriptor == descriptor && m.params == params
case _ => false
}
}
/** Information about a class, as read from a class file.
*/
trait ClassInfo extends BaseInfo {
/** The parent class's fully qualified name.
*/
def superClassName: String
/** A list of the interfaces, as class names, that the class implements;
* or, an empty list if it implements no interfaces.
*/
def interfaces: List[String]
/** The class's JVM signature.
*/
def signature: String
/** Where the class was found (directory, jar file, or zip file).
*/
def location: File
/** A set of the methods in the class.
*/
def methods: Set[MethodInfo]
/** A set of the fields in the class.
*/
def fields: Set[FieldInfo]
/** A set of the runtime-retained annotations in the class.
*/
def annotations: Set[AnnotationInfo]
/** Convenience method to determine whether this class directly
* implements a specific interface. Since a `ClassInfo` object contains
* information about a single class, this method cannot determine
* whether a class indirectly implements an interface. That capability
* is a higher-order operation.
*
* @param interface the name of the interface
*
* @return whether the class implements the interface
*/
def implements(interface: String) = interfaces contains interface
}
/** A `ClassFinder` finds classes in a class path, returning the result in a
* lazy iterator. The iterator can then be filtered, mapped, or passed to
* the utility methods in the `ClassFinder` companion object.
*
* @param path a sequence of directories, jars and zips to search
*/
class ClassFinder(path: Seq[File]) {
val classpath = path.toList
/** Find all classes in the specified path, which can contain directories,
* zip files and jar files. Returns metadata about each class in a
* `ClassInfo` object. The `ClassInfo` objects are returned lazily,
* rather than loaded all up-front.
*
* @return a `Stream` of `ClassInfo` objects
*/
def getClasses(): Stream[ClassInfo] = find(classpath)
/* ---------------------------------------------------------------------- *\
Private Methods
\* ---------------------------------------------------------------------- */
private def find(path: Seq[File]): Stream[ClassInfo] = {
path match {
case Nil => Stream.empty[ClassInfo]
case item :: Nil => findClassesIn(item)
case item :: tail => findClassesIn(item) ++ find(tail)
}
}
private def findClassesIn(f: File): Stream[ClassInfo] = {
val name = f.getPath.toLowerCase
if (name.endsWith(".jar"))
processJar(f)
else if (name.endsWith(".zip"))
processZip(f)
else if (f.isDirectory)
processDirectory(f)
else
Stream.empty[ClassInfo]
}
private def processJar(file: File): Stream[ClassInfo] = {
val jar = new JarFile(file)
val list1 = processOpenZip(file, jar)
var manifest = jar.getManifest
if (manifest == null)
list1
else {
val path = loadManifestPath(jar, file, manifest)
val list2 = find(path)
list1 ++ list2
}
}
private def loadManifestPath(jar: JarFile,
jarFile: File,
manifest: JarManifest): List[File] = {
import scala.collection.JavaConversions._
val attrs = manifest.getMainAttributes
val value = attrs.get("Class-Path").asInstanceOf[String]
if (value == null)
Nil
else {
val parent = jarFile.getParent
val tokens = value.split("""\s+""").toList
if (parent == null)
tokens.map(new File(_))
else
tokens.map(s => new File(parent + File.separator + s))
}
}
private def processZip(file: File): Stream[ClassInfo] =
processOpenZip(file, new ZipFile(file))
private def processOpenZip(file: File, zipFile: ZipFile) = {
import scala.collection.JavaConversions._
val classInfoIterators =
zipFile.entries.
toStream.
filter((e: ZipEntry) => isClass(e)).
map((e: ZipEntry) => classData(zipFile.getInputStream(e), file))
for { it <- classInfoIterators
data <- it }
yield data
}
// Structural type that matches both ZipEntry and File
private type FileEntry = {
def isDirectory(): Boolean
def getName(): String
}
private def isClass(e: FileEntry): Boolean =
(! e.isDirectory) && e.getName.toLowerCase.endsWith(".class")
private def processDirectory(dir: File): Stream[ClassInfo] = {
import grizzled.file.Implicits._
import java.io.FileInputStream
val inputStreams = dir.listRecursively().filter(isClass).
map(f => new FileInputStream(f))
val iterators =
for (fis <- inputStreams) yield {
try {
classData(fis, dir)
}
finally {
fis.close()
}
}
for { it <- iterators
data <- it }
yield data
}
private def classData(is: InputStream,
location: File): Iterator[ClassInfo] = {
import org.clapper.classutil.asm.ClassFile
ClassFile.load(is, location)
}
}
/** The entrance to the factory floor, providing methods for finding and
* filtering classes.
*/
object ClassFinder {
/** Convenient method for getting the standard JVM classpath, into a
* variable suitable for use with the `find()` method.
*
* @return the classpath, as a list of `File` objects
*/
def classpath = System.getProperty("java.class.path").
split(File.pathSeparator).
map(s => if (s.trim.length == 0) "." else s).
map(new File(_)).
toList
/** Instantiate a new `ClassFinder` that will search the specified
* classpath, or the default classpath, if no classpath is defined.
*
* @param path the classpath, which is a sequence of `File`
* objects representing directories, jars and zip files
* to search. Defaults to `classpath` if empty.
*
* @return a new `ClassFinder` object
*/
def apply(path: Seq[File] = Seq.empty[File]) =
new ClassFinder(if (path.nonEmpty) path else classpath)
/** Create a map from an Iterator of ClassInfo objects. The resulting
* map is indexed by class name.
*
* @return a map of (classname, `ClassInfo`) pairs
*/
def classInfoMap(iterator: Iterator[ClassInfo]): Map[String, ClassInfo] =
iterator.map(c => c.name -> c).toMap
/** Create a map from a Stream of ClassInfo objects. The resulting map is
* indexed by class name.
*
* @return a map of (classname, `ClassInfo`) pairs
*/
def classInfoMap(stream: Stream[ClassInfo]): Map[String, ClassInfo] =
classInfoMap(stream.toIterator)
/** Convenience method that scans the specified classes for all concrete
* classes that are subclasses of a superclass or trait. A subclass, in this
* definition, is a class that directly or indirectly (a) implements an
* interface (if the named class is an interface) or (b) extends a
* subclass (if the named class is a class). The class must be
* concrete, so intermediate abstract classes are not returned, though
* any children of such abstract classes will be.
*
* '''WARNINGS'''
*
* This method converts the stream to a map of classes, for easier
* lookup. Thus, upon its return, the stream will be empty. You can
* certainly recreate the stream, but at a cost. If you need to make
* multiple calls to this method with the same classpath, consider
* converting the stream to a map first, as shown below:
* {{{
* val finder = ClassFinder(myPath)
* val classes = finder.getClasses // classes is an Stream[ClassInfo]
* val classMap = ClassFinder.classInfoMap // runs the stream out, once
* val foos = ClassFinder.concreteSubclasses(classOf[org.example.Foo], classMap)
* val bars = ClassFinder.concreteSubclasses(classOf[Bar], classMap)
* }}}
*
* This method can chew up a lot of temporary heap space, if called
* with a large classpath.
*
* @param ancestor the `Class` object of the superclass or trait for which
* to find descendent concrete subclasses
* @param classes the stream of `ClassInfo` objects to search
*
* @return an iterator of `ClassInfo` objects that are concrete subclasses
* of `ancestor`. The iterator will be empty if no matching classes
* could be found.
*/
def concreteSubclasses(ancestor: Class[_], classes: Stream[ClassInfo]):
Iterator[ClassInfo] = {
findConcreteSubclasses(ancestor.getName, ClassFinder.classInfoMap(classes))
}
/** Variant of `concreteSubclasses()` that takes a string class name and
* a `Stream` of `ClassInfo` objects, rather than a `Class` and a `Stream`.
*
* @param ancestor the name of the class for which to find descendent
* concrete subclasses
* @param classes the stream of `ClassInfo` objects to search
*
* @return an iterator of `ClassInfo` objects that are concrete subclasses
* of `ancestor`. The iterator will be empty if no matching classes
* could be found.
*
* @see `concreteSubclasses(Class[_], Stream[ClassInfo])`
*/
def concreteSubclasses(ancestor: String, classes: Stream[ClassInfo]):
Iterator[ClassInfo] = {
findConcreteSubclasses(ancestor, ClassFinder.classInfoMap(classes))
}
/** Variant of `concreteSubclasses()` that takes a class and an `Iterator`
* of `ClassInfo` objects, rather than a `Class` and a `Stream`.
*
* @example
* {{{
* val finder = ClassFinder(myPath)
* val classes = finder.getClasses // classes is an Stream[ClassInfo]
* // Of course, it's easier just to call the version that takes a
* // Stream...
* ClassFinder.concreteSubclasses(classOf[Baz], classes.toIterator)
* }}}
*
* @param ancestor the `Class` object of the superclass or trait for which
* to find descendent concrete subclasses
* @param classes the iterator of `ClassInfo` objects to search
*
* @return an iterator of `ClassInfo` objects that are concrete subclasses
* of `ancestor`. The iterator will be empty if no matching classes
* could be found.
*
* @see `concreteSubclasses(Class[_], Stream[ClassInfo])`
*/
def concreteSubclasses(ancestor: Class[_], classes: Iterator[ClassInfo]):
Iterator[ClassInfo] = {
findConcreteSubclasses(ancestor.getName, ClassFinder.classInfoMap(classes))
}
/** Variant of `concreteSubclasses()` that takes a string class name and
* an `Iterator` of `ClassInfo` objects, rather than a `Class` and an
* `Iterator`.
*
* @example
* {{{
* val finder = ClassFinder(myPath)
* val classes = finder.getClasses // classes is an Stream[ClassInfo]
* // Of course, it's easier just to call the version that takes a
* // Stream...
* ClassFinder.concreteSubclasses("org.example.Foo", classes.toIterator)
* }}}
*
* @param ancestor the name of the class for which to find descendent
* concrete subclasses
* @param classes the stream of `ClassInfo` objects to search
*
* @return an iterator of `ClassInfo` objects that are concrete subclasses
* of `ancestor`. The iterator will be empty if no matching classes
* could be found.
*
* @see `concreteSubclasses(String, Stream[ClassInfo])`
* @see `concreteSubclasses(Class[_], Iterator[ClassInfo])`
*/
def concreteSubclasses(ancestor: String, classes: Iterator[ClassInfo]):
Iterator[ClassInfo] = {
findConcreteSubclasses(ancestor, ClassFinder.classInfoMap(classes))
}
/** Variant of `concreteSubclasses()` that takes a class and a `Map`
* `ClassInfo` objects.
*
* @param ancestor the `Class` object of the superclass or trait for which
* to find descendent concrete subclasses
* @param classes the iterator of `ClassInfo` objects to search
*
* @return an iterator of `ClassInfo` objects that are concrete subclasses
* of `ancestor`. The iterator will be empty if no matching classes
* could be found.
*
* @see `concreteSubclasses(Class[_], Stream[ClassInfo])`
* @see `concreteSubclasses(Class[_], Iterator[ClassInfo])`
*/
def concreteSubclasses(ancestor: Class[_], classes: Map[String, ClassInfo]):
Iterator[ClassInfo] = {
findConcreteSubclasses(ancestor.getName, classes)
}
/** Variant of `concreteSubclasses()` that takes a string class name and
* a map, rather than a `Class` and a map.
*
* @param ancestor the name of the class for which to find descendent
* concrete subclasses
* @param classes the iterator of `ClassInfo` objects to search
*
* @return an iterator of `ClassInfo` objects that are concrete subclasses
* of `ancestor`. The iterator will be empty if no matching classes
* could be found.
*
* @see `concreteSubclasses(Class[_], Map[String, ClassInfo])`
* @see `concreteSubclasses(String, Stream[ClassInfo])`
* @see `concreteSubclasses(String, Iterator[ClassInfo])`
*/
def concreteSubclasses(ancestor: String, classes: Map[String, ClassInfo]):
Iterator[ClassInfo] = {
findConcreteSubclasses(ancestor, classes)
}
// -------------------------------------------------------------------------
// Private methods
// -------------------------------------------------------------------------
private def findConcreteSubclasses(ancestor: String,
classes: Map[String, ClassInfo]):
Iterator[ClassInfo] = {
// Convert the set of classes to search into a map of ClassInfo objects
// indexed by class name.
@tailrec def classMatches(targetClassInfo: ClassInfo,
classesToCheck: Seq[ClassInfo]): Boolean = {
val targetName = targetClassInfo.name // could use ancestor, but, yuck.
val classNames = classesToCheck.map(_.name)
val interfaceNamesToCheck = classesToCheck.flatMap(_.interfaces)
if (classNames contains targetName) {
// The current classes we're checking match the target class. Done.
true
}
else if (interfaceNamesToCheck contains targetName) {
// At least one of current classes implements an interface that is
// the target class. Done.
true
}
else {
val superClasses = classesToCheck.flatMap { c =>
classes.get(c.superClassName)
}
val interfaces = interfaceNamesToCheck.flatMap { i =>
classes.get(i)
}
val newClassesToCheck = superClasses ++ interfaces
if (newClassesToCheck.isEmpty) {
// No matches. Done.
false
}
else {
// Dive deeper.
classMatches(targetClassInfo, newClassesToCheck)
}
}
}
// Find the ancestor class
classes.get(ancestor).map { classInfo =>
classes.values
.toIterator
.filter(_.isConcrete)
.filter(c => classMatches(classInfo, Seq(c)))
}
.getOrElse(Iterator.empty)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy