
org.opalj.bi.reader.ClassFileReader.scala Maven / Gradle / Ivy
The newest version!
/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package bi
package reader
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.ByteArrayInputStream
import java.io.DataInputStream
import java.io.BufferedInputStream
import java.io.IOException
import java.io.ByteArrayOutputStream
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.Files
import java.util.zip.{ZipEntry, ZipFile}
import java.net.URL
import java.net.URI
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicInteger
import java.util.jar.JarInputStream
import java.util.jar.JarEntry
import scala.util.control.ControlThrowable
import scala.collection.JavaConverters._
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Await
import scala.concurrent.duration.Duration
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
import org.apache.commons.text.similarity.LevenshteinDistance
import org.opalj.log.OPALLogger.error
import org.opalj.log.OPALLogger.info
import org.opalj.control.fillArrayOfInt
import org.opalj.io.process
import org.opalj.concurrent.NumberOfThreadsForIOBoundTasks
import org.opalj.concurrent.BoundedExecutionContext
import org.opalj.concurrent.parForeachSeqElement
import org.opalj.bytecode.BytecodeProcessingFailedException
import org.opalj.collection.immutable.RefArray
import org.opalj.concurrent.Tasks
/**
* Implements the template method to read in a Java class file. Additionally,
* several convenience methods are defined to read in class files from various
* sources (Streams, Files, JAR archives).
*
* This library supports class files from version 45 (Java 1.1) up to version 54 (Java 10).
*
* ==Notes for Implementors==
* Reading of the class file's major structures: the constant pool, fields, methods
* and the attributes is delegated to corresponding readers.
* This enables a very high-level of adaptability.
*
* For further details see the JVM Specification: The ClassFile Structure.
*/
trait ClassFileReader extends ClassFileReaderConfiguration with Constant_PoolAbstractions {
//
// TYPE DEFINITIONS AND FACTORY METHODS
//
/**
* The type of the object that represents a Java class file.
*/
type ClassFile
/**
* The inherited interfaces.
*/
final type Interfaces = Array[Constant_Pool_Index]
/**
* The type of the object that represents the fields of a class.
*/
type Fields
/**
* The type of the object that represents all methods of a class.
*/
type Methods
/**
* The type of the object that represents a class declaration's
* attributes (e.g., the source file attribute.)
*/
type Attributes
// METHODS DELEGATING TO OTHER READERS
//
/**
* Reads the constant pool using the given stream.
*
* When this method is called the given stream has to be positioned at the very
* beginning of the constant pool. This method is called by the template method that
* reads in a class file to delegate the reading of the constant pool. Only
* information belonging to the constant pool are allowed to be read.
*
* The stream must not be closed after reading the constant pool.
*/
protected def Constant_Pool(in: DataInputStream): Constant_Pool
/**
* Reads all field declarations using the given stream and constant pool.
*
* The given stream is positioned directly before a class file's "fields_count" field.
* This method is called by the template method that reads in a class file to
* delegate the reading of the declared fields.
*/
protected def Fields(cp: Constant_Pool, in: DataInputStream): Fields
/**
* Reads all method declarations using the given stream and constant pool.
*
* The given stream is positioned directly before a class file's "methods_count" field.
* This method is called by the
* template method that reads in a class file to delegate the reading of the
* declared method.
*/
protected def Methods(cp: Constant_Pool, in: DataInputStream): Methods
/**
* Reads all attributes using the given stream and constant pool.
*
* The given stream is positioned directly before a class file's "attributes_count"
* field.
* This method is called by the template method that reads in a class file to
* delegate the reading of the attributes.
*
* '''From the Specification'''
*
* The attributes [...] appearing in the attributes table of a ClassFile
* structure are the InnerClasses, EnclosingMethod, Synthetic, Signature,
* SourceFile, SourceDebugExtension, Deprecated, RuntimeVisibleAnnotations,
* RuntimeInvisibleAnnotations, BootstrapMethods, RuntimeVisibleTypeAnnotations,
* and RuntimeInvisibleTypeAnnotations attributes.
*/
protected def Attributes(
cp: Constant_Pool,
ap: AttributeParent,
ap_name_index: Constant_Pool_Index,
ap_descriptor_index: Constant_Pool_Index,
in: DataInputStream
): Attributes
/**
* Factory method to create the `ClassFile` object that represents the class
* file as a whole, plus any `ClassFile`s that have been synthesized in the process
* of parsing it.
*
* The result will always contain at least one `ClassFile` object, namely the one that
* is created from this method's parameters. Regardless of how many `ClassFile`s the
* result contains, the `ClassFile` created from this method's parameters will always
* be the result's first element.
*/
protected def ClassFile(
cp: Constant_Pool,
minor_version: Int,
major_version: Int,
access_flags: Int,
this_class: Constant_Pool_Index,
super_class: Constant_Pool_Index,
interfaces: Interfaces,
fields: Fields,
methods: Methods,
attributes: Attributes
): ClassFile
//
// IMPLEMENTATION
//
import ClassFileReader.ExceptionHandler
final val defaultExceptionHandler: ExceptionHandler = (source, t) ⇒ {
error("class file reader", s"processing $source failed", t)
}
private[this] var classFilePostProcessors = RefArray.empty[List[ClassFile] ⇒ List[ClassFile]]
/**
* Register a class file post processor. A class file post processor
* can transform the completely read and reified class file. Post processors
* can only be registered before the usage of a class file reader. '''Registering
* new `ClassFilePostProcessors` while processing class files is not supported
* and the behavior is undefined'''.
*
* @note `PostProcessors` will be executed in last-in-first-out order.
*/
def registerClassFilePostProcessor(p: List[ClassFile] ⇒ List[ClassFile]): Unit = {
classFilePostProcessors :+= p
}
/**
* Template method that reads a Java class file from the given input stream.
*
* All other methods to read a class file use this method to eventually parse a
* class file.
*
* ==Class File Structure==
* Parses a class file according to the specification:
*
* ClassFile {
* u4 magic;
* u2 minor_version;
* u2 major_version;
* u2 constant_pool_count;
* cp_info constant_pool[constant_pool_count-1];
* u2 access_flags;
* u2 this_class;
* u2 super_class;
* u2 interfaces_count;
* u2 interfaces[interfaces_count];
* u2 fields_count;
* field_info fields[fields_count];
* u2 methods_count;
* method_info methods[methods_count];
* u2 attributes_count;
* attribute_info attributes[attributes_count];
* }
*
*
* @param in The `DataInputStream` from which the class file will be read. The
* stream is not closed by this method.
* '''It is highly recommended that the stream is buffered; otherwise the
* performance will be terrible!'''
*/
def ClassFile(in: DataInputStream): List[ClassFile] = {
// magic
val readMagic = in.readInt
if (ClassFileMagic != readMagic)
throw BytecodeProcessingFailedException("the file does not start with 0xCAFEBABE")
val minor_version = in.readUnsignedShort
val major_version = in.readUnsignedShort
def unsupportedVersion =
s"unsupported class file version: $major_version.$minor_version"+
" (Supported: 45(Java 1.1) <= version <= "+
s"$LatestSupportedJavaMajorVersion(${jdkVersion(LatestSupportedJavaMajorVersion)}))"
// let's make sure that we support this class file's version
if (major_version < 45) // at least JDK 1.1 or we back out
throw BytecodeProcessingFailedException(unsupportedVersion)
if (major_version > LatestSupportedJavaMajorVersion || (
major_version == LatestSupportedJavaMajorVersion
&& minor_version > LatestSupportedJavaVersion.minor
)) {
// Just log an error message for newer version, we might still be able to handle the
// class if it doesn't use any features introduced in an unsupported version
error("class file reader", unsupportedVersion)
}
val cp = Constant_Pool(in)
val access_flags = in.readUnsignedShort
val this_class = in.readUnsignedShort
val super_class = in.readUnsignedShort
val interfaces = {
val interfaces_count = in.readUnsignedShort
fillArrayOfInt(interfaces_count) { in.readUnsignedShort }
}
val fields = Fields(cp, in)
val methods = Methods(cp, in)
val attributes = Attributes(cp, AttributesParent.ClassFile, this_class, -1, in)
var classFile = ClassFile(
cp,
minor_version, major_version,
access_flags,
this_class, super_class, interfaces,
fields, methods,
attributes
)
// Perform transformations that are specific to this class file.
// (Used, e.g., to finally resolve the invokedynamic instructions.)
classFile = applyDeferredActions(cp, classFile)
// Perform general transformations on class files.
classFilePostProcessors.foldLeft(List(classFile)) { (classFiles, postProcessor) ⇒
postProcessor(classFiles)
}
}
//
// CONVENIENCE METHODS TO LOAD CLASS FILES FROM VARIOUS SOURCES
//
/**
* Reads in a class file.
*
* @param create A function that creates a new `InputStream` and
* which must not return `null`. If you already do have an open input stream
* which should not be closed after reading the class file use
* `...ClassFileReader.ClassFile(java.io.DataInputStream) : ClassFile` instead.
* The (newly created) `InputStream` returned by calling `create` is closed by
* this method.
* The created input stream will automatically be wrapped by OPAL to enable
* efficient reading of the class file.
*/
def ClassFile(create: () ⇒ InputStream): List[ClassFile] = {
process(create()) {
case null ⇒
throw new IllegalArgumentException("the created stream is null")
case dis: DataInputStream ⇒ ClassFile(dis)
case bis: BufferedInputStream ⇒ ClassFile(new DataInputStream(bis))
case bas: ByteArrayInputStream ⇒ ClassFile(new DataInputStream(bas))
case is ⇒
ClassFile(new DataInputStream(new BufferedInputStream(is)))
}
}
def isClassFileRepository(filename: String, containerName: Option[String]): Boolean = {
if (containerName.isDefined) {
// We don't want to extract inner jars,... from jmods (the default jmods contain
// jars which contain class files also found in the jmods.)
val containerNameLength = containerName.get.length
if (containerNameLength > 5 && containerName.get.endsWith(".jmod")) {
return false;
}
}
val filenameLength = filename.length
filenameLength > 4 && {
val ending = filename.substring(filenameLength - 4, filenameLength).toLowerCase
(ending == "jmod" && filename.charAt(filenameLength - 5) == '.') ||
ending == ".jar" || ending == ".zip" || ending == ".war" || ending == ".ear"
}
}
protected[this] def ClassFile(jarFile: ZipFile, jarEntry: ZipEntry): List[ClassFile] = {
process(jarFile.getInputStream(jarEntry)) { in ⇒
ClassFile(new DataInputStream(new BufferedInputStream(in)))
}
}
/**
* Reads in a single class file from a Jar file.
*
* @param jarFile An existing ZIP/JAR file that contains class files.
* @param jarFileEntryName The name of a class file stored in the specified ZIP/JAR file.
*/
@throws[java.io.IOException]("if the file is empty or the entry cannot be found")
def ClassFile(jarFile: File, jarFileEntryName: String): List[ClassFile] = {
if (jarFile.length() == 0)
throw new IOException(s"the file $jarFile is empty");
val levenshteinDistance = new LevenshteinDistance()
process(new ZipFile(jarFile)) { zf ⇒
val jarEntry = zf.getEntry(jarFileEntryName)
if (jarEntry == null) {
var names: List[(Int, String)] = Nil
val zfEntries = zf.entries()
while (zfEntries.hasMoreElements) {
val zfEntry = zfEntries.nextElement()
val zfEntryName = zfEntry.getName
val distance = levenshteinDistance(zfEntryName, jarFileEntryName).intValue()
names = (distance, zfEntryName) :: names
}
val mostRelatedNames = names.sortWith((l, r) ⇒ l._1 < r._1).map(_._2).take(15)
val ending = if (mostRelatedNames.length > 15) ", ...)" else ")"
val messageHeader = s"the file $jarFile does not contain $jarFileEntryName"
val message = mostRelatedNames.mkString(s"$messageHeader (similar: ", ", ", ending)
throw new IOException(message)
}
ClassFile(zf, jarEntry)
}
}
/**
* Reads in a single class file from a Jar file.
*
* @param jarFileName the name of an existing ZIP/JAR file that contains class files.
* @param jarFileEntryName the name of a class file stored in the specified ZIP/JAR file.
*/
@throws[java.io.IOException]("if the file is empty or the entry cannot be found")
def ClassFile(jarFileName: String, jarFileEntryName: String): List[ClassFile] = {
ClassFile(new File(jarFileName), jarFileEntryName)
}
/**
* Reads in parallel all class files stored in the given jar/zip file.
*
* @param jarFile Some valid (non-empty) jar File.
* @return The loaded class files.
*/
def ClassFiles(
jarFile: ZipFile,
exceptionHandler: ExceptionHandler
): List[(ClassFile, URL)] = {
val Lock = new Object
var classFiles: List[(ClassFile, URL)] = Nil
def addClassFile(cf: ClassFile, url: URL): Unit = {
Lock.synchronized {
classFiles ::= ((cf, url))
}
}
ClassFiles(jarFile, addClassFile, exceptionHandler)
classFiles
}
/**
* Reads the class files from the given JarInputStream
*/
// The following solution is inspired by Ben Hermann's solution found at:
// https://github.com/delphi-hub/delphi-crawler/blob/feature/streamworkaround/src/main/scala/de/upb/cs/swt/delphi/crawler/tools/JarStreamReader.scala
// and
// https://github.com/delphi-hub/delphi-crawler/blob/develop/src/main/scala/de/upb/cs/swt/delphi/crawler/tools/ClassStreamReader.scala
def ClassFiles(in: ⇒ JarInputStream): List[(ClassFile, String)] = process(in) { in ⇒
var je: JarEntry = in.getNextJarEntry()
var futures: List[Future[List[(ClassFile, String)]]] = Nil
while (je != null) {
val entryName = je.getName
if (entryName.endsWith(".class") || entryName.endsWith(".jar")) {
val entryBytes = {
val baos = new ByteArrayOutputStream()
val buffer = new Array[Byte](32 * 1024)
Stream.continually(in.read(buffer)).takeWhile(_ > 0).foreach { bytesRead ⇒
baos.write(buffer, 0, bytesRead)
baos.flush()
}
baos.toByteArray
}
futures ::= Future[List[(ClassFile, String)]] {
if (entryName.endsWith(".class")) {
val cfs = ClassFile(new DataInputStream(new ByteArrayInputStream(entryBytes)))
cfs map { cf ⇒ (cf, entryName) }
} else { // ends with ".jar"
info("class file reader", s"reading inner jar $entryName")
ClassFiles(new JarInputStream(new ByteArrayInputStream(entryBytes)))
}
}(
// we can't use the OPALExecutionContext here, because the number of
// threads is bounded and (depending on the nesting level, we may need
// more threads..)
ExecutionContext.global
)
}
je = in.getNextJarEntry()
}
futures.flatMap(f ⇒ Await.result(f, Duration.Inf))
}
/**
* Reads '''in parallel''' all class files stored in the given jar file. For each
* successfully read class file the function `classFileHandler` is called.
*
* @param zipFile A valid zip file that contains `.class` files and other
* `.jar` files; other files are ignored. Inner jar files are also unzipped.
* @param classFileHandler A function that is called for each class file in
* the given jar file.
* Given that the jarFile is read in parallel '''this function has to be
* thread safe'''.
* @param exceptionHandler The exception handler that is called when the reading
* of a class file fails. '''This function has to be thread safe'''.
*/
def ClassFiles(
zipFile: ZipFile,
classFileHandler: (ClassFile, URL) ⇒ Unit,
exceptionHandler: ExceptionHandler
): Unit = {
val zipFileURL = new File(zipFile.getName).toURI.toURL.toExternalForm
val jarFileName = s"jar:$zipFileURL!/"
ClassFiles(jarFileName, zipFile, classFileHandler, exceptionHandler)
}
private def ClassFiles(
jarFileURL: String, // the complete path to the given jar file.
jarFile: ZipFile,
classFileHandler: (ClassFile, URL) ⇒ Unit,
exceptionHandler: ExceptionHandler
): Unit = {
import scala.collection.JavaConverters._
// First let's collect all inner Jar Entries, then do the processing.
// Otherwise - if the OPALExecutionContextTaskSupport uses a fixed
// sized thread pool - we may run out of threads... to process anything.
val innerJarEntries = new ConcurrentLinkedQueue[ZipEntry]
val jarEntries = jarFile.entries.asScala.toArray
val nextEntryIndex = new AtomicInteger(jarEntries.length - 1)
val parallelismLevel = NumberOfThreadsForIOBoundTasks
val futures: Array[Future[Unit]] = new Array(parallelismLevel)
var futureIndex = 0
while (futureIndex < parallelismLevel) {
futures(futureIndex) = Future[Unit] {
var index = -1
while ({ index = nextEntryIndex.getAndDecrement; index } >= 0) {
val jarEntry = jarEntries(index)
if (!jarEntry.isDirectory && jarEntry.getSize > 0) {
val jarEntryName = jarEntry.getName
if (jarEntryName.endsWith(".class")) {
try {
val url = new URL(jarFileURL + jarEntry.getName)
val classFiles = ClassFile(jarFile, jarEntry)
classFiles foreach (classFile ⇒ classFileHandler(classFile, url))
} catch {
case ct: ControlThrowable ⇒ throw ct
case t: Throwable ⇒ exceptionHandler(jarEntryName, t)
}
} else if (isClassFileRepository(jarEntryName, Some(jarFile.getName))) {
innerJarEntries.add(jarEntry)
}
}
}
}(org.opalj.concurrent.OPALHTBoundedExecutionContext)
futureIndex += 1
}
while ({ futureIndex -= 1; futureIndex } >= 0) {
Await.ready(futures(futureIndex), Duration.Inf)
}
innerJarEntries.iterator().forEachRemaining { jarEntry ⇒
// TODO make this commons.vfs compatible...
// jar:jar/...!/...!...
val nextJarFileURL = s"${jarFileURL}jar:${jarEntry.getName}!/"
try {
val jarData = new Array[Byte](jarEntry.getSize.toInt)
val din = new DataInputStream(jarFile.getInputStream(jarEntry))
din.readFully(jarData)
din.close()
ClassFiles(nextJarFileURL, jarData, classFileHandler, exceptionHandler)
} catch {
case ct: ControlThrowable ⇒ throw ct
case t: Throwable ⇒ exceptionHandler(nextJarFileURL, t)
}
}
}
/**
* Loads class files from an in-memory representation of a jar file given in form
* of a byte array.
* This is done by writing the jar file data to a temporary file and then loading
* the class files from it as done with any other jar file.
*/
private def ClassFiles(
jarFileURL: String,
jarData: Array[Byte],
classFileHandler: (ClassFile, URL) ⇒ Unit,
exceptionHandler: ExceptionHandler
): Unit = {
val pathToEntry = jarFileURL.substring(0, jarFileURL.length - 3)
val entry = pathToEntry.substring(pathToEntry.lastIndexOf('/') + 1)
try {
val jarFile = File.createTempFile(entry, ".zip")
process { new java.io.FileOutputStream(jarFile) } { fout ⇒ fout.write(jarData) }
ClassFiles(jarFileURL, new ZipFile(jarFile), classFileHandler, exceptionHandler)
jarFile.delete()
} catch {
case ct: ControlThrowable ⇒ throw ct
case t: Throwable ⇒ exceptionHandler(pathToEntry, t)
}
}
private[this] def processJar(
file: File,
exceptionHandler: ExceptionHandler = defaultExceptionHandler
): List[(ClassFile, URL)] = {
try {
process(new ZipFile(file)) { zf ⇒ ClassFiles(zf, exceptionHandler) }
} catch {
case e: Exception ⇒ { exceptionHandler(file, e); Nil }
}
}
private[this] def processClassFile(
file: File,
exceptionHandler: ExceptionHandler = defaultExceptionHandler
): List[(ClassFile, URL)] = {
try {
process(
new DataInputStream(new BufferedInputStream(new FileInputStream(file)))
) { in ⇒ ClassFile(in).map(classFile ⇒ (classFile, file.toURI.toURL)) }
} catch {
case e: Exception ⇒ { exceptionHandler(file, e); Nil }
}
}
/**
* Loads class files from the given file location.
* - If the file denotes a single ".class" file this class file is loaded.
* - If the file object denotes a ".jar|.war|.ear|.zip" file, all class files in the
* jar file will be loaded.
* - If the file object specifies a directory object, all ".class" files
* in the directory and in all subdirectories are loaded as well as all
* class files stored in ".jar" files in one of the directories. This class loads
* all class files in parallel. However, this does not effect analyses working on the
* resulting `List`.
*/
def ClassFiles(
file: File,
exceptionHandler: ExceptionHandler = defaultExceptionHandler
): List[(ClassFile, URL)] = {
if (!file.exists()) {
Nil
} else if (file.isFile) {
val filename = file.getName
if (file.length() == 0) Nil
else if (isClassFileRepository(filename, None)) processJar(file)
else if (filename.endsWith(".class")) processClassFile(file)
else Nil
} else if (file.isDirectory) {
val jarFiles = ArrayBuffer.empty[File]
val classFiles = ArrayBuffer.empty[File]
def collectFiles(files: Array[File]): Unit = {
if (files eq null)
return ;
files foreach { file ⇒
val filename = file.getName
if (file.isFile) {
if (file.length() == 0) Nil
else if (isClassFileRepository(filename, None)) jarFiles += file
else if (filename.endsWith(".class")) classFiles += file
} else if (file.isDirectory) {
collectFiles(file.listFiles())
} else {
info(
"class file reader",
s"ignored: $file it is neither a file nor a directory"
)
}
}
}
// 1. get the list of all files in the directory as well as all subdirectories
collectFiles(file.listFiles())
// 2. get all class files
var allClassFiles = List.empty[(ClassFile, URL)]
// 2.1 load - in parallel - all ".class" files
if (classFiles.nonEmpty) {
val theClassFiles = new ConcurrentLinkedQueue[(ClassFile, URL)]
parForeachSeqElement(classFiles, NumberOfThreadsForIOBoundTasks) { classFile ⇒
theClassFiles.addAll(processClassFile(classFile, exceptionHandler).asJava)
}
allClassFiles ++= theClassFiles.asScala
}
// 2.2 load - one after the other - all ".jar" files (processing jar files
// is already parallelized.)
jarFiles.foreach { jarFile ⇒ allClassFiles ++= processJar(jarFile) }
// 3. return all loaded class files
allClassFiles
} else {
throw new UnknownError(s"$file is neither a file nor a directory")
}
}
def AllClassFiles(
files: Traversable[File],
exceptionHandler: ExceptionHandler = defaultExceptionHandler
): Traversable[(ClassFile, URL)] = {
files.flatMap(file ⇒ ClassFiles(file, exceptionHandler))
}
/** Returns the class files of the current Java Runtime Image grouped by module. */
def JRTClassFiles: Iterable[(String, List[(ClassFile, URL)])] = {
def traverseModule(module: Path): List[(ClassFile, URL)] = {
var allClassFiles = List.empty[(ClassFile, URL)]
def traversePath(p: Path): Unit = {
if (Files.isDirectory(p)) {
try {
for (subPath ← Files.newDirectoryStream(p, "*").asScala) {
traversePath(subPath)
}
} catch {
case e: Exception ⇒ {
error(
"class file reader",
"failed processing Java 9+ Runtime Image (jrt:/)",
e
)
}
}
} else if (p.getFileName.toString.endsWith(".class")) {
val cf = ClassFile(() ⇒ Files.newInputStream(p))
allClassFiles = cf.map(c ⇒ (c, p.toUri.toURL)) ++: allClassFiles
}
}
traversePath(module)
allClassFiles
}
val allModulesPath = FileSystems.getFileSystem(URI.create("jrt:/")).getPath("/modules")
for {
modulePath ← Files.newDirectoryStream(allModulesPath, "*").asScala
if Files.isDirectory(modulePath)
} yield {
(modulePath.getFileName.toString, traverseModule(modulePath))
}
}
/**
* Goes over all files in parallel and calls back the given function which has to be thread-safe!
*/
def processClassFiles(
files: Traversable[File],
progressReporter: File ⇒ Unit,
classFileProcessor: ((ClassFile, URL)) ⇒ Unit,
exceptionHandler: ExceptionHandler = defaultExceptionHandler
): Unit = {
val ts = Tasks[File] { (tasks: Tasks[File], file: File) ⇒
if (file.isFile && file.length() > 0) {
val filename = file.getName
if (isClassFileRepository(filename, None)) {
if (!filename.endsWith("-javadoc.jar") &&
!filename.endsWith("-sources.jar")) {
progressReporter(file)
processJar(file, exceptionHandler).foreach(classFileProcessor)
}
} else if (filename.endsWith(".class")) {
progressReporter(file)
processClassFile(file, exceptionHandler).foreach(classFileProcessor)
}
} else if (file.isDirectory) {
progressReporter(file)
file.listFiles().foreach(tasks.submit)
}
}(
// We need a fresh/privately owned execution context with a fixed number of threads
// to avoid that – if the processor also uses the fixed size pool –
// we potentially run out of threads!
BoundedExecutionContext(
"ClassFileReader.processClassFiles",
NumberOfThreadsForIOBoundTasks
)
)
files.foreach(ts.submit)
ts.join()
}
/**
* Searches for the first class file that is accepted by the filter. If no class file
* can be found that is accepted by the filter the set of all class names is returned.
*
* @param files Some file. If the file names a .jar file the .jar file is opened and
* searched for a corresponding class file. If the file identifies a "directory"
* then, all files in that directory are processed.
*/
def findClassFile(
files: Traversable[File],
progressReporter: File ⇒ Unit,
classFileFilter: ClassFile ⇒ Boolean,
className: ClassFile ⇒ String,
exceptionHandler: ExceptionHandler = defaultExceptionHandler
): Either[(ClassFile, URL), Set[String]] = {
var classNames = Set.empty[String]
files.filter(_.exists()) foreach { file ⇒
if (file.isFile && file.length() > 0) {
val filename = file.getName
(
if (isClassFileRepository(filename, None)) {
if (!filename.endsWith("-javadoc.jar") &&
!filename.endsWith("-sources.jar")) {
progressReporter(file)
processJar(file, exceptionHandler)
} else {
Nil
}
} else if (filename.endsWith(".class")) {
progressReporter(file)
processClassFile(file, exceptionHandler)
} else {
Nil
}
) filter { cfSource ⇒
val (cf, _) = cfSource
classNames += className(cf)
classFileFilter(cf)
} foreach { e ⇒ return Left(e); }
} else if (file.isDirectory) {
file.listFiles { (dir: File, name: String) ⇒
dir.isDirectory || isClassFileRepository(file.toString, None)
} foreach { f ⇒
findClassFile(
List(f), progressReporter, classFileFilter, className, exceptionHandler
) match {
case Left(cf) ⇒
return Left(cf);
case Right(moreClassNames) ⇒
classNames ++= moreClassNames
/*nothing else to do... let's continue*/
}
}
}
}
Right(classNames)
}
}
/**
* Helper methods related to reading class files.
*/
object ClassFileReader {
type ExceptionHandler = (AnyRef, Throwable) ⇒ Unit
final val SuppressExceptionHandler: ExceptionHandler = (_, _) ⇒ {}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy