diff --git a/.classpath b/.classpath index 5576b4361..dff964fcf 100644 --- a/.classpath +++ b/.classpath @@ -22,6 +22,7 @@ + @@ -36,5 +37,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..4391ce004 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: lukehutch diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..15cffe75f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "daily" + time: "02:00" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "03:00" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..d87baada3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: Java CI + +on: + pull_request: + branches: + - latest + push: + branches: + - latest + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + java: [ '8', '11', '13', '15', '16', '17', '18', '19' ] + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: ${{ matrix.java }} + - name: print Java version + run: java -version + - name: Build with Maven + run: ./mvnw --no-transfer-progress -B clean test diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..8006381f8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,47 @@ +name: "CodeQL" + +on: + push: + branches: [ "latest" ] + pull_request: + branches: [ "latest" ] + schedule: + - cron: "47 11 * * 6" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ java ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java SDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 8 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.gitignore b/.gitignore index 46cc9ac4a..b39e9e349 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ +!.gitignore + pom.xml.releaseBackup release.properties *.class !CompiledWithJDK8.class +!module-info.class + /target/ bin/ tmp/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..44f3cf2c1 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/.project b/.project index b8e1f6a83..8b3ccf423 100644 --- a/.project +++ b/.project @@ -17,8 +17,18 @@ - org.eclipse.pde.PluginNature org.eclipse.m2e.core.maven2Nature org.eclipse.jdt.core.javanature + + + 1700088758021 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + diff --git a/.travis.yml b/.travis.yml index 935fbadb5..3a0a18254 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,16 @@ language: java jdk: - openjdk8 - - openjdk11 # LTS - - openjdk-ea # Early access + - openjdk11 + - openjdk12 + - openjdk13 + #- openjdk-ea -sudo: true # https://github.com/travis-ci/travis-ci/issues/6593 +#ignore default install step +install: true cache: directories: - $HOME/.m2 + +script: ./mvnw clean verify diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..7b016a89f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE-ClassGraph.txt similarity index 96% rename from LICENSE rename to LICENSE-ClassGraph.txt index fcdf34dec..eddec3610 100644 --- a/LICENSE +++ b/LICENSE-ClassGraph.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Luke Hutchison +Copyright (c) 2019 Luke Hutchison Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4fe571e13..7f48fb220 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # ClassGraph -ClassGraph Logo       Duke Award Logo +ClassGraph Logo       Duke Award logo     Google Open Source Peer Bonus logo -ClassGraph (formerly **FastClasspathScanner**) is an uber-fast, ultra-lightweight, parallelized classpath scanner, module scanner, and build-time/runtime annotation processor for Java, Scala, Kotlin and other JVM languages. +ClassGraph is an uber-fast parallelized classpath scanner and module scanner for Java, Scala, Kotlin and other JVM languages. -| _ClassGraph won a Duke's Choice Award (a recognition of the most useful and/or innovative software in the Java ecosystem) at Oracle Code One 2018._ Thanks to all the users who have reported bugs, requested features, offered suggestions, and submitted pull requests to help get ClassGraph to where it is today. | +| _ClassGraph won a Duke's Choice Award (a recognition of the most useful and/or innovative software in the Java ecosystem) at Oracle Code One 2018, and a Google Open Source Peer Bonus award in 2022._ Thanks to all the users who have reported bugs, requested features, offered suggestions, and submitted pull requests to help get ClassGraph to where it is today. | |-----------------------------| [![Platforms: Windows, Mac OS X, Linux, Android (build-time)](https://img.shields.io/badge/platforms-Windows,_Mac_OS_X,_Linux,_Android_(build--time)-blue.svg)](#) @@ -15,20 +15,25 @@ ClassGraph (formerly **FastClasspathScanner**) is an uber-fast, ultra-lightweigh [![GitHub issues](https://img.shields.io/github/issues/classgraph/classgraph.svg)](https://github.com/classgraph/classgraph/issues/) [![lgtm alerts](https://img.shields.io/lgtm/alerts/g/classgraph/classgraph.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/classgraph/classgraph/alerts/) [![lgtm code quality](https://img.shields.io/lgtm/grade/java/g/classgraph/classgraph.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/classgraph/classgraph/context:java) -[![Codacy Badge](https://img.shields.io/codacy/grade/31d639dd3874454c917f80ea2bff8155.svg?style=flat)](https://www.codacy.com/app/lukehutch/classgraph) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/ebc65f685d504cfcb379533d28d6353c)](https://www.codacy.com/gh/classgraph/classgraph/dashboard?utm_source=github.com&utm_medium=referral&utm_content=classgraph/classgraph&utm_campaign=Badge_Grade) +
+[![Dependencies: none](https://img.shields.io/badge/dependencies-none-blue.svg)](#) +[![Dependents](https://badgen.net/github/dependents-repo/classgraph/classgraph)](https://github.com/classgraph/classgraph/network/dependents?package_id=UGFja2FnZS0xODcxNTE4NTM%3D) +[![GitHub stars chart](https://img.shields.io/badge/github%20stars-chart-blue.svg)](https://seladb.github.io/StarTrack-js/#/preload?r=classgraph,classgraph)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.classgraph/classgraph/badge.svg)](https://mvnrepository.com/artifact/io.github.classgraph/classgraph) [![Javadocs](http://www.javadoc.io/badge/io.github.classgraph/classgraph.svg)](https://javadoc.io/doc/io.github.classgraph/classgraph)
-[![License: MIT](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/classgraph/classgraph/blob/master/LICENSE) -[![Dependencies: none](https://img.shields.io/badge/dependencies-none-lightgrey.svg)](#) +[![Gitter chat](https://img.shields.io/badge/gitter-join%20chat-blue.svg)](https://gitter.im/classgraph/Lobby)
-[![GitHub stars chart](https://img.shields.io/badge/github%20stars-chart-yellow.svg)](https://seladb.github.io/StarTrack-js/?u=classgraph&r=classgraph) -[![Gitter chat](https://img.shields.io/badge/gitter-join%20chat-yellow.svg)](https://gitter.im/classgraph/Lobby) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/classgraph/classgraph/blob/master/LICENSE) + +| ClassGraph is stable and mature, and has a low bug report rate, despite being used by hundreds of projects. | +|-----------------------------| ### ClassGraph vs. Java Introspection -ClassGraph has the ability to "invert" the Java class and/or reflection API, or has the ability to index classes and resources. For example, the Java class and reflection API can tell you the interfaces implemented by a given class, or can give you the list of annotations on a class; ClassGraph can find **all classes that implement a given interface**, or can find **all classes that are annotated with a given annotation**. The Java API can load the content of a resource file with a specific path in a specific ClassLoader, but ClassGraph can find and load **all resources in all classloaders with paths matching a given pattern**. +ClassGraph has the ability to "invert" the Java class and/or reflection API, or has the ability to index classes and resources. For example, the Java class and reflection API can tell you the superclass of a given class, or the interfaces implemented by a given class, or can give you the list of annotations on a class; ClassGraph can find **all classes that extend a given class** (all subclasses of a given class), or **all classes that implement a given interface**, or **all classes that are annotated with a given annotation**. The Java API can load the content of a resource file with a specific path in a specific ClassLoader, but ClassGraph can find and load **all resources in all classloaders with paths matching a given pattern**. ### Examples @@ -39,10 +44,10 @@ String pkg = "com.xyz"; String routeAnnotation = pkg + ".Route"; try (ScanResult scanResult = new ClassGraph() - .verbose() // Log to stderr - .enableAllInfo() // Scan classes, methods, fields, annotations - .whitelistPackages(pkg) // Scan com.xyz and subpackages (omit to scan all packages) - .scan()) { // Start the scan + .verbose() // Log to stderr + .enableAllInfo() // Scan classes, methods, fields, annotations + .acceptPackages(pkg) // Scan com.xyz and subpackages (omit to scan all packages) + .scan()) { // Start the scan for (ClassInfo routeClassInfo : scanResult.getClassesWithAnnotation(routeAnnotation)) { AnnotationInfo routeAnnotationInfo = routeClassInfo.getAnnotationInfo(routeAnnotation); List routeParamVals = routeAnnotationInfo.getParameterValues(); @@ -56,10 +61,11 @@ try (ScanResult scanResult = The following code finds all JSON files in `META-INF/config` in all ClassLoaders or modules, and calls the method `readJson(String path, String content)` with the path and content of each file. ```java -try (ScanResult scanResult = new ClassGraph().whitelistPathsNonRecursive("META-INF/config").scan()) { - scanResult.getResourcesWithExtension("json").forEachByteArray((Resource res, byte[] content) -> { - readJson(res.getPath(), new String(content, StandardCharsets.UTF_8)); - }); +try (ScanResult scanResult = new ClassGraph().acceptPathsNonRecursive("META-INF/config").scan()) { + scanResult.getResourcesWithExtension("json") + .forEachByteArray((Resource res, byte[] content) -> { + readJson(res.getPath(), new String(content, StandardCharsets.UTF_8)); + }); } ``` @@ -69,49 +75,109 @@ See the [code examples](https://github.com/classgraph/classgraph/wiki/Code-examp ClassGraph provides a number of important capabilities to the JVM ecosystem: -* ClassGraph has the ability to build a model in memory of the entire relatedness graph of all classes, annotations, interfaces, methods and fields that are visible to the JVM. This graph can be [queried in a wide range of ways](https://github.com/classgraph/classgraph/wiki/Code-examples), enabling some degree of *metaprogramming* in JVM languages -- the ability to write code that analyzes or responds to the properties of other code. +* ClassGraph has the ability to build a model in memory of the entire relatedness graph of all classes, annotations, interfaces, methods and fields that are visible to the JVM, and can even read [type annotations](https://docs.oracle.com/javase/tutorial/java/annotations/type_annotations.html). This graph of class metadata can be [queried in a wide range of ways](https://github.com/classgraph/classgraph/wiki/Code-examples), enabling some degree of *metaprogramming* in JVM languages -- the ability to write code that analyzes or responds to the properties of other code. * ClassGraph reads the classfile bytecode format directly, so it can read all information about classes without loading or initializing them. * ClassGraph is fully compatible with the new JPMS module system (Project Jigsaw / JDK 9+), i.e. it can scan both the traditional classpath and the module path. However, the code is also fully backwards compatible with JDK 7 and JDK 8 (i.e. the code is compiled in Java 7 compatibility mode, and all interaction with the module system is implemented via reflection for backwards compatibility). -* ClassGraph scans the classpath or module path using [carefully optimized multithreaded code](https://github.com/classgraph/classgraph/wiki/How-fast-is-ClassGraph%3F) for the shortest possible scan times, and it runs as close as possible to I/O bandwidth limits, even on a fast SSD. +* ClassGraph scans the classpath or module path using [carefully optimized multithreaded code](https://github.com/classgraph/classgraph/wiki/How-fast-is-ClassGraph) for the shortest possible scan times, and it runs as close as possible to I/O bandwidth limits, even on a fast SSD. * ClassGraph handles more [classpath specification mechanisms](https://github.com/classgraph/classgraph/wiki/Classpath-specification-mechanisms) found in the wild than any other classpath scanner, making code that depends upon ClassGraph maximally portable. * ClassGraph can scan the classpath and module path either at runtime or [at build time](https://github.com/classgraph/classgraph/wiki/Build-Time-Scanning) (e.g. to implement annotation processing for Android). * ClassGraph can [find classes that are duplicated or defined more than once in the classpath or module path](https://github.com/classgraph/classgraph/wiki/Code-examples#find-all-duplicate-class-definitions-in-the-classpath-or-module-path), which can help find the cause of strange class resolution behaviors. -* ClassGraph can [create GraphViz visualizations of the class graph structure](https://github.com/classgraph/classgraph/wiki/API:-ClassInfo#generating-a-graphviz-dot-file-for-class-graph-visualization), which can help with code understanding: (click to enlarge | [see graph legend here](https://github.com/classgraph/classgraph/blob/master/src/test/java/com/xyz/classgraph-fig-legend.png)) +* ClassGraph can [create GraphViz visualizations of the class graph structure](https://github.com/classgraph/classgraph/wiki/ClassInfo-API#generating-a-graphviz-dot-file-for-class-graph-visualization), which can help with code understanding: (click to enlarge; [see graph legend here](https://github.com/classgraph/classgraph/blob/master/src/test/java/com/xyz/classgraph-fig-legend.png))

- Class graph visualization + Class graph visualization

-## Documentation +## Downloading -[See the wiki for complete documentation and usage information.](https://github.com/classgraph/classgraph/wiki) +### Maven dependency + +Replace `X.Y.Z` below with the latest [release number](https://github.com/classgraph/classgraph/releases). (Alternatively, you could use `LATEST` in place of `X.Y.Z` instead if you just want to grab the latest version -- although be aware that that may lead to non-reproducible builds, since the ClassGraph version number could increase at any time. You could use [dependency locking](https://docs.gradle.org/current/userguide/dependency_locking.html) to address this.) + +```xml + + io.github.classgraph + classgraph + X.Y.Z + +``` + +See instructions for [use as a module](https://github.com/classgraph/classgraph/wiki#use-as-a-module). + +### Running on JDK 16+ + +The JDK team decided to start enforcing strong encapsulation in JDK 16+. That will means that by default, ClassGraph will not be able to find the classpath of your project, if all of the following are true: + +* You are running on JDK 16+ +* You are using a legacy classloader (rather than the module system) +* Your classloader does not expose its classpath via a public field or method (i.e. the full classpath can only be determined by reflection of private fields or methods). + +If your ClassGraph code works in JDK versions less than 16 but breaks in JDK 16+ (meaning that ClassGraph can no longer find your classes), you have probably run into this problem. + +ClassGraph can use either of the following libraries to silently circumvent all of Java's security mechanisms (visibility/access checks, security manager restrictions, and strong encapsulation), in order to read the classpath from private fields and methods of classloaders. + +* Narcissus by Luke Hutchison (@lukehutch), author of ClassGraph +* JVM-Driver by Roberto Gentili (@burningwave), author of [Burningwave Core](https://github.com/burningwave/core). + +**To clarify, you do *only* need to use Narcissus or JVM-driver if ClassGraph cannot find the classpath elements from your classloader, due to the enforcement of strong encapsulation, or if it is problematic that you are getting reflection access warnings on the console.** -## Status +To use one of these libraries: -**FastClasspathScanner was renamed to ClassGraph, and released as version 4**. +* Upgrade ClassGraph to the latest version +* Either: + 1. Add the [Narcissus](https://github.com/toolfactory/narcissus) library to your project as an extra dependency (this includes a native library, and only Linux x86/x64, Windows x86/x64, and Mac OS X x64 are currently supported -- feel free to contribute native code builds for other platforms or architectures). + 2. Set `ClassGraph.CIRCUMVENT_ENCAPSULATION = CircumventEncapsulationMethod.NARCISSUS;` before interacting with ClassGraph in any other way (this will load the Narcissus library as ClassGraph's reflection driver). +* Or: + 1. Add the [JVM-Driver](https://github.com/toolfactory/jvm-driver) library to your project as an extra dependency (this uses only Java code and works to bypass encapsulation without native code for all JDK versions between 8 and 18). + 2. Set `ClassGraph.CIRCUMVENT_ENCAPSULATION = CircumventEncapsulationMethod.JVM_DRIVER;` before interacting with ClassGraph in any other way (this will load the JVM-Driver library as ClassGraph's reflection driver). -ClassGraph has a completely revamped API. See the [porting notes](https://github.com/classgraph/classgraph/wiki/Porting-FastClasspathScanner-code-to-ClassGraph) for information on porting from the older FastClasspathScanner version 3 API. +JDK 16's strong encapsulation is just the first step of trying to lock down Java's internals, so further restrictions are possible (e.g. it is likely that `setAccessible(true)` will fail in future JDK releases, even within a module, and probably the JNI API will be locked down soon, making Narcissus require a commandline flag to work). Therefore, **please convince your upstream runtime environment maintainers to expose the full classpath from their classloader using a public method or field, otherwise ClassGraph may stop working for your runtime environment in the future.** -In particular, the Maven group id has changed from `io.github.lukehutch.fast-classpath-scanner` to **`io.github.classgraph`** in version 4. Please see the new [Maven dependency rule](https://github.com/classgraph/classgraph/wiki) and module "requires" line in the Wiki documentation. +### Pre-built JARs + +You can get pre-built JARs (usable on JRE 7 or newer) from [Sonatype](https://oss.sonatype.org/#nexus-search;quick~io.github.classgraph). + +### Building from source + +ClassGraph must be built on JDK 8 or newer (due to the presence of `@FunctionalInterface` annotations on some interfaces), but is built using `-target 1.7` for backwards compatibility with JRE 7. + +The following commands will build the most recent version of ClassGraph from git master. The compiled package will then be in the "classgraph/target" directory. + +```bash +git clone https://github.com/classgraph/classgraph.git +cd classgraph +export JAVA_HOME=/usr/java/default # Or similar -- Maven needs JAVA_HOME +./mvnw -Dmaven.test.skip=true package +``` + +This will allow you to build a local SNAPSHOT jar in `target/`. Alternatively, use `./mvnw -Dmaven.test.skip=true install` to build a SNAPSHOT jar and then copy it into your local repository, so that you can use it in your Maven projects. Note that may need to do `./mvnw dependency:resolve` in your project if you overwrite an older snapshot with a newer one. + +`./mvnw -U` updates from remote repositories an may overwrite your local artifact. But you can always change the `artifactId` or the `groupId` of your local ClassGraph build to place your local build artifact in another location within your local repository. + +## Documentation + +[See the wiki for complete documentation and usage information.](https://github.com/classgraph/classgraph/wiki) + +**ClassGraph was known as FastClasspathScanner prior to version 4**. See the [porting notes](https://github.com/classgraph/classgraph/wiki/Porting-FastClasspathScanner-code-to-ClassGraph) for information on porting from the older FastClasspathScanner API. ## Mailing List * Feel free to subscribe to the [ClassGraph-Users](https://groups.google.com/d/forum/classgraph-users) email list for updates, or to ask questions. * There is also a [Gitter room](https://gitter.im/classgraph/Lobby) for discussion of ClassGraph. -## Author +## Sponsorship ClassGraph was written by Luke Hutchison ([@LH](http://twitter.com/LH) on Twitter). -Please donate if this library makes your life easier: +If ClassGraph is critical to your work, you can help fund further development through the [GitHub Sponsors Program](https://github.com/sponsors/lukehutch). -[![](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=luke.hutch@gmail.com&lc=US&item_name=Luke%20Hutchison&item_number=ClassGraph&no_note=0¤cy_code=USD&bn=PP-DonationsBF:btn_donateCC_LG.gif:NonHostedGuest) + -### Acknowledgments +## Acknowledgments ClassGraph would not be possible without contributions from numerous users, including in the form of bug reports, feature requests, code contributions, and assistance with testing. -### Alternatives +## Alternatives Some other classpath scanning mechanisms include: @@ -129,17 +195,21 @@ Some other classpath scanning mechanisms include: * [Javassist](http://jboss-javassist.github.io/javassist/) * [ObjectWeb ASM](http://asm.ow2.org/) * [QDox](https://github.com/paul-hammant/qdox), a fast Java source parser and indexer -* [bndtools](https://github.com/bndtools/bnd), which is able to ["crawl"/parse the bytecode of class files](https://github.com/bndtools/bnd/blob/master/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java) to find all imports/dependencies, among other things. +* [bndtools](https://github.com/bndtools/bnd), which is able to ["crawl"/parse the bytecode of class files](https://github.com/bndtools/bnd/blob/master/biz.aQute.bndlib/src/aQute/bnd/osgi/Clazz.java) to find all imports/dependencies, among other things. * [coffea](https://github.com/sbilinski/coffea), a command line tool and Python library for analyzing static dependences in Java bytecode +* [org.clapper.classutil.ClassFinder](https://github.com/bmc/classutil/blob/master/src/main/scala/org/clapper/classutil/ClassFinder.scala) +* [com.google.common.reflect.ClassPath](https://github.com/google/guava/blob/master/guava/src/com/google/common/reflect/ClassPath.java) +* [jdependency](https://github.com/tcurdt/jdependency) +* [Burningwave Core](https://github.com/burningwave/core#burningwave-core-) ## License **The MIT License (MIT)** -**Copyright (c) 2019 Luke Hutchison** - +**Copyright (c) 2022 Luke Hutchison** + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/mvnw b/mvnw new file mode 100755 index 000000000..e9cf8d330 --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.3 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 000000000..3fd2be860 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.3 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 5caad0585..f19d6afaf 100644 --- a/pom.xml +++ b/pom.xml @@ -1,513 +1,566 @@ - 4.0.0 + 4.0.0 - io.github.classgraph - classgraph - 4.8.12-SNAPSHOT - ClassGraph + io.github.classgraph + classgraph + 4.8.184 + ClassGraph - The uber-fast, ultra-lightweight classpath and module path scanner for JVM languages. + The uber-fast, ultra-lightweight classpath and module scanner for JVM languages. - https://github.com/classgraph/classgraph + https://github.com/classgraph/classgraph - - - The MIT License (MIT) - http://opensource.org/licenses/MIT - repo - - + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + - - - Luke Hutchison - luke.hutch@gmail.com - -- - https://github.com/lukehutch - - + + + Luke Hutchison + luke.hutch@gmail.com + ClassGraph + https://github.com/classgraph + + - - scm:git:git@github.com:classgraph/classgraph.git - scm:git:git@github.com:classgraph/classgraph.git - https://github.com/classgraph/classgraph - HEAD - + + scm:git:git@github.com:classgraph/classgraph.git + scm:git:git@github.com:classgraph/classgraph.git + https://github.com/classgraph/classgraph + classgraph-4.8.182 + - - https://github.com/classgraph/classgraph/issues - + + https://github.com/classgraph/classgraph/issues + - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + + UTF-8 + UTF-8 - - - UTF-8 - UTF-8 + + - - - + + + + + - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-enforcer-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.felix - maven-bundle-plugin - - - org.moditect - moditect-maven-plugin - - - org.codehaus.mojo - build-helper-maven-plugin - - - org.apache.maven.plugins - maven-jar-plugin - - - org.apache.maven.plugins - maven-source-plugin - - - org.apache.maven.plugins - maven-javadoc-plugin - - - org.sonatype.plugins - nexus-staging-maven-plugin - - - org.apache.maven.plugins - maven-release-plugin - - + + + + + io.github.toolfactory + narcissus + 1.0.11 + true + - - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M2 - - - org.codehaus.mojo - animal-sniffer-enforcer-rule - 1.17 - - - - - - check-signatures - test - - enforce - - - - - - org.codehaus.mojo.signature - - java17 - 1.0 - - - - - - - + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + org.openjdk.jmh + jmh-core + 1.37 + test + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + test + + + org.assertj + assertj-core + 3.25.3 + test + + + javax.enterprise + cdi-api + 2.0 + test + + + org.ops4j.pax.url + pax-url-aether + 2.6.14 + test + + + org.slf4j + slf4j-api + 2.0.13 + test + + + org.slf4j + slf4j-jdk14 + 2.0.13 + test + + + org.hibernate.javax.persistence + hibernate-jpa-2.1-api + 1.0.2.Final + test + + + com.google.jimfs + jimfs + 1.3.0 + test + + + jakarta.validation + jakarta.validation-api + 3.0.2 + test + - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.0 - - UTF-8 - - - - - - - 7 - 7 - - - - default-testCompile - test-compile - - testCompile - - - UTF-8 - - 8 - 8 - - - - + + + + + + + org.eclipse.jdt + org.eclipse.jdt.annotation + 2.3.0 + provided + + - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M3 - + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.codehaus.mojo + build-helper-maven-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.0 + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.3 + - - - org.apache.felix - maven-bundle-plugin - 4.1.0 - true - - - Utilities - ${project.groupId}.${project.artifactId} - ${project.description} - Luke Hutchison - - - - - bundle-manifest - process-classes - - manifest - - - - + + + org.apache.maven.plugins + maven-clean-plugin + 3.3.2 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.1 + - - - - - org.moditect - moditect-maven-plugin - 1.0.0.Beta2 - - - add-module-infos - package - - add-module-info - - - - - 9 - true - - - - - - - /** - ClassGraph, the uber-fast, ultra-lightweight, parallelized - Java classpath scanner, - module scanner, and annotation processor for JVM - languages. https://github.com/classgraph/classgraph */ - module - io.github.classgraph { - exports io.github.classgraph; - requires java.xml; - requires jdk.unsupported; - requires java.management; - requires java.logging; - } - - - - - - + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + true + + + - - - org.codehaus.mojo - build-helper-maven-plugin - 3.0.0 - - - add-test-source - generate-test-sources - - add-test-source - - - - src/test/perf - - - - - + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + org.codehaus.mojo + animal-sniffer-enforcer-rule + 1.23 + + + + + + enforce-versions + validate + + enforce + + + + + [3.6.3,) + + + + + + + check-signatures + compile + + enforce + + + + + + org.codehaus.mojo.signature + java17 + 1.0 + + + + + + + - - - org.apache.maven.plugins - maven-jar-plugin - 3.1.1 - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - true - true - - - - + + org.apache.maven.plugins + maven-resources-plugin + + + + copy-license-to-target + process-resources + + copy-resources + + + ${project.build.outputDirectory} + + + ${basedir} + false + + LICENSE-ClassGraph.txt + + + + + + + copy-license-to-javadocs + process-resources + + copy-resources + + + ${project.build.directory}/apidocs + + + ${basedir} + false + + LICENSE-ClassGraph.txt + + + + + + + - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - - jar-no-fork - - - - + + org.apache.maven.plugins + maven-antrun-plugin + + + + + + + + + + + add-module-info-to-jar + package + + run + + + + + + + + + + + + + + + add-modular-javadoc + verify + + run + + + + + + + + + + + - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.1 - - - attach-javadocs - - jar - - - none - ${javadoc.html.version} - nonapi.* - - - - + + + org.apache.maven.plugins + maven-compiler-plugin + + UTF-8 + + 8 + false + + -Xlint:all + -Xlint:-options + -Werror + + + + + default-testCompile + test-compile + + testCompile + + + UTF-8 + 8 + + -parameters + + + + + - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - true - - ossrh - https://oss.sonatype.org/ - true - - + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${surefireArgLine} + + - - - org.apache.maven.plugins - maven-release-plugin - 2.5.3 - - true - false - release - deploy - - - - - + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test/perf + + + + + - - - - - junit - junit - 4.13-beta-1 - test - - - org.openjdk.jmh - jmh-core - 1.21 - test - - - org.openjdk.jmh - jmh-generator-annprocess - 1.21 - test - - - org.assertj - assertj-core - 3.11.1 - test - - - javax.enterprise - cdi-api - 1.0-SP4 - test - - - org.ops4j.pax.url - pax-url-aether - 2.6.1 - test - - - org.slf4j - slf4j-api - 1.8.0-beta2 - test - - - org.slf4j - slf4j-jdk14 - 1.8.0-beta2 - test - - - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.2.Final - test - - + + + org.apache.maven.plugins + maven-jar-plugin + + true + + true + + true + true + + + + Utilities + http://opensource.org/licenses/MIT + 2 + ClassGraph + ${project.groupId}.${project.artifactId} + Luke Hutchison + ${project.description} + ${project.version} + osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.7))" + io.github.classgraph;version="${project.version}" + + + javax.xml.xpath,javax.xml.namespace,javax.xml.parsers,org.w3c.dom,sun.misc;resolution:="optional",sun.nio.ch;resolution:="optional",io.github.toolfactory.narcissus;resolution:="optional",io.github.toolfactory.jvm;resolution:="optional" + + java.xml,jdk.unsupported,java.management,java.logging + + + true + + + + - - - - jdk9plus - - -html5 - - - true - - - [9,) - - + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + package + + jar-no-fork + + + + - - - release - - - performRelease - true - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - - + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + package + + jar + + + 8 + ${javadoc.html.version} + all + nonapi.* + public + + + + + + + - - - - only-eclipse - - - - m2e.version - - - - - - - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - [1.0.0,) - - enforce - - - - - - - - - - - - - - - + + + + jdk9plus + + [9,) + + + -html5 + + + true + + + + jdk17plus + + [17,) + + + --enable-native-access=ALL-UNNAMED + + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + + + --pinentry-mode + loopback + + ${gpg.keyname} + ${gpg.keyname} + + + + sign-artifacts + verify + + sign + + + + + + + + diff --git a/src/main/java/io/github/classgraph/AnnotationClassRef.java b/src/main/java/io/github/classgraph/AnnotationClassRef.java index 50a6d0ccb..3ac4343ac 100644 --- a/src/main/java/io/github/classgraph/AnnotationClassRef.java +++ b/src/main/java/io/github/classgraph/AnnotationClassRef.java @@ -28,15 +28,12 @@ */ package io.github.classgraph; -import java.util.Set; - import nonapi.io.github.classgraph.types.ParseException; /** * Stores the type descriptor of a {@code Class}, as found in an annotation parameter value. */ public class AnnotationClassRef extends ScanResultObject { - /** The type descriptor str. */ private String typeDescriptorStr; @@ -78,14 +75,14 @@ public String getName() { /** * Get the type signature. * - * @return The type signature of the {@code Class} reference. This will be a {@link ClassRefTypeSignature} or - * a {@link BaseTypeSignature}. + * @return The type signature of the {@code Class} reference. This will be a {@link ClassRefTypeSignature}, a + * {@link BaseTypeSignature}, or an {@link ArrayTypeSignature}. */ private TypeSignature getTypeSignature() { if (typeSignature == null) { try { - // There can't be any type variables to resolve in either ClassRefTypeSignature or - // BaseTypeSignature, so just set definingClassName to null + // There can't be any type variables to resolve in ClassRefTypeSignature, + // BaseTypeSignature or ArrayTypeSignature, so just set definingClassName to null typeSignature = TypeSignature.parse(typeDescriptorStr, /* definingClassName = */ null); typeSignature.setScanResult(scanResult); } catch (final ParseException e) { @@ -110,7 +107,9 @@ public Class loadClass(final boolean ignoreExceptions) { if (typeSignature instanceof BaseTypeSignature) { return ((BaseTypeSignature) typeSignature).getType(); } else if (typeSignature instanceof ClassRefTypeSignature) { - return ((ClassRefTypeSignature) typeSignature).loadClass(ignoreExceptions); + return typeSignature.loadClass(ignoreExceptions); + } else if (typeSignature instanceof ArrayTypeSignature) { + return typeSignature.loadClass(ignoreExceptions); } else { throw new IllegalArgumentException("Got unexpected type " + typeSignature.getClass().getName() + " for ref type signature: " + typeDescriptorStr); @@ -139,9 +138,11 @@ protected String getClassName() { if (className == null) { getTypeSignature(); if (typeSignature instanceof BaseTypeSignature) { - className = ((BaseTypeSignature) typeSignature).getType().getName(); + className = ((BaseTypeSignature) typeSignature).getTypeStr(); } else if (typeSignature instanceof ClassRefTypeSignature) { className = ((ClassRefTypeSignature) typeSignature).getFullyQualifiedClassName(); + } else if (typeSignature instanceof ArrayTypeSignature) { + className = typeSignature.getClassName(); } else { throw new IllegalArgumentException("Got unexpected type " + typeSignature.getClass().getName() + " for ref type signature: " + typeDescriptorStr); @@ -160,8 +161,8 @@ protected String getClassName() { */ @Override public ClassInfo getClassInfo() { - getClassName(); - return super.getClassInfo(); + getTypeSignature(); + return typeSignature.getClassInfo(); } /* (non-Javadoc) @@ -175,14 +176,6 @@ void setScanResult(final ScanResult scanResult) { } } - /* (non-Javadoc) - * @see io.github.classgraph.ScanResultObject#findReferencedClassNames(java.util.Set) - */ - @Override - protected void findReferencedClassNames(final Set classNames) { - classNames.add(getClassName()); - } - // ------------------------------------------------------------------------------------------------------------- /* (non-Javadoc) @@ -198,25 +191,30 @@ public int hashCode() { */ @Override public boolean equals(final Object obj) { - if (!(obj instanceof AnnotationClassRef)) { + if (obj == this) { + return true; + } else if (!(obj instanceof AnnotationClassRef)) { return false; } return getTypeSignature().equals(((AnnotationClassRef) obj).getTypeSignature()); } - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ @Override - public String toString() { - String prefix = "class "; - if (scanResult != null) { - final ClassInfo ci = getClassInfo(); - // The JDK uses "interface" for both interfaces and annotations in Annotation::toString - if (ci != null && ci.isInterfaceOrAnnotation()) { - prefix = "interface "; - } - } - return prefix + getTypeSignature().toString(); + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + // More recent versions of Annotation::toString() have dropped the "class"/"interface" prefix, + // and added ".class" to the end of the class reference (which does not actually match the + // annotation source syntax...) + + // String prefix = "class "; + // if (scanResult != null) { + // final ClassInfo ci = getClassInfo(); + // // The JDK uses "interface" for both interfaces and annotations in Annotation::toString + // if (ci != null && ci.isInterfaceOrAnnotation()) { + // prefix = "interface "; + // } + // } + + /* prefix + */ + buf.append(getTypeSignature().toString(useSimpleNames)).append(".class"); } } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/AnnotationEnumValue.java b/src/main/java/io/github/classgraph/AnnotationEnumValue.java index 94e05e1a1..4b855a260 100644 --- a/src/main/java/io/github/classgraph/AnnotationEnumValue.java +++ b/src/main/java/io/github/classgraph/AnnotationEnumValue.java @@ -29,14 +29,12 @@ package io.github.classgraph; import java.lang.reflect.Field; -import java.util.Set; /** * Class for wrapping an enum constant value (split into class name and constant name), as used as an annotation * parameter value. */ public class AnnotationEnumValue extends ScanResultObject implements Comparable { - /** The class name. */ private String className; @@ -106,6 +104,13 @@ public String getName() { */ public Object loadClassAndReturnEnumValue(final boolean ignoreExceptions) throws IllegalArgumentException { final Class classRef = super.loadClass(ignoreExceptions); + if (classRef == null) { + if (ignoreExceptions) { + return null; + } else { + throw new IllegalArgumentException("Enum class " + className + " could not be loaded"); + } + } if (!classRef.isEnum()) { throw new IllegalArgumentException("Class " + className + " is not an enum"); } @@ -113,15 +118,15 @@ public Object loadClassAndReturnEnumValue(final boolean ignoreExceptions) throws try { field = classRef.getDeclaredField(valueName); } catch (final ReflectiveOperationException | SecurityException e) { - throw new IllegalArgumentException("Could not find enum constant " + toString(), e); + throw new IllegalArgumentException("Could not find enum constant " + this, e); } if (!field.isEnumConstant()) { - throw new IllegalArgumentException("Field " + toString() + " is not an enum constant"); + throw new IllegalArgumentException("Field " + this + " is not an enum constant"); } try { return field.get(null); } catch (final ReflectiveOperationException | SecurityException e) { - throw new IllegalArgumentException("Field " + toString() + " is not accessible", e); + throw new IllegalArgumentException("Field " + this + " is not accessible", e); } } @@ -139,14 +144,6 @@ public Object loadClassAndReturnEnumValue() throws IllegalArgumentException { // ------------------------------------------------------------------------------------------------------------- - /* (non-Javadoc) - * @see io.github.classgraph.ScanResultObject#findReferencedClassNames(java.util.Set) - */ - @Override - void findReferencedClassNames(final Set referencedClassNames) { - referencedClassNames.add(className); - } - /* (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ @@ -160,11 +157,13 @@ public int compareTo(final AnnotationEnumValue o) { * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(final Object o) { - if (!(o instanceof AnnotationEnumValue)) { + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof AnnotationEnumValue)) { return false; } - return compareTo((AnnotationEnumValue) o) == 0; + return compareTo((AnnotationEnumValue) obj) == 0; } /* (non-Javadoc) @@ -175,11 +174,10 @@ public int hashCode() { return className.hashCode() * 11 + valueName.hashCode(); } - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ @Override - public String toString() { - return className + "." + valueName; + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + buf.append(useSimpleNames ? ClassInfo.getSimpleName(className) : className); + buf.append('.'); + buf.append(valueName); } } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/AnnotationInfo.java b/src/main/java/io/github/classgraph/AnnotationInfo.java index 4c0c38042..356f1a41a 100644 --- a/src/main/java/io/github/classgraph/AnnotationInfo.java +++ b/src/main/java/io/github/classgraph/AnnotationInfo.java @@ -40,11 +40,11 @@ import java.util.Map.Entry; import java.util.Set; -import nonapi.io.github.classgraph.utils.ReflectionUtils; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.utils.LogNode; /** Holds metadata about a specific annotation instance on a class, method, method parameter or field. */ public class AnnotationInfo extends ScanResultObject implements Comparable, HasName { - /** The name. */ private String name; @@ -114,24 +114,28 @@ public AnnotationParameterValueList getDefaultParameterValues() { /** * Get the parameter values. * + * @param includeDefaultValues + * if true, include default values for any annotation parameter value that is missing. * @return The parameter values of this annotation, including any default parameter values inherited from the - * annotation class definition, or the empty list if none. + * annotation class definition (if requested), or the empty list if none. */ - public AnnotationParameterValueList getParameterValues() { + public AnnotationParameterValueList getParameterValues(final boolean includeDefaultValues) { + final ClassInfo classInfo = getClassInfo(); + if (classInfo == null) { + // ClassInfo has not yet been set, just return values without defaults + // (happens when trying to log AnnotationInfo during scanning, before ScanResult is available) + return annotationParamValues == null ? AnnotationParameterValueList.EMPTY_LIST : annotationParamValues; + } + // Lazily convert any Object[] arrays of boxed types to primitive arrays + if (annotationParamValues != null && !annotationParamValuesHasBeenConvertedToPrimitive) { + annotationParamValues.convertWrapperArraysToPrimitiveArrays(classInfo); + annotationParamValuesHasBeenConvertedToPrimitive = true; + } + if (!includeDefaultValues) { + // Don't include defaults + return annotationParamValues == null ? AnnotationParameterValueList.EMPTY_LIST : annotationParamValues; + } if (annotationParamValuesWithDefaults == null) { - final ClassInfo classInfo = getClassInfo(); - if (classInfo == null) { - // ClassInfo has not yet been set, just return values without defaults - // (happens when trying to log AnnotationInfo during scanning, before ScanResult is available) - return annotationParamValues == null ? AnnotationParameterValueList.EMPTY_LIST - : annotationParamValues; - } - - // Lazily convert any Object[] arrays of boxed types to primitive arrays - if (annotationParamValues != null && !annotationParamValuesHasBeenConvertedToPrimitive) { - annotationParamValues.convertWrapperArraysToPrimitiveArrays(classInfo); - annotationParamValuesHasBeenConvertedToPrimitive = true; - } if (classInfo.annotationDefaultParamValues != null && !classInfo.annotationDefaultParamValuesHasBeenConvertedToPrimitive) { classInfo.annotationDefaultParamValues.convertWrapperArraysToPrimitiveArrays(classInfo); @@ -193,6 +197,16 @@ public AnnotationParameterValueList getParameterValues() { return annotationParamValuesWithDefaults; } + /** + * Get the parameter values. + * + * @return The parameter values of this annotation, including any default parameter values inherited from the + * annotation class definition, or the empty list if none. + */ + public AnnotationParameterValueList getParameterValues() { + return getParameterValues(true); + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -205,17 +219,6 @@ protected String getClassName() { return name; } - /** - * Get the class info. - * - * @return The {@link ClassInfo} object for the annotation class. - */ - @Override - public ClassInfo getClassInfo() { - getClassName(); - return super.getClassInfo(); - } - /* (non-Javadoc) * @see io.github.classgraph.ScanResultObject#setScanResult(io.github.classgraph.ScanResult) */ @@ -230,23 +233,32 @@ void setScanResult(final ScanResult scanResult) { } /** - * Find the names of any classes referenced in the type descriptors of annotation parameters. + * Get {@link ClassInfo} objects for any classes referenced in the type descriptor or type signature. * - * @param referencedClassNames - * the referenced class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info */ @Override - void findReferencedClassNames(final Set referencedClassNames) { - referencedClassNames.add(name); + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + super.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); if (annotationParamValues != null) { for (final AnnotationParameterValue annotationParamValue : annotationParamValues) { - annotationParamValue.findReferencedClassNames(referencedClassNames); + annotationParamValue.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } } } // ------------------------------------------------------------------------------------------------------------- + /** Return the {@link ClassInfo} object for the annotation class. */ + @Override + public ClassInfo getClassInfo() { + return super.getClassInfo(); + } + /** * Load the {@link Annotation} class corresponding to this {@link AnnotationInfo} object, by calling * {@code getClassInfo().loadClass()}, then create a new instance of the annotation, with the annotation @@ -276,7 +288,7 @@ void findReferencedClassNames(final Set referencedClassNames) { public Annotation loadClassAndInstantiate() { final Class annotationClass = getClassInfo().loadClass(Annotation.class); return (Annotation) Proxy.newProxyInstance(annotationClass.getClassLoader(), - new Class[] { annotationClass }, new AnnotationInvocationHandler(annotationClass, this)); + new Class[] { annotationClass }, new AnnotationInvocationHandler(annotationClass, this)); } /** {@link InvocationHandler} for dynamically instantiating an {@link Annotation} object. */ @@ -311,7 +323,7 @@ private static class AnnotationInvocationHandler implements InvocationHandler { if (instantiatedValue == null) { // Annotations cannot contain null values throw new IllegalArgumentException("Got null value for annotation parameter " + apv.getName() - + " of annotation " + annotationInfo.getName()); + + " of annotation " + annotationInfo.name); } this.annotationParameterValuesInstantiated.put(apv.getName(), instantiatedValue); } @@ -331,7 +343,7 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg + (args == null ? 0 : args.length) + ", expected " + paramTypes.length); } if (args != null && paramTypes.length == 1) { - if (methodName.equals("equals") && paramTypes[0] == Object.class) { + if ("equals".equals(methodName) && paramTypes[0] == Object.class) { // equals() needs to function the same as the JDK implementation // (see src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java in the JDK) if (this == args[0]) { @@ -339,11 +351,14 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg } else if (!annotationClass.isInstance(args[0])) { return false; } + final ReflectionUtils reflectionUtils = annotationInfo.scanResult == null + ? new ReflectionUtils() + : annotationInfo.scanResult.reflectionUtils; for (final Entry ent : annotationParameterValuesInstantiated.entrySet()) { final String paramName = ent.getKey(); final Object paramVal = ent.getValue(); - final Object otherParamVal = ReflectionUtils.invokeMethod(args[0], paramName, - /* throwException = */ false); + final Object otherParamVal = reflectionUtils.invokeMethod(/* throwException = */ false, + args[0], paramName); if ((paramVal == null) != (otherParamVal == null)) { // Annotation values should never be null, but just to be safe return false; @@ -469,7 +484,7 @@ void convertWrapperArraysToPrimitiveArrays() { */ @Override public int compareTo(final AnnotationInfo o) { - final int diff = getName().compareTo(o.getName()); + final int diff = this.name.compareTo(o.name); if (diff != 0) { return diff; } @@ -480,8 +495,8 @@ public int compareTo(final AnnotationInfo o) { } else if (o.annotationParamValues == null) { return 1; } else { - for (int i = 0, max = Math.max(annotationParamValues.size(), - o.annotationParamValues.size()); i < max; i++) { + for (int i = 0, + max = Math.max(annotationParamValues.size(), o.annotationParamValues.size()); i < max; i++) { if (i >= annotationParamValues.size()) { return -1; } else if (i >= o.annotationParamValues.size()) { @@ -502,11 +517,13 @@ public int compareTo(final AnnotationInfo o) { */ @Override public boolean equals(final Object obj) { - if (!(obj instanceof AnnotationInfo)) { + if (obj == this) { + return true; + } else if (!(obj instanceof AnnotationInfo)) { return false; } - final AnnotationInfo o = (AnnotationInfo) obj; - return this.compareTo(o) == 0; + final AnnotationInfo other = (AnnotationInfo) obj; + return this.compareTo(other) == 0; } /* (non-Javadoc) @@ -514,7 +531,7 @@ public boolean equals(final Object obj) { */ @Override public int hashCode() { - int h = getName().hashCode(); + int h = name.hashCode(); if (annotationParamValues != null) { for (final AnnotationParameterValue e : annotationParamValues) { h = h * 7 + e.getName().hashCode() * 3 + e.getValue().hashCode(); @@ -523,14 +540,9 @@ public int hashCode() { return h; } - /** - * Render as a string, into a StringBuilder buffer. - * - * @param buf - * The buffer. - */ - void toString(final StringBuilder buf) { - buf.append('@').append(getName()); + @Override + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + buf.append('@').append(useSimpleNames ? ClassInfo.getSimpleName(name) : name); final AnnotationParameterValueList paramVals = getParameterValues(); if (!paramVals.isEmpty()) { buf.append('('); @@ -540,22 +552,12 @@ void toString(final StringBuilder buf) { } final AnnotationParameterValue paramVal = paramVals.get(i); if (paramVals.size() > 1 || !"value".equals(paramVal.getName())) { - paramVal.toString(buf); + paramVal.toString(useSimpleNames, buf); } else { - paramVal.toStringParamValueOnly(buf); + paramVal.toStringParamValueOnly(useSimpleNames, buf); } } buf.append(')'); } } - - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - final StringBuilder buf = new StringBuilder(); - toString(buf); - return buf.toString(); - } } diff --git a/src/main/java/io/github/classgraph/AnnotationInfoList.java b/src/main/java/io/github/classgraph/AnnotationInfoList.java index bef4cb9df..79a0151d7 100644 --- a/src/main/java/io/github/classgraph/AnnotationInfoList.java +++ b/src/main/java/io/github/classgraph/AnnotationInfoList.java @@ -28,15 +28,18 @@ */ package io.github.classgraph; +import java.lang.annotation.Annotation; import java.lang.annotation.Repeatable; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import io.github.classgraph.ClassInfo.RelType; +import nonapi.io.github.classgraph.utils.Assert; +import nonapi.io.github.classgraph.utils.CollectionUtils; +import nonapi.io.github.classgraph.utils.LogNode; /** A list of {@link AnnotationInfo} objects. */ public class AnnotationInfoList extends MappableInfoList { @@ -47,30 +50,49 @@ public class AnnotationInfoList extends MappableInfoList { */ private AnnotationInfoList directlyRelatedAnnotations; + /** serialVersionUID */ + private static final long serialVersionUID = 1L; + + /** An unmodifiable empty {@link AnnotationInfoList}. */ + static final AnnotationInfoList EMPTY_LIST = new AnnotationInfoList(); + static { + EMPTY_LIST.makeUnmodifiable(); + } + /** - * Constructor. + * Return an unmodifiable empty {@link AnnotationInfoList}. + * + * @return the unmodifiable empty {@link AnnotationInfoList}. + */ + public static AnnotationInfoList emptyList() { + return EMPTY_LIST; + } + + /** + * Construct a new modifiable empty list of {@link AnnotationInfo} objects. */ - AnnotationInfoList() { + public AnnotationInfoList() { super(); } /** - * Constructor. + * Construct a new modifiable empty list of {@link AnnotationInfo} objects, given a size hint. * * @param sizeHint * the size hint */ - AnnotationInfoList(final int sizeHint) { + public AnnotationInfoList(final int sizeHint) { super(sizeHint); } /** - * Constructor. + * Construct a new modifiable empty {@link AnnotationInfoList}, given an initial list of {@link AnnotationInfo} + * objects. * * @param reachableAnnotations * the reachable annotations */ - AnnotationInfoList(final AnnotationInfoList reachableAnnotations) { + public AnnotationInfoList(final AnnotationInfoList reachableAnnotations) { // If only reachable annotations are given, treat all of them as direct this(reachableAnnotations, reachableAnnotations); } @@ -89,59 +111,6 @@ public class AnnotationInfoList extends MappableInfoList { this.directlyRelatedAnnotations = directlyRelatedAnnotations; } - /** An unmodifiable empty {@link AnnotationInfoList}. */ - static final AnnotationInfoList EMPTY_LIST = new AnnotationInfoList() { - @Override - public boolean add(final AnnotationInfo e) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void add(final int index, final AnnotationInfo element) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean remove(final Object o) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public AnnotationInfo remove(final int index) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final int index, final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean removeAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean retainAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void clear() { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public AnnotationInfo set(final int index, final AnnotationInfo element) { - throw new IllegalArgumentException("List is immutable"); - } - }; - // ------------------------------------------------------------------------------------------------------------- /** @@ -183,14 +152,19 @@ public AnnotationInfoList filter(final AnnotationInfoFilter filter) { // ------------------------------------------------------------------------------------------------------------- /** - * Find the names of any classes referenced in the annotations in this list or their parameters. + * Get {@link ClassInfo} objects for any classes referenced in this list. * - * @param referencedClassNames - * the referenced class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info + * @param log + * the log */ - void findReferencedClassNames(final Set referencedClassNames) { + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { for (final AnnotationInfo ai : this) { - ai.findReferencedClassNames(referencedClassNames); + ai.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } } @@ -205,11 +179,14 @@ void findReferencedClassNames(final Set referencedClassNames) { * the containing class * @param forwardRelType * the forward relationship type for linking (or null for none) - * @param reverseRelType - * the reverse relationship type for linking (or null for none) + * @param reverseRelType0 + * the first reverse relationship type for linking (or null for none) + * @param reverseRelType1 + * the second reverse relationship type for linking (or null for none) */ void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames, - final ClassInfo containingClassInfo, final RelType forwardRelType, final RelType reverseRelType) { + final ClassInfo containingClassInfo, final RelType forwardRelType, final RelType reverseRelType0, + final RelType reverseRelType1) { List repeatableAnnotations = null; for (int i = size() - 1; i >= 0; --i) { final AnnotationInfo ai = get(i); @@ -237,11 +214,19 @@ void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames, add(ai); // Link annotation, if necessary - if (forwardRelType != null && reverseRelType != null) { + if (forwardRelType != null + && (reverseRelType0 != null || reverseRelType1 != null)) { final ClassInfo annotationClass = ai.getClassInfo(); if (annotationClass != null) { containingClassInfo.addRelatedClass(forwardRelType, annotationClass); - annotationClass.addRelatedClass(reverseRelType, containingClassInfo); + if (reverseRelType0 != null) { + annotationClass.addRelatedClass(reverseRelType0, + containingClassInfo); + } + if (reverseRelType1 != null) { + annotationClass.addRelatedClass(reverseRelType1, + containingClassInfo); + } } } } @@ -333,10 +318,10 @@ static AnnotationInfoList getIndirectAnnotations(final AnnotationInfoList direct final AnnotationInfoList directAnnotationInfoSorted = directAnnotationInfo == null ? AnnotationInfoList.EMPTY_LIST : new AnnotationInfoList(directAnnotationInfo); - Collections.sort(directAnnotationInfoSorted); + CollectionUtils.sortIfNotEmpty(directAnnotationInfoSorted); final AnnotationInfoList annotationInfoList = new AnnotationInfoList(reachableAnnotationInfo, directAnnotationInfoSorted); - Collections.sort(annotationInfoList); + CollectionUtils.sortIfNotEmpty(annotationInfoList); return annotationInfoList; } @@ -361,6 +346,18 @@ public AnnotationInfoList directOnly() { // ------------------------------------------------------------------------------------------------------------- + /** + * Get the {@link Repeatable} annotation with the given class, or the empty list if none found. + * + * @param annotationClass + * The class to search for. + * @return The list of annotations with the given class, or the empty list if none found. + */ + public AnnotationInfoList getRepeatable(final Class annotationClass) { + Assert.isAnnotation(annotationClass); + return getRepeatable(annotationClass.getName()); + } + /** * Get the {@link Repeatable} annotation with the given name, or the empty list if none found. * @@ -394,14 +391,13 @@ public AnnotationInfoList getRepeatable(final String name) { * @see java.util.ArrayList#equals(java.lang.Object) */ @Override - public boolean equals(final Object o) { - if (this == o) { + public boolean equals(final Object obj) { + if (this == obj) { return true; - } - if (!(o instanceof AnnotationInfoList)) { + } else if (!(obj instanceof AnnotationInfoList)) { return false; } - final AnnotationInfoList other = (AnnotationInfoList) o; + final AnnotationInfoList other = (AnnotationInfoList) obj; if ((directlyRelatedAnnotations == null) != (other.directlyRelatedAnnotations == null)) { return false; } diff --git a/src/main/java/io/github/classgraph/AnnotationParameterValue.java b/src/main/java/io/github/classgraph/AnnotationParameterValue.java index 76efb2324..caa3db213 100644 --- a/src/main/java/io/github/classgraph/AnnotationParameterValue.java +++ b/src/main/java/io/github/classgraph/AnnotationParameterValue.java @@ -29,13 +29,15 @@ package io.github.classgraph; import java.lang.reflect.Array; +import java.util.Map; import java.util.Objects; import java.util.Set; +import nonapi.io.github.classgraph.utils.LogNode; + /** A wrapper used to pair annotation parameter names with annotation parameter values. */ public class AnnotationParameterValue extends ScanResultObject implements HasName, Comparable { - /** The the parameter name. */ private String name; @@ -136,15 +138,18 @@ void setScanResult(final ScanResult scanResult) { } /** - * Get the names of any classes referenced in the annotation parameters. + * Get {@link ClassInfo} objects for any classes referenced in the annotation parameters. * - * @param referencedClassNames - * the referenced class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info */ @Override - void findReferencedClassNames(final Set referencedClassNames) { + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { if (value != null) { - value.findReferencedClassNames(referencedClassNames); + value.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } } @@ -177,25 +182,78 @@ Object instantiate(final ClassInfo annotationClassInfo) { // ------------------------------------------------------------------------------------------------------------- /* (non-Javadoc) - * @see java.lang.Object#toString() + * @see java.lang.Comparable#compareTo(java.lang.Object) */ @Override - public String toString() { - final StringBuilder buf = new StringBuilder(); - toString(buf); - return buf.toString(); + public int compareTo(final AnnotationParameterValue other) { + if (other == this) { + return 0; + } + final int diff = name.compareTo(other.getName()); + if (diff != 0) { + return diff; + } + if (value.equals(other.value)) { + return 0; + } + // Use toString() order (which can be slow) as a last-ditch effort -- only happens + // if the annotation has multiple parameters of the same name but different value. + final Object p0 = getValue(); + final Object p1 = other.getValue(); + return p0 == null || p1 == null ? (p0 == null ? 0 : 1) - (p1 == null ? 0 : 1) + : toStringParamValueOnly().compareTo(other.toStringParamValueOnly()); + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof AnnotationParameterValue)) { + return false; + } + final AnnotationParameterValue other = (AnnotationParameterValue) obj; + return this.name.equals(other.name) && (value == null) == (other.value == null) + && (value == null || value.equals(other.value)); + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(name, value); + } + + // ------------------------------------------------------------------------------------------------------------- + + @Override + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + buf.append(name); + buf.append("="); + toStringParamValueOnly(useSimpleNames, buf); } /** - * To string. + * Write an annotation parameter value's string representation to the buffer. * + * @param val + * the value + * @param useSimpleNames + * the use simple names * @param buf - * the buf + * the buffer */ - void toString(final StringBuilder buf) { - buf.append(name); - buf.append("="); - toStringParamValueOnly(buf); + private static void toString(final Object val, final boolean useSimpleNames, final StringBuilder buf) { + if (val == null) { + buf.append("null"); + } else if (val instanceof ScanResultObject) { + ((ScanResultObject) val).toString(useSimpleNames, buf); + } else { + buf.append(val); + } } /** @@ -204,22 +262,22 @@ void toString(final StringBuilder buf) { * @param buf * the buf */ - void toStringParamValueOnly(final StringBuilder buf) { + void toStringParamValueOnly(final boolean useSimpleNames, final StringBuilder buf) { if (value == null) { buf.append("null"); } else { final Object paramVal = value.get(); final Class valClass = paramVal.getClass(); if (valClass.isArray()) { - buf.append('['); + buf.append('{'); for (int j = 0, n = Array.getLength(paramVal); j < n; j++) { if (j > 0) { buf.append(", "); } final Object elt = Array.get(paramVal, j); - buf.append(elt == null ? "null" : elt.toString()); + toString(elt, useSimpleNames, buf); } - buf.append(']'); + buf.append('}'); } else if (paramVal instanceof String) { buf.append('"'); buf.append(paramVal.toString().replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r")); @@ -229,46 +287,19 @@ void toStringParamValueOnly(final StringBuilder buf) { buf.append(paramVal.toString().replace("'", "\\'").replace("\n", "\\n").replace("\r", "\\r")); buf.append('\''); } else { - buf.append(paramVal.toString()); + toString(paramVal, useSimpleNames, buf); } } } - /* (non-Javadoc) - * @see java.lang.Comparable#compareTo(java.lang.Object) - */ - @Override - public int compareTo(final AnnotationParameterValue o) { - final int diff = name.compareTo(o.getName()); - if (diff != 0) { - return diff; - } - // Use toString() order and get() (which can be slow) as a last-ditch effort -- only happens - // if the annotation has multiple parameters of the same name but different value. - final Object p0 = getValue(); - final Object p1 = o.getValue(); - return p0 == null || p1 == null ? (p0 == null ? 0 : 1) - (p1 == null ? 0 : 1) - : p0.toString().compareTo(p1.toString()); - } - - /* (non-Javadoc) - * @see java.lang.Object#equals(java.lang.Object) - */ - @Override - public boolean equals(final Object obj) { - if (!(obj instanceof AnnotationParameterValue)) { - return false; - } - final AnnotationParameterValue o = (AnnotationParameterValue) obj; - return this.compareTo(o) == 0 && (value == null) == (o.value == null) - && (value == null || value.equals(o.value)); - } - - /* (non-Javadoc) - * @see java.lang.Object#hashCode() + /** + * To string, param value only. + * + * @return the string. */ - @Override - public int hashCode() { - return Objects.hash(name, value); + private String toStringParamValueOnly() { + final StringBuilder buf = new StringBuilder(); + toStringParamValueOnly(false, buf); + return buf.toString(); } } diff --git a/src/main/java/io/github/classgraph/AnnotationParameterValueList.java b/src/main/java/io/github/classgraph/AnnotationParameterValueList.java index 3fd9d3d5d..f6dd71ef1 100644 --- a/src/main/java/io/github/classgraph/AnnotationParameterValueList.java +++ b/src/main/java/io/github/classgraph/AnnotationParameterValueList.java @@ -29,102 +29,76 @@ package io.github.classgraph; import java.util.Collection; +import java.util.Map; import java.util.Set; +import nonapi.io.github.classgraph.utils.LogNode; + /** A list of {@link AnnotationParameterValue} objects. */ public class AnnotationParameterValueList extends MappableInfoList { + /** serialVersionUID */ + private static final long serialVersionUID = 1L; + + /** An unmodifiable empty {@link AnnotationParameterValueList}. */ + static final AnnotationParameterValueList EMPTY_LIST = new AnnotationParameterValueList(); + static { + EMPTY_LIST.makeUnmodifiable(); + } /** - * Constructor. + * Return an unmodifiable empty {@link AnnotationParameterValueList}. + * + * @return the unmodifiable empty {@link AnnotationParameterValueList}. */ - AnnotationParameterValueList() { + public static AnnotationParameterValueList emptyList() { + return EMPTY_LIST; + } + + /** + * Construct a new modifiable empty list of {@link AnnotationParameterValue} objects. + */ + public AnnotationParameterValueList() { super(); } /** - * Constructor. + * Construct a new modifiable empty list of {@link AnnotationParameterValue} objects, given a size hint. * * @param sizeHint * the size hint */ - AnnotationParameterValueList(final int sizeHint) { + public AnnotationParameterValueList(final int sizeHint) { super(sizeHint); } /** - * Constructor. + * Construct a new modifiable empty {@link AnnotationParameterValueList}, given an initial list of + * {@link AnnotationParameterValue} objects. * * @param annotationParameterValueCollection - * the annotation parameter value collection + * the collection of {@link AnnotationParameterValue} objects. */ - AnnotationParameterValueList(final Collection annotationParameterValueCollection) { + public AnnotationParameterValueList( + final Collection annotationParameterValueCollection) { super(annotationParameterValueCollection); } - /** An unmodifiable empty {@link AnnotationParameterValueList}. */ - static final AnnotationParameterValueList EMPTY_LIST = new AnnotationParameterValueList() { - @Override - public boolean add(final AnnotationParameterValue e) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void add(final int index, final AnnotationParameterValue element) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean remove(final Object o) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public AnnotationParameterValue remove(final int index) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final int index, final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean removeAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean retainAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void clear() { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public AnnotationParameterValue set(final int index, final AnnotationParameterValue element) { - throw new IllegalArgumentException("List is immutable"); - } - }; - // ------------------------------------------------------------------------------------------------------------- /** - * Find the names of any classes referenced in the methods in this list. + * Get {@link ClassInfo} objects for any classes referenced in the methods in this list. * - * @param referencedClassNames - * the referenced class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info + * @param log + * the log */ - void findReferencedClassNames(final Set referencedClassNames) { + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { for (final AnnotationParameterValue apv : this) { - apv.findReferencedClassNames(referencedClassNames); + apv.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } } diff --git a/src/main/java/io/github/classgraph/ArrayClassInfo.java b/src/main/java/io/github/classgraph/ArrayClassInfo.java new file mode 100644 index 000000000..56b512b94 --- /dev/null +++ b/src/main/java/io/github/classgraph/ArrayClassInfo.java @@ -0,0 +1,251 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph; + +import java.util.Map; +import java.util.Set; + +import nonapi.io.github.classgraph.utils.LogNode; + +/** + * Holds metadata about an array class. This class extends {@link ClassInfo} with additional methods relevant to + * array classes, in particular {@link #getArrayTypeSignature()}, {@link #getTypeSignatureStr()}, + * {@link #getElementTypeSignature()}, {@link #getElementClassInfo()}, {@link #loadElementClass()}, and + * {@link #getNumDimensions()}. + * + *

+ * An {@link ArrayClassInfo} object will not have any methods, fields or annotations. + * {@link ClassInfo#isArrayClass()} will return true for this subclass of {@link ClassInfo}. + */ +public class ArrayClassInfo extends ClassInfo { + /** The array type signature. */ + private ArrayTypeSignature arrayTypeSignature; + + /** The element class info. */ + private ClassInfo elementClassInfo; + + /** Default constructor for deserialization. */ + ArrayClassInfo() { + super(); + } + + /** + * Constructor. + * + * @param arrayTypeSignature + * the array type signature + */ + ArrayClassInfo(final ArrayTypeSignature arrayTypeSignature) { + super(arrayTypeSignature.getClassName(), /* modifiers = */ 0, /* resource = */ null); + this.arrayTypeSignature = arrayTypeSignature; + // Pre-load fields from element type + getElementClassInfo(); + } + + /* (non-Javadoc) + * @see io.github.classgraph.ClassInfo#setScanResult(io.github.classgraph.ScanResult) + */ + @Override + void setScanResult(final ScanResult scanResult) { + super.setScanResult(scanResult); + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get the raw type signature string of the array class, e.g. "[[I" for "int[][]". + * + * @return The raw type signature string of the array class. + */ + @Override + public String getTypeSignatureStr() { + return arrayTypeSignature.getTypeSignatureStr(); + } + + /** + * Returns null, because array classes do not have a ClassTypeSignature. Call {@link #getArrayTypeSignature()} + * instead. + * + * @return null (always). + */ + @Override + public ClassTypeSignature getTypeSignature() { + return null; + } + + /** + * Get the type signature of the class. + * + * @return The class type signature, if available, otherwise returns null. + */ + public ArrayTypeSignature getArrayTypeSignature() { + return arrayTypeSignature; + } + + /** + * Get the type signature of the array elements. + * + * @return The type signature of the array elements. + */ + public TypeSignature getElementTypeSignature() { + return arrayTypeSignature.getElementTypeSignature(); + } + + /** + * Get the number of dimensions of the array. + * + * @return The number of dimensions of the array. + */ + public int getNumDimensions() { + return arrayTypeSignature.getNumDimensions(); + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get the {@link ClassInfo} instance for the array element type. + * + * @return the {@link ClassInfo} instance for the array element type. Returns null if the element type was not + * found during the scan. In particular, will return null for arrays that have a primitive element type. + */ + public ClassInfo getElementClassInfo() { + if (elementClassInfo == null) { + final TypeSignature elementTypeSignature = arrayTypeSignature.getElementTypeSignature(); + if (!(elementTypeSignature instanceof BaseTypeSignature)) { + elementClassInfo = arrayTypeSignature.getElementTypeSignature().getClassInfo(); + if (elementClassInfo != null) { + // Copy over relevant fields from array element ClassInfo + this.classpathElement = elementClassInfo.classpathElement; + this.classfileResource = elementClassInfo.classfileResource; + this.classLoader = elementClassInfo.classLoader; + this.isScannedClass = elementClassInfo.isScannedClass; + this.isExternalClass = elementClassInfo.isExternalClass; + this.moduleInfo = elementClassInfo.moduleInfo; + this.packageInfo = elementClassInfo.packageInfo; + } + } + } + return elementClassInfo; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get a {@code Class} reference for the array element type. Causes the ClassLoader to load the element + * class, if it is not already loaded. + * + * @param ignoreExceptions + * Whether or not to ignore exceptions. + * @return a {@code Class} reference for the array element type. Also works for arrays of primitive element + * type. + */ + public Class loadElementClass(final boolean ignoreExceptions) { + return arrayTypeSignature.loadElementClass(ignoreExceptions); + } + + /** + * Get a {@code Class} reference for the array element type. Causes the ClassLoader to load the element + * class, if it is not already loaded. + * + * @return a {@code Class} reference for the array element type. Also works for arrays of primitive element + * type. + */ + public Class loadElementClass() { + return arrayTypeSignature.loadElementClass(); + } + + /** + * Obtain a {@code Class} reference for the array class named by this {@link ArrayClassInfo} object. Causes + * the ClassLoader to load the element class, if it is not already loaded. + * + * @param ignoreExceptions + * Whether or not to ignore exceptions + * @return The class reference, or null, if ignoreExceptions is true and there was an exception or error loading + * the class. + * @throws IllegalArgumentException + * if ignoreExceptions is false and there were problems loading the class. + */ + @Override + public Class loadClass(final boolean ignoreExceptions) { + if (classRef == null) { + classRef = arrayTypeSignature.loadClass(ignoreExceptions); + } + return classRef; + } + + /** + * Obtain a {@code Class} reference for the array class named by this {@link ArrayClassInfo} object. Causes + * the ClassLoader to load the element class, if it is not already loaded. + * + * @return The class reference. + * @throws IllegalArgumentException + * if there were problems loading the class. + */ + @Override + public Class loadClass() { + if (classRef == null) { + classRef = arrayTypeSignature.loadClass(); + } + return classRef; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get {@link ClassInfo} objects for any classes referenced in the type descriptor or type signature. + * + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info + */ + @Override + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + super.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + } + + // ------------------------------------------------------------------------------------------------------------- + + /* (non-Javadoc) + * @see io.github.classgraph.ClassInfo#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + return super.equals(obj); + } + + /* (non-Javadoc) + * @see io.github.classgraph.ClassInfo#hashCode() + */ + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/src/main/java/io/github/classgraph/ArrayTypeSignature.java b/src/main/java/io/github/classgraph/ArrayTypeSignature.java index aa7f6fa32..6565c9e14 100644 --- a/src/main/java/io/github/classgraph/ArrayTypeSignature.java +++ b/src/main/java/io/github/classgraph/ArrayTypeSignature.java @@ -28,18 +28,31 @@ */ package io.github.classgraph; +import java.lang.reflect.Array; +import java.util.List; +import java.util.Objects; import java.util.Set; +import io.github.classgraph.Classfile.TypePathNode; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.Parser; /** An array type signature. */ public class ArrayTypeSignature extends ReferenceTypeSignature { - /** The array element type signature. */ - private final TypeSignature elementTypeSignature; + /** The raw type signature string for the array type. */ + private final String typeSignatureStr; - /** The number of array dimensions. */ - private final int numDims; + /** Human-readable class name, e.g. "java.lang.String[]". */ + private String className; + + /** Array class info. */ + private ArrayClassInfo arrayClassInfo; + + /** The element class. */ + private Class elementClassRef; + + /** The nested type (another {@link ArrayTypeSignature}, or the base element type). */ + private final TypeSignature nestedType; // ------------------------------------------------------------------------------------------------------------- @@ -50,31 +63,98 @@ public class ArrayTypeSignature extends ReferenceTypeSignature { * The type signature of the array elements. * @param numDims * The number of array dimensions. + * @param typeSignatureStr + * Raw array type signature string (e.g. "[[I") */ - ArrayTypeSignature(final TypeSignature elementTypeSignature, final int numDims) { + ArrayTypeSignature(final TypeSignature elementTypeSignature, final int numDims, final String typeSignatureStr) { super(); - this.elementTypeSignature = elementTypeSignature; - this.numDims = numDims; + final boolean typeSigHasTwoOrMoreDims = typeSignatureStr.startsWith("[["); + if (numDims < 1) { + throw new IllegalArgumentException("numDims < 1"); + } else if ((numDims >= 2) != typeSigHasTwoOrMoreDims) { + throw new IllegalArgumentException("numDims does not match type signature"); + } + this.typeSignatureStr = typeSignatureStr; + this.nestedType = typeSigHasTwoOrMoreDims + // Strip one array dimension for nested type + ? new ArrayTypeSignature(elementTypeSignature, numDims - 1, typeSignatureStr.substring(1)) + // Nested type for innermost dimension is element type + : elementTypeSignature; + } + + /** + * Get the raw array type signature string, e.g. "[[I". + * + * @return the raw array type signature string. + */ + public String getTypeSignatureStr() { + return typeSignatureStr; } /** - * Get the element type signature. + * Get the type signature of the innermost element type of the array. * - * @return The type signature of the array elements. + * @return The type signature of the innermost element type. */ public TypeSignature getElementTypeSignature() { - return elementTypeSignature; + ArrayTypeSignature curr = this; + while (curr.nestedType instanceof ArrayTypeSignature) { + curr = (ArrayTypeSignature) curr.nestedType; + } + return curr.getNestedType(); } /** - * Get the number of dimensions. + * Get the number of dimensions of the array. * * @return The number of dimensions of the array. */ public int getNumDimensions() { + int numDims = 1; + ArrayTypeSignature curr = this; + while (curr.nestedType instanceof ArrayTypeSignature) { + curr = (ArrayTypeSignature) curr.nestedType; + numDims++; + } return numDims; } + /** + * Get the nested type, which is another {@link ArrayTypeSignature} with one dimension fewer, if this array has + * 2 or more dimensions, otherwise this returns the element type. + * + * @return The nested type. + */ + public TypeSignature getNestedType() { + return nestedType; + } + + @Override + protected void addTypeAnnotation(final List typePath, final AnnotationInfo annotationInfo) { + if (typePath.isEmpty()) { + addTypeAnnotation(annotationInfo); + } else { + final TypePathNode head = typePath.get(0); + if (head.typePathKind != 0 || head.typeArgumentIdx != 0) { + throw new IllegalArgumentException("typePath element contains bad values: " + head); + } + nestedType.addTypeAnnotation(typePath.subList(1, typePath.size()), annotationInfo); + } + } + + /** + * Get a list of {@link AnnotationInfo} objects for the type annotations on this array type, or null if none. + * + * @see #getNestedType() if you want to read for type annotations on inner (nested) dimensions of the array + * type. + * @return a list of {@link AnnotationInfo} objects for the type annotations of on this array type, or null if + * none. + */ + @Override + public AnnotationInfoList getTypeAnnotationInfo() { + return typeAnnotationInfo; + } + // ------------------------------------------------------------------------------------------------------------- /* (non-Javadoc) @@ -82,8 +162,10 @@ public int getNumDimensions() { */ @Override protected String getClassName() { - // getClassInfo() is not valid for this type, so getClassName() does not need to be implemented - throw new IllegalArgumentException("getClassName() cannot be called here"); + if (className == null) { + className = toString(); + } + return className; } /* (non-Javadoc) @@ -91,7 +173,30 @@ protected String getClassName() { */ @Override protected ClassInfo getClassInfo() { - throw new IllegalArgumentException("getClassInfo() cannot be called here"); + return getArrayClassInfo(); + } + + /** + * Return an {@link ArrayClassInfo} instance for the array class, cast to its superclass. + * + * @return the {@link ArrayClassInfo} instance. + */ + public ArrayClassInfo getArrayClassInfo() { + if (arrayClassInfo == null) { + if (scanResult != null) { + final String clsName = getClassName(); + // Cache ArrayClassInfo instances using scanResult.classNameToClassInfo, if scanResult is available + arrayClassInfo = (ArrayClassInfo) scanResult.classNameToClassInfo.get(clsName); + if (arrayClassInfo == null) { + scanResult.classNameToClassInfo.put(clsName, arrayClassInfo = new ArrayClassInfo(this)); + arrayClassInfo.setScanResult(this.scanResult); + } + } else { + // scanResult is not yet available, create an uncached instance of an ArrayClassInfo for this type + arrayClassInfo = new ArrayClassInfo(this); + } + } + return arrayClassInfo; } /* (non-Javadoc) @@ -100,17 +205,119 @@ protected ClassInfo getClassInfo() { @Override void setScanResult(final ScanResult scanResult) { super.setScanResult(scanResult); - if (elementTypeSignature != null) { - elementTypeSignature.setScanResult(scanResult); + nestedType.setScanResult(scanResult); + if (arrayClassInfo != null) { + arrayClassInfo.setScanResult(scanResult); } } - /* (non-Javadoc) - * @see io.github.classgraph.HierarchicalTypeSignature#findReferencedClassNames(java.util.Set) + /** + * Get the names of any classes referenced in the type signature. + * + * @param refdClassNames + * the referenced class names. + */ + @Override + protected void findReferencedClassNames(final Set refdClassNames) { + nestedType.findReferencedClassNames(refdClassNames); + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get a {@code Class} reference for the innermost array element type. Causes the ClassLoader to load the + * class, if it is not already loaded. + * + * @param ignoreExceptions + * Whether or not to ignore exceptions. + * @return a {@code Class} reference for the innermost array element type. Also works for arrays of primitive + * element type. + */ + public Class loadElementClass(final boolean ignoreExceptions) { + if (elementClassRef == null) { + // Try resolving element type against base types (int, etc.) + final TypeSignature elementTypeSignature = getElementTypeSignature(); + if (elementTypeSignature instanceof BaseTypeSignature) { + elementClassRef = ((BaseTypeSignature) elementTypeSignature).getType(); + } else { + if (scanResult != null) { + elementClassRef = elementTypeSignature.loadClass(ignoreExceptions); + } else { + // Fallback, if scanResult is not set + final String elementTypeName = elementTypeSignature.getClassName(); + try { + elementClassRef = Class.forName(elementTypeName); + } catch (final Throwable t) { + if (!ignoreExceptions) { + throw new IllegalArgumentException( + "Could not load array element class " + elementTypeName, t); + } + } + } + } + } + return elementClassRef; + } + + /** + * Get a {@code Class} reference for the array element type. Causes the ClassLoader to load the element + * class, if it is not already loaded. + * + * @return a {@code Class} reference for the array element type. Also works for arrays of primitive element + * type. + */ + public Class loadElementClass() { + return loadElementClass(/* ignoreExceptions = */ false); + } + + /** + * Obtain a {@code Class} reference for the array class named by this {@link ArrayClassInfo} object. Causes + * the ClassLoader to load the element class, if it is not already loaded. + * + * @param ignoreExceptions + * Whether or not to ignore exceptions. + * @return The class reference, or null, if ignoreExceptions is true and there was an exception or error loading + * the class. + * @throws IllegalArgumentException + * if ignoreExceptions is false and there were problems loading the class. + */ + @Override + public Class loadClass(final boolean ignoreExceptions) { + if (classRef == null) { + // Get the element type + Class eltClassRef = null; + if (ignoreExceptions) { + try { + eltClassRef = loadElementClass(); + } catch (final IllegalArgumentException e) { + return null; + } + } else { + eltClassRef = loadElementClass(); + } + if (eltClassRef == null) { + throw new IllegalArgumentException( + "Could not load array element class " + getElementTypeSignature()); + } + // Create an array of the target number of dimensions, with size zero in each dimension + final Object eltArrayInstance = Array.newInstance(eltClassRef, new int[getNumDimensions()]); + // Get the class reference from the array instance + classRef = eltArrayInstance.getClass(); + } + return classRef; + } + + /** + * Obtain a {@code Class} reference for the array class named by this {@link ArrayClassInfo} object. Causes + * the ClassLoader to load the element class, if it is not already loaded. + * + * @return The class reference. + * @throws IllegalArgumentException + * if there were problems loading the class. */ @Override - void findReferencedClassNames(final Set referencedClassNames) { - elementTypeSignature.findReferencedClassNames(referencedClassNames); + public Class loadClass() { + return loadClass(/* ignoreExceptions = */ false); } // ------------------------------------------------------------------------------------------------------------- @@ -120,7 +327,7 @@ void findReferencedClassNames(final Set referencedClassNames) { */ @Override public int hashCode() { - return elementTypeSignature.hashCode() + numDims * 15; + return 1 + nestedType.hashCode(); } /* (non-Javadoc) @@ -128,14 +335,14 @@ public int hashCode() { */ @Override public boolean equals(final Object obj) { - if (this == obj) { + if (obj == this) { return true; - } - if (!(obj instanceof ArrayTypeSignature)) { + } else if (!(obj instanceof ArrayTypeSignature)) { return false; } - final ArrayTypeSignature o = (ArrayTypeSignature) obj; - return o.elementTypeSignature.equals(this.elementTypeSignature) && o.numDims == this.numDims; + final ArrayTypeSignature other = (ArrayTypeSignature) obj; + return Objects.equals(this.typeAnnotationInfo, other.typeAnnotationInfo) + && this.nestedType.equals(other.nestedType); } /* (non-Javadoc) @@ -150,24 +357,41 @@ public boolean equalsIgnoringTypeParams(final TypeSignature other) { return false; } final ArrayTypeSignature o = (ArrayTypeSignature) other; - return o.elementTypeSignature.equalsIgnoringTypeParams(this.elementTypeSignature) - && o.numDims == this.numDims; + return this.nestedType.equalsIgnoringTypeParams(o.nestedType); } - /* (non-Javadoc) - * @see io.github.classgraph.TypeSignature#toStringInternal(boolean) - */ + // ------------------------------------------------------------------------------------------------------------- + @Override - protected String toStringInternal(final boolean useSimpleNames) { - final StringBuilder buf = new StringBuilder(); - buf.append( - useSimpleNames ? elementTypeSignature.toStringWithSimpleNames() : elementTypeSignature.toString()); - for (int i = 0; i < numDims; i++) { + protected void toStringInternal(final boolean useSimpleNames, final AnnotationInfoList annotationsToExclude, + final StringBuilder buf) { + // Start with innermost array element type + getElementTypeSignature().toStringInternal(useSimpleNames, annotationsToExclude, buf); + + // Append array dimensions + for (ArrayTypeSignature curr = this;;) { + if (curr.typeAnnotationInfo != null && !curr.typeAnnotationInfo.isEmpty()) { + for (final AnnotationInfo annotationInfo : curr.typeAnnotationInfo) { + if (buf.length() == 0 || buf.charAt(buf.length() - 1) != ' ') { + buf.append(' '); + } + annotationInfo.toString(useSimpleNames, buf); + } + buf.append(' '); + } + buf.append("[]"); + + if (curr.nestedType instanceof ArrayTypeSignature) { + curr = (ArrayTypeSignature) curr.nestedType; + } else { + break; + } } - return buf.toString(); } + // ------------------------------------------------------------------------------------------------------------- + /** * Parses the array type signature. * @@ -181,6 +405,7 @@ protected String toStringInternal(final boolean useSimpleNames) { */ static ArrayTypeSignature parse(final Parser parser, final String definingClassName) throws ParseException { int numArrayDims = 0; + final int begin = parser.getPosition(); while (parser.peek() == '[') { numArrayDims++; parser.next(); @@ -190,7 +415,8 @@ static ArrayTypeSignature parse(final Parser parser, final String definingClassN if (elementTypeSignature == null) { throw new ParseException(parser, "elementTypeSignature == null"); } - return new ArrayTypeSignature(elementTypeSignature, numArrayDims); + final String typeSignatureStr = parser.getSubsequence(begin, parser.getPosition()).toString(); + return new ArrayTypeSignature(elementTypeSignature, numArrayDims, typeSignatureStr); } else { return null; } diff --git a/src/main/java/io/github/classgraph/BaseTypeSignature.java b/src/main/java/io/github/classgraph/BaseTypeSignature.java index 1cd4d75cb..db42bbb88 100644 --- a/src/main/java/io/github/classgraph/BaseTypeSignature.java +++ b/src/main/java/io/github/classgraph/BaseTypeSignature.java @@ -28,69 +28,179 @@ */ package io.github.classgraph; +import java.util.List; +import java.util.Objects; import java.util.Set; +import io.github.classgraph.Classfile.TypePathNode; import nonapi.io.github.classgraph.types.Parser; /** A type signature for a base type (byte, char, double, float, int, long, short, boolean, or void). */ public class BaseTypeSignature extends TypeSignature { - /** A base type (byte, char, double, float, int, long, short, boolean, or void). */ - private final String baseType; + /** The type signature character used to represent the base type. */ + private final char typeSignatureChar; // ------------------------------------------------------------------------------------------------------------- /** * Constructor. - * - * @param baseType - * the base type */ - BaseTypeSignature(final String baseType) { + BaseTypeSignature(final char typeSignatureChar) { super(); - this.baseType = baseType; + switch (typeSignatureChar) { + case 'B': + case 'C': + case 'D': + case 'F': + case 'I': + case 'J': + case 'S': + case 'Z': + case 'V': + this.typeSignatureChar = typeSignatureChar; + break; + default: + throw new IllegalArgumentException( + "Illegal " + BaseTypeSignature.class.getSimpleName() + " type: '" + typeSignatureChar + "'"); + } } // ------------------------------------------------------------------------------------------------------------- /** - * Get the type as a string. + * Get the name of the type as a string. * - * @return The base type, such as "int", "float", or "void". + * @param typeChar + * the type character, e.g. 'I'. + * @return The name of the type, e.g. "int", or null if there was no match. */ - public String getTypeStr() { - return baseType; + static String getTypeStr(final char typeChar) { + switch (typeChar) { + case 'B': + return "byte"; + case 'C': + return "char"; + case 'D': + return "double"; + case 'F': + return "float"; + case 'I': + return "int"; + case 'J': + return "long"; + case 'S': + return "short"; + case 'Z': + return "boolean"; + case 'V': + return "void"; + default: + return null; + } } /** - * Get the type. + * Get the name of the type as a string. * - * @return The class of the base type, such as int.class, float.class, or void.class. + * @param typeStr + * the type character, e.g. "int". + * @return The type, character, e.g. 'I', or '\0' if there was no match. */ - public Class getType() { - switch (baseType) { + static char getTypeChar(final String typeStr) { + switch (typeStr) { case "byte": - return byte.class; + return 'B'; case "char": - return char.class; + return 'C'; case "double": - return double.class; + return 'D'; case "float": - return float.class; + return 'F'; case "int": - return int.class; + return 'I'; case "long": - return long.class; + return 'J'; case "short": - return short.class; + return 'S'; case "boolean": - return boolean.class; + return 'Z'; case "void": + return 'V'; + default: + return '\0'; + } + } + + /** + * Get the type for a type character. + * + * @param typeChar + * the type character, e.g. 'I'. + * @return The type class, e.g. int.class, or null if there was no match. + */ + static Class getType(final char typeChar) { + switch (typeChar) { + case 'B': + return byte.class; + case 'C': + return char.class; + case 'D': + return double.class; + case 'F': + return float.class; + case 'I': + return int.class; + case 'J': + return long.class; + case 'S': + return short.class; + case 'Z': + return boolean.class; + case 'V': return void.class; default: - throw new IllegalArgumentException("Unknown base type " + baseType); + return null; } } + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get the type signature char used to represent the type, e.g. 'I' for int. + * + * @return the type signature char, as a one-char String. + */ + public char getTypeSignatureChar() { + return typeSignatureChar; + } + + /** + * Get the name of the type as a string. + * + * @return The name of the type, such as "int", "float", or "void". + */ + public String getTypeStr() { + return getTypeStr(typeSignatureChar); + } + + /** + * Get the type. + * + * @return The class of the base type, such as int.class, float.class, or void.class. + */ + public Class getType() { + return getType(typeSignatureChar); + } + + // ------------------------------------------------------------------------------------------------------------- + + @Override + protected void addTypeAnnotation(final List typePath, final AnnotationInfo annotationInfo) { + addTypeAnnotation(annotationInfo); + } + + // ------------------------------------------------------------------------------------------------------------- + /* (non-Javadoc) * @see io.github.classgraph.ScanResultObject#loadClass() */ @@ -106,8 +216,8 @@ Class loadClass() { Class loadClass(final Class superclassOrInterfaceType) { final Class type = getType(); if (!superclassOrInterfaceType.isAssignableFrom(type)) { - throw new IllegalArgumentException( - "Primitive class " + baseType + " cannot be cast to " + superclassOrInterfaceType.getName()); + throw new IllegalArgumentException("Primitive class " + getTypeStr() + " cannot be cast to " + + superclassOrInterfaceType.getName()); } @SuppressWarnings("unchecked") final Class classT = (Class) type; @@ -127,31 +237,31 @@ static BaseTypeSignature parse(final Parser parser) { switch (parser.peek()) { case 'B': parser.next(); - return new BaseTypeSignature("byte"); + return new BaseTypeSignature('B'); case 'C': parser.next(); - return new BaseTypeSignature("char"); + return new BaseTypeSignature('C'); case 'D': parser.next(); - return new BaseTypeSignature("double"); + return new BaseTypeSignature('D'); case 'F': parser.next(); - return new BaseTypeSignature("float"); + return new BaseTypeSignature('F'); case 'I': parser.next(); - return new BaseTypeSignature("int"); + return new BaseTypeSignature('I'); case 'J': parser.next(); - return new BaseTypeSignature("long"); + return new BaseTypeSignature('J'); case 'S': parser.next(); - return new BaseTypeSignature("short"); + return new BaseTypeSignature('S'); case 'Z': parser.next(); - return new BaseTypeSignature("boolean"); + return new BaseTypeSignature('Z'); case 'V': parser.next(); - return new BaseTypeSignature("void"); + return new BaseTypeSignature('V'); default: return null; } @@ -164,8 +274,7 @@ static BaseTypeSignature parse(final Parser parser) { */ @Override protected String getClassName() { - // getClassInfo() is not valid for this type, so getClassName() does not need to be implemented - throw new IllegalArgumentException("getClassName() cannot be called here"); + return getTypeStr(); } /* (non-Javadoc) @@ -173,15 +282,32 @@ protected String getClassName() { */ @Override protected ClassInfo getClassInfo() { - throw new IllegalArgumentException("getClassInfo() cannot be called here"); + return null; + } + + /** + * Get the names of any classes referenced in the type signature. + * + * @param refdClassNames + * the referenced class names. + */ + @Override + protected void findReferencedClassNames(final Set refdClassNames) { + // Don't add byte.class, int.class, etc. } /* (non-Javadoc) - * @see io.github.classgraph.HierarchicalTypeSignature#findReferencedClassNames(java.util.Set) + * @see io.github.classgraph.ScanResultObject#setScanResult(ScanResult) */ @Override - void findReferencedClassNames(final Set classNameListOut) { - // Don't return byte.class, int.class, etc. + void setScanResult(final ScanResult scanResult) { + // Don't set ScanResult for BaseTypeSignature objects (#419). + // The ScanResult is not needed, since this class does not classload through the ScanResult. + // Also, specific instances of BaseTypeSignature for each primitive type are assigned to static fields + // in this class, which are shared across all usages of this class, so they should not contain any + // values that are specific to a given ScanResult. Setting the ScanResult from different scan processes + // would cause the scanResult field to only reflect the result of the most recent scan, and the reference + // to that scan would prevent garbage collection. } // ------------------------------------------------------------------------------------------------------------- @@ -191,7 +317,7 @@ void findReferencedClassNames(final Set classNameListOut) { */ @Override public int hashCode() { - return baseType.hashCode(); + return typeSignatureChar; } /* (non-Javadoc) @@ -199,7 +325,14 @@ public int hashCode() { */ @Override public boolean equals(final Object obj) { - return obj instanceof BaseTypeSignature && ((BaseTypeSignature) obj).baseType.equals(this.baseType); + if (obj == this) { + return true; + } else if (!(obj instanceof BaseTypeSignature)) { + return false; + } + final BaseTypeSignature other = (BaseTypeSignature) obj; + return Objects.equals(this.typeAnnotationInfo, other.typeAnnotationInfo) + && other.typeSignatureChar == this.typeSignatureChar; } /* (non-Javadoc) @@ -210,14 +343,22 @@ public boolean equalsIgnoringTypeParams(final TypeSignature other) { if (!(other instanceof BaseTypeSignature)) { return false; } - return baseType.equals(((BaseTypeSignature) other).baseType); + return typeSignatureChar == ((BaseTypeSignature) other).typeSignatureChar; } - /* (non-Javadoc) - * @see io.github.classgraph.TypeSignature#toStringInternal(boolean) - */ + // ------------------------------------------------------------------------------------------------------------- + @Override - protected String toStringInternal(final boolean useSimpleNames) { - return baseType; + protected void toStringInternal(final boolean useSimpleNames, final AnnotationInfoList annotationsToExclude, + final StringBuilder buf) { + if (typeAnnotationInfo != null) { + for (final AnnotationInfo annotationInfo : typeAnnotationInfo) { + if (annotationsToExclude == null || !annotationsToExclude.contains(annotationInfo)) { + annotationInfo.toString(useSimpleNames, buf); + buf.append(' '); + } + } + } + buf.append(getTypeStr()); } } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/ClassGraph.java b/src/main/java/io/github/classgraph/ClassGraph.java index 0cba62060..d77c06a8b 100644 --- a/src/main/java/io/github/classgraph/ClassGraph.java +++ b/src/main/java/io/github/classgraph/ClassGraph.java @@ -29,8 +29,13 @@ package io.github.classgraph; import java.io.File; +import java.io.InputStream; +import java.lang.reflect.AccessibleObject; import java.net.URI; import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; @@ -41,11 +46,12 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.regex.Pattern; -import nonapi.io.github.classgraph.ScanSpec; -import nonapi.io.github.classgraph.WhiteBlackList; import nonapi.io.github.classgraph.classpath.SystemJarFinder; import nonapi.io.github.classgraph.concurrency.AutoCloseableExecutorService; import nonapi.io.github.classgraph.concurrency.InterruptionChecker; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.AcceptReject; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.JarUtils; import nonapi.io.github.classgraph.utils.LogNode; import nonapi.io.github.classgraph.utils.VersionFinder; @@ -59,7 +65,6 @@ * https://github.com/classgraph/classgraph/wiki */ public class ClassGraph { - /** The scanning specification. */ ScanSpec scanSpec = new ScanSpec(); @@ -67,7 +72,7 @@ public class ClassGraph { * The default number of worker threads to use while scanning. This number gave the best results on a relatively * modern laptop with SSD, while scanning a large classpath. */ - private static final int DEFAULT_NUM_WORKER_THREADS = Math.max( + static final int DEFAULT_NUM_WORKER_THREADS = Math.max( // Always scan with at least 2 threads 2, // (int) Math.ceil( @@ -77,14 +82,54 @@ public class ClassGraph { Runtime.getRuntime().availableProcessors() * 1.25) // ); - /** If non-null, log while scanning. */ + /** + * Method to use to attempt to circumvent encapsulation in JDK 16+, in order to get access to a classloader's + * private classpath. + */ + public static enum CircumventEncapsulationMethod { + /** + * Use the reflection API and {@link AccessibleObject#setAccessible(boolean)} to try to gain access to + * private classpath fields or methods in order to determine the classpath. + */ + NONE, + + /** + * Use the Narcissus library to try to gain access to + * private classloader fields or methods in order to determine the classpath. + */ + NARCISSUS, + } + + /** + * If you are running on JDK 16+, the JDK enforces strong encapsulation, and ClassGraph may be unable to read + * the classpath from your classloader if the classloader does not make the classpath available via a public + * method or field. + * + *

+ * To enable a workaround to this, set this static field to {@link CircumventEncapsulationMethod#NARCISSUS} + * before interacting with ClassGraph in any other way, and also include the + * Narcissus library on the classpath or module path. + * + *

+ * Narcissus uses JNI to circumvent encapsulation and field/method access controls. Narcissus employs a native + * code library, and is currently only compiled for Linux x86/x64, Windows x86/x64, and Mac OS X x64 bit. + */ + public static CircumventEncapsulationMethod CIRCUMVENT_ENCAPSULATION = CircumventEncapsulationMethod.NONE; + + private final ReflectionUtils reflectionUtils; + + /** + * If non-null, log while scanning. + */ private LogNode topLevelLog; // ------------------------------------------------------------------------------------------------------------- /** Construct a ClassGraph instance. */ public ClassGraph() { - // Blank + reflectionUtils = new ReflectionUtils(); + // Initialize ScanResult, if this is the first call to ClassGraph constructor + ScanResult.init(reflectionUtils); } /** @@ -110,6 +155,20 @@ public ClassGraph verbose() { return this; } + /** + * Switches on verbose logging to System.err if verbose is true. + * + * @param verbose + * if true, enable verbose logging. + * @return this (for method chaining). + */ + public ClassGraph verbose(final boolean verbose) { + if (verbose) { + verbose(); + } + return this; + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -137,12 +196,14 @@ public ClassGraph enableAllInfo() { } /** - * Enables the scanning of classfiles, producing {@link ClassInfo} objects in the {@link ScanResult}. + * Enables the scanning of classfiles, producing {@link ClassInfo} objects in the {@link ScanResult}. Implicitly + * disables {@link #enableMultiReleaseVersions()}. * * @return this (for method chaining). */ public ClassGraph enableClassInfo() { scanSpec.enableClassInfo = true; + scanSpec.enableMultiReleaseVersions = false; return this; } @@ -214,7 +275,30 @@ public ClassGraph ignoreFieldVisibility() { /** * Enables the saving of static final field constant initializer values. By default, constant initializer values - * are not scanned. Automatically calls {@link #enableClassInfo()} and {@link #enableFieldInfo()}. + * are not scanned. If this is enabled, you can obtain the constant field initializer values from + * {@link FieldInfo#getConstantInitializerValue()}. + * + *

+ * Note that constant initializer values are usually only of primitive type, or String constants (or values that + * can be computed and reduced to one of those types at compiletime). + * + *

+ * Also note that it is up to the compiler as to whether or not a constant-valued field is assigned as a + * constant in the field definition itself, or whether it is assigned manually in static class initializer + * blocks -- so your mileage may vary in being able to extract constant initializer values. + * + *

+ * In fact in Kotlin, even constant initializers for non-static / non-final fields are stored in a field + * attribute in the classfile (and so these values may be picked up by ClassGraph by calling this method), + * although any field initializers for non-static fields are supposed to be ignored by the JVM according to the + * classfile spec, so the Kotlin compiler may change in future to stop generating these values, and you probably + * shouldn't rely on being able to get the initializers for non-static fields in Kotlin. (As far as non-final + * fields, javac simply does not add constant initializer values to the field attributes list for non-final + * fields, even if they are static, but the spec doesn't say whether or not the JVM should ignore constant + * initializers for non-final fields.) + * + *

+ * Automatically calls {@link #enableClassInfo()} and {@link #enableFieldInfo()}. * * @return this (for method chaining). */ @@ -319,9 +403,9 @@ public ClassGraph disableModuleScanning() { // ------------------------------------------------------------------------------------------------------------- /** - * Causes ClassGraph to return classes that are not in the whitelisted packages, but that are directly referred - * to by classes within whitelisted packages as a superclass, implemented interface or annotation. - * (Automatically calls {@link #enableClassInfo()}.) + * Causes ClassGraph to return classes that are not in the accepted packages, but that are directly referred to + * by classes within accepted packages as a superclass, implemented interface or annotation. (Automatically + * calls {@link #enableClassInfo()}.) * * @return this (for method chaining). */ @@ -370,7 +454,12 @@ public ClassGraph removeTemporaryFilesAfterScan() { * @return this (for method chaining). */ public ClassGraph overrideClasspath(final String overrideClasspath) { - scanSpec.overrideClasspath(overrideClasspath); + if (overrideClasspath.isEmpty()) { + throw new IllegalArgumentException("Can't override classpath with an empty path"); + } + for (final String classpathElement : JarUtils.smartPathSplit(overrideClasspath, scanSpec)) { + scanSpec.addClasspathOverride(classpathElement); + } return this; } @@ -387,11 +476,12 @@ public ClassGraph overrideClasspath(final String overrideClasspath) { * @return this (for method chaining). */ public ClassGraph overrideClasspath(final Iterable overrideClasspathElements) { - final String overrideClasspath = JarUtils.pathElementsToPathStr(overrideClasspathElements); - if (overrideClasspath.isEmpty()) { + if (!overrideClasspathElements.iterator().hasNext()) { throw new IllegalArgumentException("Can't override classpath with an empty path"); } - overrideClasspath(overrideClasspath); + for (final Object classpathElement : overrideClasspathElements) { + scanSpec.addClasspathOverride(classpathElement); + } return this; } @@ -408,11 +498,12 @@ public ClassGraph overrideClasspath(final Iterable overrideClasspathElements) * @return this (for method chaining). */ public ClassGraph overrideClasspath(final Object... overrideClasspathElements) { - final String overrideClasspath = JarUtils.pathElementsToPathStr(overrideClasspathElements); - if (overrideClasspath.isEmpty()) { + if (overrideClasspathElements.length == 0) { throw new IllegalArgumentException("Can't override classpath with an empty path"); } - overrideClasspath(overrideClasspath); + for (final Object classpathElement : overrideClasspathElements) { + scanSpec.addClasspathOverride(classpathElement); + } return this; } @@ -437,6 +528,22 @@ public interface ClasspathElementFilter { boolean includeClasspathElement(String classpathElementPathStr); } + /** + * Add a classpath element URL filter. The includeClasspathElement method should return true if the {@link URL} + * passed to it corresponds to a classpath element that you want to scan. + */ + @FunctionalInterface + public interface ClasspathElementURLFilter { + /** + * Whether or not to include a given classpath element in the scan. + * + * @param classpathElementURL + * The {@link URL} of a classpath element. + * @return true if you want to scan the {@link URL}. + */ + boolean includeClasspathElement(URL classpathElementURL); + } + /** * Add a classpath element filter. The provided ClasspathElementFilter should return true if the path string * passed to it is a path you want to scan. @@ -451,6 +558,20 @@ public ClassGraph filterClasspathElements(final ClasspathElementFilter classpath return this; } + /** + * Add a classpath element filter. The provided ClasspathElementFilter should return true if the {@link URL} + * passed to it is a URL you want to scan. + * + * @param classpathElementURLFilter + * The filter function to use. This function should return true if the classpath element {@link URL} + * should be scanned, and false if not. + * @return this (for method chaining). + */ + public ClassGraph filterClasspathElementsByURL(final ClasspathElementURLFilter classpathElementURLFilter) { + scanSpec.filterClasspathElements(classpathElementURLFilter); + return this; + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -471,7 +592,9 @@ public ClassGraph addClassLoader(final ClassLoader classLoader) { /** * Completely override (and ignore) system ClassLoaders and the java.class.path system property. Also causes - * modules not to be scanned. + * modules not to be scanned. Note that you may want to use this together with + * {@link #ignoreParentClassLoaders()} to extract classpath URLs from only the classloaders you specified in the + * parameter to `overrideClassLoaders`, and not their parent classloaders. * *

* This call is ignored if {@link #overrideClasspath(String)} is called. @@ -548,7 +671,7 @@ public ClassGraph ignoreParentModuleLayers() { * Scan one or more specific packages and their sub-packages. * *

- * N.B. Automatically calls {@link #enableClassInfo()} -- call {@link #whitelistPaths(String...)} instead if you + * N.B. Automatically calls {@link #enableClassInfo()} -- call {@link #acceptPaths(String...)} instead if you * only need to scan resources. * * @param packageNames @@ -556,35 +679,45 @@ public ClassGraph ignoreParentModuleLayers() { * wildcard ({@code '*'}). * @return this (for method chaining). */ - public ClassGraph whitelistPackages(final String... packageNames) { + public ClassGraph acceptPackages(final String... packageNames) { enableClassInfo(); for (final String packageName : packageNames) { - final String packageNameNormalized = WhiteBlackList.normalizePackageOrClassName(packageName); - if (packageNameNormalized.startsWith("!") || packageNameNormalized.startsWith("-")) { - throw new IllegalArgumentException( - "This style of whitelisting/blacklisting is no longer supported: " + packageNameNormalized); - } - // Whitelist package - scanSpec.packageWhiteBlackList.addToWhitelist(packageNameNormalized); - final String path = WhiteBlackList.packageNameToPath(packageNameNormalized); - scanSpec.pathWhiteBlackList.addToWhitelist(path + "/"); + final String packageNameNormalized = AcceptReject.normalizePackageOrClassName(packageName); + // Accept package + scanSpec.packageAcceptReject.addToAccept(packageNameNormalized); + final String path = AcceptReject.packageNameToPath(packageNameNormalized); + scanSpec.pathAcceptReject.addToAccept(path + "/"); if (packageNameNormalized.isEmpty()) { - scanSpec.pathWhiteBlackList.addToWhitelist(""); + scanSpec.pathAcceptReject.addToAccept(""); } if (!packageNameNormalized.contains("*")) { - // Whitelist sub-packages + // Accept sub-packages if (packageNameNormalized.isEmpty()) { - scanSpec.packagePrefixWhiteBlackList.addToWhitelist(""); - scanSpec.pathPrefixWhiteBlackList.addToWhitelist(""); + scanSpec.packagePrefixAcceptReject.addToAccept(""); + scanSpec.pathPrefixAcceptReject.addToAccept(""); } else { - scanSpec.packagePrefixWhiteBlackList.addToWhitelist(packageNameNormalized + "."); - scanSpec.pathPrefixWhiteBlackList.addToWhitelist(path + "/"); + scanSpec.packagePrefixAcceptReject.addToAccept(packageNameNormalized + "."); + scanSpec.pathPrefixAcceptReject.addToAccept(path + "/"); } } } return this; } + /** + * Use {@link #acceptPackages(String...)} instead. + * + * @param packageNames + * The fully-qualified names of packages to scan (using '.' as a separator). May include a glob + * wildcard ({@code '*'}). + * @return this (for method chaining). + * @deprecated Use {@link #acceptPackages(String...)} instead. + */ + @Deprecated + public ClassGraph whitelistPackages(final String... packageNames) { + return acceptPackages(packageNames); + } + /** * Scan one or more specific paths, and their sub-directories or nested paths. * @@ -593,36 +726,50 @@ public ClassGraph whitelistPackages(final String... packageNames) { * separator). May include a glob wildcard ({@code '*'}). * @return this (for method chaining). */ - public ClassGraph whitelistPaths(final String... paths) { + public ClassGraph acceptPaths(final String... paths) { for (final String path : paths) { - final String pathNormalized = WhiteBlackList.normalizePath(path); - // Whitelist path - final String packageName = WhiteBlackList.pathToPackageName(pathNormalized); - scanSpec.packageWhiteBlackList.addToWhitelist(packageName); - scanSpec.pathWhiteBlackList.addToWhitelist(pathNormalized + "/"); + final String pathNormalized = AcceptReject.normalizePath(path); + // Accept path + final String packageName = AcceptReject.pathToPackageName(pathNormalized); + scanSpec.packageAcceptReject.addToAccept(packageName); + scanSpec.pathAcceptReject.addToAccept(pathNormalized + "/"); if (pathNormalized.isEmpty()) { - scanSpec.pathWhiteBlackList.addToWhitelist(""); + scanSpec.pathAcceptReject.addToAccept(""); } if (!pathNormalized.contains("*")) { - // Whitelist sub-directories / nested paths + // Accept sub-directories / nested paths if (pathNormalized.isEmpty()) { - scanSpec.packagePrefixWhiteBlackList.addToWhitelist(""); - scanSpec.pathPrefixWhiteBlackList.addToWhitelist(""); + scanSpec.packagePrefixAcceptReject.addToAccept(""); + scanSpec.pathPrefixAcceptReject.addToAccept(""); } else { - scanSpec.packagePrefixWhiteBlackList.addToWhitelist(packageName + "."); - scanSpec.pathPrefixWhiteBlackList.addToWhitelist(pathNormalized + "/"); + scanSpec.packagePrefixAcceptReject.addToAccept(packageName + "."); + scanSpec.pathPrefixAcceptReject.addToAccept(pathNormalized + "/"); } } } return this; } + /** + * Use {@link #acceptPaths(String...)} instead. + * + * @param paths + * The paths to scan, relative to the package root of the classpath element (with '/' as a + * separator). May include a glob wildcard ({@code '*'}). + * @return this (for method chaining). + * @deprecated Use {@link #acceptPaths(String...)} instead. + */ + @Deprecated + public ClassGraph whitelistPaths(final String... paths) { + return acceptPaths(paths); + } + /** * Scan one or more specific packages, without recursively scanning sub-packages unless they are themselves - * whitelisted. + * accepted. * *

- * N.B. Automatically calls {@link #enableClassInfo()} -- call {@link #whitelistPathsNonRecursive(String...)} + * N.B. Automatically calls {@link #enableClassInfo()} -- call {@link #acceptPathsNonRecursive(String...)} * instead if you only need to scan resources. * *

@@ -635,27 +782,40 @@ public ClassGraph whitelistPaths(final String... paths) { * * @return this (for method chaining). */ - public ClassGraph whitelistPackagesNonRecursive(final String... packageNames) { + public ClassGraph acceptPackagesNonRecursive(final String... packageNames) { enableClassInfo(); for (final String packageName : packageNames) { - final String packageNameNormalized = WhiteBlackList.normalizePackageOrClassName(packageName); + final String packageNameNormalized = AcceptReject.normalizePackageOrClassName(packageName); if (packageNameNormalized.contains("*")) { throw new IllegalArgumentException("Cannot use a glob wildcard here: " + packageNameNormalized); } - // Whitelist package, but not sub-packages - scanSpec.packageWhiteBlackList.addToWhitelist(packageNameNormalized); - scanSpec.pathWhiteBlackList - .addToWhitelist(WhiteBlackList.packageNameToPath(packageNameNormalized) + "/"); + // Accept package, but not sub-packages + scanSpec.packageAcceptReject.addToAccept(packageNameNormalized); + scanSpec.pathAcceptReject.addToAccept(AcceptReject.packageNameToPath(packageNameNormalized) + "/"); if (packageNameNormalized.isEmpty()) { - scanSpec.pathWhiteBlackList.addToWhitelist(""); + scanSpec.pathAcceptReject.addToAccept(""); } } return this; } + /** + * Use {@link #acceptPackagesNonRecursive(String...)} instead. + * + * @param packageNames + * The fully-qualified names of packages to scan (with '.' as a separator). May not include a glob + * wildcard ({@code '*'}). + * @return this (for method chaining). + * @deprecated Use {@link #acceptPackagesNonRecursive(String...)} instead. + */ + @Deprecated + public ClassGraph whitelistPackagesNonRecursive(final String... packageNames) { + return acceptPackagesNonRecursive(packageNames); + } + /** * Scan one or more specific paths, without recursively scanning sub-directories or nested paths unless they are - * themselves whitelisted. + * themselves accepted. * *

* This may be particularly useful for scanning the package root ("") without recursively scanning everything in @@ -666,211 +826,299 @@ public ClassGraph whitelistPackagesNonRecursive(final String... packageNames) { * separator). May not include a glob wildcard ({@code '*'}). * @return this (for method chaining). */ - public ClassGraph whitelistPathsNonRecursive(final String... paths) { + public ClassGraph acceptPathsNonRecursive(final String... paths) { for (final String path : paths) { if (path.contains("*")) { throw new IllegalArgumentException("Cannot use a glob wildcard here: " + path); } - final String pathNormalized = WhiteBlackList.normalizePath(path); - // Whitelist path, but not sub-directories / nested paths - scanSpec.packageWhiteBlackList.addToWhitelist(WhiteBlackList.pathToPackageName(pathNormalized)); - scanSpec.pathWhiteBlackList.addToWhitelist(pathNormalized + "/"); + final String pathNormalized = AcceptReject.normalizePath(path); + // Accept path, but not sub-directories / nested paths + scanSpec.packageAcceptReject.addToAccept(AcceptReject.pathToPackageName(pathNormalized)); + scanSpec.pathAcceptReject.addToAccept(pathNormalized + "/"); if (pathNormalized.isEmpty()) { - scanSpec.pathWhiteBlackList.addToWhitelist(""); + scanSpec.pathAcceptReject.addToAccept(""); } } return this; } + /** + * Use {@link #acceptPathsNonRecursive(String...)} instead. + * + * @param paths + * The paths to scan, relative to the package root of the classpath element (with '/' as a + * separator). May not include a glob wildcard ({@code '*'}). + * @return this (for method chaining). + * @deprecated Use {@link #acceptPathsNonRecursive(String...)} instead. + */ + @Deprecated + public ClassGraph whitelistPathsNonRecursive(final String... paths) { + return acceptPathsNonRecursive(paths); + } + /** * Prevent the scanning of one or more specific packages and their sub-packages. * *

- * N.B. Automatically calls {@link #enableClassInfo()} -- call {@link #blacklistPaths(String...)} instead if you + * N.B. Automatically calls {@link #enableClassInfo()} -- call {@link #rejectPaths(String...)} instead if you * only need to scan resources. * * @param packageNames - * The fully-qualified names of packages to blacklist (with '.' as a separator). May include a glob + * The fully-qualified names of packages to reject (with '.' as a separator). May include a glob * wildcard ({@code '*'}). * @return this (for method chaining). */ - public ClassGraph blacklistPackages(final String... packageNames) { + public ClassGraph rejectPackages(final String... packageNames) { enableClassInfo(); for (final String packageName : packageNames) { - final String packageNameNormalized = WhiteBlackList.normalizePackageOrClassName(packageName); + final String packageNameNormalized = AcceptReject.normalizePackageOrClassName(packageName); if (packageNameNormalized.isEmpty()) { throw new IllegalArgumentException( - "Blacklisting the root package (\"\") will cause nothing to be scanned"); + "Rejecting the root package (\"\") will cause nothing to be scanned"); } - // Blacklisting always prevents further recursion, no need to blacklist sub-packages - scanSpec.packageWhiteBlackList.addToBlacklist(packageNameNormalized); - final String path = WhiteBlackList.packageNameToPath(packageNameNormalized); - scanSpec.pathWhiteBlackList.addToBlacklist(path + "/"); + // Rejecting always prevents further recursion, no need to reject sub-packages + scanSpec.packageAcceptReject.addToReject(packageNameNormalized); + final String path = AcceptReject.packageNameToPath(packageNameNormalized); + scanSpec.pathAcceptReject.addToReject(path + "/"); if (!packageNameNormalized.contains("*")) { - // Blacklist sub-packages (zipfile entries can occur in any order) - scanSpec.packagePrefixWhiteBlackList.addToBlacklist(packageNameNormalized + "."); - scanSpec.pathPrefixWhiteBlackList.addToBlacklist(path + "/"); + // Reject sub-packages (zipfile entries can occur in any order) + scanSpec.packagePrefixAcceptReject.addToReject(packageNameNormalized + "."); + scanSpec.pathPrefixAcceptReject.addToReject(path + "/"); } } return this; } + /** + * Use {@link #rejectPackages(String...)} instead. + * + * @param packageNames + * The fully-qualified names of packages to reject (with '.' as a separator). May include a glob + * wildcard ({@code '*'}). + * @return this (for method chaining). + * @deprecated Use {@link #rejectPackages(String...)} instead. + */ + @Deprecated + public ClassGraph blacklistPackages(final String... packageNames) { + return rejectPackages(packageNames); + } + /** * Prevent the scanning of one or more specific paths and their sub-directories / nested paths. * * @param paths - * The paths to blacklist (with '/' as a separator). May include a glob wildcard ({@code '*'}). + * The paths to reject (with '/' as a separator). May include a glob wildcard ({@code '*'}). * @return this (for method chaining). */ - public ClassGraph blacklistPaths(final String... paths) { + public ClassGraph rejectPaths(final String... paths) { for (final String path : paths) { - final String pathNormalized = WhiteBlackList.normalizePath(path); + final String pathNormalized = AcceptReject.normalizePath(path); if (pathNormalized.isEmpty()) { throw new IllegalArgumentException( - "Blacklisting the root package (\"\") will cause nothing to be scanned"); + "Rejecting the root package (\"\") will cause nothing to be scanned"); } - // Blacklisting always prevents further recursion, no need to blacklist sub-directories / nested paths - final String packageName = WhiteBlackList.pathToPackageName(pathNormalized); - scanSpec.packageWhiteBlackList.addToBlacklist(packageName); - scanSpec.pathWhiteBlackList.addToBlacklist(pathNormalized + "/"); + // Rejecting always prevents further recursion, no need to reject sub-directories / nested paths + final String packageName = AcceptReject.pathToPackageName(pathNormalized); + scanSpec.packageAcceptReject.addToReject(packageName); + scanSpec.pathAcceptReject.addToReject(pathNormalized + "/"); if (!pathNormalized.contains("*")) { - // Blacklist sub-directories / nested paths - scanSpec.packagePrefixWhiteBlackList.addToBlacklist(packageName + "."); - scanSpec.pathPrefixWhiteBlackList.addToBlacklist(pathNormalized + "/"); + // Reject sub-directories / nested paths + scanSpec.packagePrefixAcceptReject.addToReject(packageName + "."); + scanSpec.pathPrefixAcceptReject.addToReject(pathNormalized + "/"); } } return this; } + /** + * Use {@link #rejectPaths(String...)} instead. + * + * @param paths + * The paths to reject (with '/' as a separator). May include a glob wildcard ({@code '*'}). + * @return this (for method chaining). + * @deprecated Use {@link #rejectPaths(String...)} instead. + */ + @Deprecated + public ClassGraph blacklistPaths(final String... paths) { + return rejectPaths(paths); + } + /** * Scan one or more specific classes, without scanning other classes in the same package unless the package is - * itself whitelisted. + * itself accepted. * *

* N.B. Automatically calls {@link #enableClassInfo()}. * * * @param classNames - * The fully-qualified names of classes to scan (using '.' as a separator). May not include a glob - * wildcard ({@code '*'}). + * The fully-qualified names of classes to scan (using '.' as a separator). To match a class name by + * glob in any package, you must include a package glob too, e.g. {@code "*.*Suffix"}. * @return this (for method chaining). */ - public ClassGraph whitelistClasses(final String... classNames) { + public ClassGraph acceptClasses(final String... classNames) { enableClassInfo(); for (final String className : classNames) { - if (className.contains("*")) { - throw new IllegalArgumentException("Cannot use a glob wildcard here: " + className); - } - final String classNameNormalized = WhiteBlackList.normalizePackageOrClassName(className); - // Whitelist the class itself - scanSpec.classWhiteBlackList.addToWhitelist(classNameNormalized); - scanSpec.classfilePathWhiteBlackList - .addToWhitelist(WhiteBlackList.classNameToClassfilePath(classNameNormalized)); + final String classNameNormalized = AcceptReject.normalizePackageOrClassName(className); + // Accept the class itself + scanSpec.classAcceptReject.addToAccept(classNameNormalized); + scanSpec.classfilePathAcceptReject + .addToAccept(AcceptReject.classNameToClassfilePath(classNameNormalized)); final String packageName = PackageInfo.getParentPackageName(classNameNormalized); // Record the package containing the class, so we can recurse to this point even if the package - // is not itself whitelisted - scanSpec.classPackageWhiteBlackList.addToWhitelist(packageName); - scanSpec.classPackagePathWhiteBlackList - .addToWhitelist(WhiteBlackList.packageNameToPath(packageName) + "/"); + // is not itself accepted + scanSpec.classPackageAcceptReject.addToAccept(packageName); + scanSpec.classPackagePathAcceptReject.addToAccept(AcceptReject.packageNameToPath(packageName) + "/"); } return this; } /** - * Specifically blacklist one or more specific classes, preventing them from being scanned even if they are in a - * whitelisted package. + * Use {@link #acceptClasses(String...)} instead. + * + * @param classNames + * The fully-qualified names of classes to scan (using '.' as a separator). + * @return this (for method chaining). + * @deprecated Use {@link #acceptClasses(String...)} instead. + */ + @Deprecated + public ClassGraph whitelistClasses(final String... classNames) { + return acceptClasses(classNames); + } + + /** + * Specifically reject one or more specific classes, preventing them from being scanned even if they are in a + * accepted package. * *

* N.B. Automatically calls {@link #enableClassInfo()}. * * @param classNames - * The fully-qualified names of classes to blacklist (using '.' as a separator). May not include a - * glob wildcard ({@code '*'}). + * The fully-qualified names of classes to reject (using '.' as a separator). To match a class name + * by glob in any package, you must include a package glob too, e.g. {@code "*.*Suffix"}. * @return this (for method chaining). */ - public ClassGraph blacklistClasses(final String... classNames) { + public ClassGraph rejectClasses(final String... classNames) { enableClassInfo(); for (final String className : classNames) { - if (className.contains("*")) { - throw new IllegalArgumentException("Cannot use a glob wildcard here: " + className); - } - final String classNameNormalized = WhiteBlackList.normalizePackageOrClassName(className); - scanSpec.classWhiteBlackList.addToBlacklist(classNameNormalized); - scanSpec.classfilePathWhiteBlackList - .addToBlacklist(WhiteBlackList.classNameToClassfilePath(classNameNormalized)); + final String classNameNormalized = AcceptReject.normalizePackageOrClassName(className); + scanSpec.classAcceptReject.addToReject(classNameNormalized); + scanSpec.classfilePathAcceptReject + .addToReject(AcceptReject.classNameToClassfilePath(classNameNormalized)); } return this; } /** - * Whitelist one or more jars. This will cause only the whitelisted jars to be scanned. + * Use {@link #rejectClasses(String...)} instead. + * + * @param classNames + * The fully-qualified names of classes to reject (using '.' as a separator). + * @return this (for method chaining). + * @deprecated Use {@link #rejectClasses(String...)} instead. + */ + @Deprecated + public ClassGraph blacklistClasses(final String... classNames) { + return rejectClasses(classNames); + } + + /** + * Accept one or more jars. This will cause only the accepted jars to be scanned. * * @param jarLeafNames * The leafnames of the jars that should be scanned (e.g. {@code "mylib.jar"}). May contain a * wildcard glob ({@code "mylib-*.jar"}). * @return this (for method chaining). */ - public ClassGraph whitelistJars(final String... jarLeafNames) { + public ClassGraph acceptJars(final String... jarLeafNames) { for (final String jarLeafName : jarLeafNames) { final String leafName = JarUtils.leafName(jarLeafName); if (!leafName.equals(jarLeafName)) { - throw new IllegalArgumentException("Can only whitelist jars by leafname: " + jarLeafName); + throw new IllegalArgumentException("Can only accept jars by leafname: " + jarLeafName); } - scanSpec.jarWhiteBlackList.addToWhitelist(leafName); + scanSpec.jarAcceptReject.addToAccept(leafName); } return this; } /** - * Blacklist one or more jars, preventing them from being scanned. + * Use {@link #acceptJars(String...)} instead. + * + * @param jarLeafNames + * The leafnames of the jars that should be scanned (e.g. {@code "mylib.jar"}). May contain a + * wildcard glob ({@code "mylib-*.jar"}). + * @return this (for method chaining). + * @deprecated Use {@link #acceptJars(String...)} instead. + */ + @Deprecated + public ClassGraph whitelistJars(final String... jarLeafNames) { + return acceptJars(jarLeafNames); + } + + /** + * Reject one or more jars, preventing them from being scanned. * * @param jarLeafNames * The leafnames of the jars that should be scanned (e.g. {@code "badlib.jar"}). May contain a * wildcard glob ({@code "badlib-*.jar"}). * @return this (for method chaining). */ - public ClassGraph blacklistJars(final String... jarLeafNames) { + public ClassGraph rejectJars(final String... jarLeafNames) { for (final String jarLeafName : jarLeafNames) { final String leafName = JarUtils.leafName(jarLeafName); if (!leafName.equals(jarLeafName)) { - throw new IllegalArgumentException("Can only blacklist jars by leafname: " + jarLeafName); + throw new IllegalArgumentException("Can only reject jars by leafname: " + jarLeafName); } - scanSpec.jarWhiteBlackList.addToBlacklist(leafName); + scanSpec.jarAcceptReject.addToReject(leafName); } return this; } /** - * Add lib or ext jars to whitelist or blacklist. + * Use {@link #rejectJars(String...)} instead. * - * @param whitelist - * if true, add to whitelist, otherwise add to blacklist. * @param jarLeafNames - * the jar leaf names to whitelist + * The leafnames of the jars that should be scanned (e.g. {@code "badlib.jar"}). May contain a + * wildcard glob ({@code "badlib-*.jar"}). + * @return this (for method chaining). + * @deprecated Use {@link #rejectJars(String...)} instead. */ - private void whitelistOrBlacklistLibOrExtJars(final boolean whitelist, final String... jarLeafNames) { + @Deprecated + public ClassGraph blacklistJars(final String... jarLeafNames) { + return rejectJars(jarLeafNames); + } + + /** + * Add lib or ext jars to accept or reject. + * + * @param accept + * if true, add to accept, otherwise add to reject. + * @param jarLeafNames + * the jar leaf names to accept + */ + private void acceptOrRejectLibOrExtJars(final boolean accept, final String... jarLeafNames) { if (jarLeafNames.length == 0) { - // If no jar leafnames are given, whitelist or blacklist all lib or ext jars + // If no jar leafnames are given, accept or reject all lib or ext jars for (final String libOrExtJar : SystemJarFinder.getJreLibOrExtJars()) { - whitelistOrBlacklistLibOrExtJars(whitelist, JarUtils.leafName(libOrExtJar)); + acceptOrRejectLibOrExtJars(accept, JarUtils.leafName(libOrExtJar)); } } else { for (final String jarLeafName : jarLeafNames) { final String leafName = JarUtils.leafName(jarLeafName); if (!leafName.equals(jarLeafName)) { - throw new IllegalArgumentException("Can only " + (whitelist ? "whitelist" : "blacklist") - + " jars by leafname: " + jarLeafName); + throw new IllegalArgumentException( + "Can only " + (accept ? "accept" : "reject") + " jars by leafname: " + jarLeafName); } if (jarLeafName.contains("*")) { // Compare wildcarded pattern against all jars in lib and ext dirs - final Pattern pattern = WhiteBlackList.globToPattern(jarLeafName); + final Pattern pattern = AcceptReject.globToPattern(jarLeafName, /* simpleGlob = */ true); boolean found = false; for (final String libOrExtJarPath : SystemJarFinder.getJreLibOrExtJars()) { final String libOrExtJarLeafName = JarUtils.leafName(libOrExtJarPath); if (pattern.matcher(libOrExtJarLeafName).matches()) { // Check for "*" in filename to prevent infinite recursion (shouldn't happen) if (!libOrExtJarLeafName.contains("*")) { - whitelistOrBlacklistLibOrExtJars(whitelist, libOrExtJarLeafName); + acceptOrRejectLibOrExtJars(accept, libOrExtJarLeafName); } found = true; } @@ -879,18 +1127,18 @@ private void whitelistOrBlacklistLibOrExtJars(final boolean whitelist, final Str topLevelLog.log("Could not find lib or ext jar matching wildcard: " + jarLeafName); } } else { - // No wildcards, just whitelist or blacklist the named jar, if present + // No wildcards, just accept or reject the named jar, if present boolean found = false; for (final String libOrExtJarPath : SystemJarFinder.getJreLibOrExtJars()) { final String libOrExtJarLeafName = JarUtils.leafName(libOrExtJarPath); if (jarLeafName.equals(libOrExtJarLeafName)) { - if (whitelist) { - scanSpec.libOrExtJarWhiteBlackList.addToWhitelist(jarLeafName); + if (accept) { + scanSpec.libOrExtJarAcceptReject.addToAccept(jarLeafName); } else { - scanSpec.libOrExtJarWhiteBlackList.addToBlacklist(jarLeafName); + scanSpec.libOrExtJarAcceptReject.addToReject(jarLeafName); } if (topLevelLog != null) { - topLevelLog.log((whitelist ? "Whitelisting" : "Blacklisting") + " lib or ext jar: " + topLevelLog.log((accept ? "Accepting" : "Rejecting") + " lib or ext jar: " + libOrExtJarPath); } found = true; @@ -906,105 +1154,210 @@ private void whitelistOrBlacklistLibOrExtJars(final boolean whitelist, final Str } /** - * Whitelist one or more jars in a JRE/JDK "lib/" or "ext/" directory (these directories are not scanned unless + * Accept one or more jars in a JRE/JDK "lib/" or "ext/" directory (these directories are not scanned unless * {@link #enableSystemJarsAndModules()} is called, by association with the JRE/JDK). * * @param jarLeafNames * The leafnames of the lib/ext jar(s) that should be scanned (e.g. {@code "mylib.jar"}). May contain * a wildcard glob ({@code '*'}). Note that if you call this method with no parameters, all JRE/JDK - * "lib/" or "ext/" jars will be whitelisted. + * "lib/" or "ext/" jars will be accepted. + * @return this (for method chaining). + */ + public ClassGraph acceptLibOrExtJars(final String... jarLeafNames) { + acceptOrRejectLibOrExtJars(/* accept = */ true, jarLeafNames); + return this; + } + + /** + * Use {@link #acceptLibOrExtJars(String...)} instead. + * + * @param jarLeafNames + * The leafnames of the lib/ext jar(s) that should be scanned (e.g. {@code "mylib.jar"}). May contain + * a wildcard glob ({@code '*'}). Note that if you call this method with no parameters, all JRE/JDK + * "lib/" or "ext/" jars will be accepted. * @return this (for method chaining). + * @deprecated Use {@link #acceptLibOrExtJars(String...)} instead. */ + @Deprecated public ClassGraph whitelistLibOrExtJars(final String... jarLeafNames) { - whitelistOrBlacklistLibOrExtJars(/* whitelist = */ true, jarLeafNames); + return acceptLibOrExtJars(jarLeafNames); + } + + /** + * Reject one or more jars in a JRE/JDK "lib/" or "ext/" directory, preventing them from being scanned. + * + * @param jarLeafNames + * The leafnames of the lib/ext jar(s) that should not be scanned (e.g. + * {@code "jre/lib/badlib.jar"}). May contain a wildcard glob ({@code '*'}). If you call this method + * with no parameters, all JRE/JDK {@code "lib/"} or {@code "ext/"} jars will be rejected. + * @return this (for method chaining). + */ + public ClassGraph rejectLibOrExtJars(final String... jarLeafNames) { + acceptOrRejectLibOrExtJars(/* accept = */ false, jarLeafNames); return this; } /** - * Blacklist one or more jars in a JRE/JDK "lib/" or "ext/" directory, preventing them from being scanned. + * Use {@link #rejectLibOrExtJars(String...)} instead. * * @param jarLeafNames * The leafnames of the lib/ext jar(s) that should not be scanned (e.g. * {@code "jre/lib/badlib.jar"}). May contain a wildcard glob ({@code '*'}). If you call this method - * with no parameters, all JRE/JDK {@code "lib/"} or {@code "ext/"} jars will be blacklisted. + * with no parameters, all JRE/JDK {@code "lib/"} or {@code "ext/"} jars will be rejected. * @return this (for method chaining). + * @deprecated Use {@link #rejectLibOrExtJars(String...)} instead. */ + @Deprecated public ClassGraph blacklistLibOrExtJars(final String... jarLeafNames) { - whitelistOrBlacklistLibOrExtJars(/* whitelist = */ false, jarLeafNames); + return rejectLibOrExtJars(jarLeafNames); + } + + /** + * Accept one or more modules for scanning. + * + * @param moduleNames + * The names of the modules that should be scanned. May contain a wildcard glob ({@code '*'}). + * @return this (for method chaining). + */ + public ClassGraph acceptModules(final String... moduleNames) { + for (final String moduleName : moduleNames) { + scanSpec.moduleAcceptReject.addToAccept(AcceptReject.normalizePackageOrClassName(moduleName)); + } return this; } /** - * Whitelist one or more modules to scan. + * Use {@link #acceptModules(String...)} instead. * * @param moduleNames * The names of the modules that should be scanned. May contain a wildcard glob ({@code '*'}). * @return this (for method chaining). + * @deprecated Use {@link #acceptModules(String...)} instead. */ + @Deprecated public ClassGraph whitelistModules(final String... moduleNames) { + return acceptModules(moduleNames); + } + + /** + * Reject one or more modules, preventing them from being scanned. + * + * @param moduleNames + * The names of the modules that should not be scanned. May contain a wildcard glob ({@code '*'}). + * @return this (for method chaining). + */ + public ClassGraph rejectModules(final String... moduleNames) { for (final String moduleName : moduleNames) { - scanSpec.moduleWhiteBlackList.addToWhitelist(WhiteBlackList.normalizePackageOrClassName(moduleName)); + scanSpec.moduleAcceptReject.addToReject(AcceptReject.normalizePackageOrClassName(moduleName)); } return this; } /** - * Blacklist one or more modules, preventing them from being scanned. + * Use {@link #rejectModules(String...)} instead. * * @param moduleNames * The names of the modules that should not be scanned. May contain a wildcard glob ({@code '*'}). * @return this (for method chaining). + * @deprecated Use {@link #rejectModules(String...)} instead. */ + @Deprecated public ClassGraph blacklistModules(final String... moduleNames) { - for (final String moduleName : moduleNames) { - scanSpec.moduleWhiteBlackList.addToBlacklist(WhiteBlackList.normalizePackageOrClassName(moduleName)); + return rejectModules(moduleNames); + } + + /** + * Accept classpath elements based on resource paths. Only classpath elements that contain resources with paths + * matching the accept will be scanned. + * + * @param resourcePaths + * The resource paths, any of which must be present in a classpath element for the classpath element + * to be scanned. May contain a wildcard glob ({@code '*'}). + * @return this (for method chaining). + */ + public ClassGraph acceptClasspathElementsContainingResourcePath(final String... resourcePaths) { + for (final String resourcePath : resourcePaths) { + final String resourcePathNormalized = AcceptReject.normalizePath(resourcePath); + scanSpec.classpathElementResourcePathAcceptReject.addToAccept(resourcePathNormalized); } return this; } /** - * Whitelist classpath elements based on resource paths. Only classpath elements that contain resources with - * paths matching the whitelist will be scanned. + * Use {@link #acceptClasspathElementsContainingResourcePath(String...)} instead. * * @param resourcePaths * The resource paths, any of which must be present in a classpath element for the classpath element * to be scanned. May contain a wildcard glob ({@code '*'}). * @return this (for method chaining). + * @deprecated Use {@link #acceptClasspathElementsContainingResourcePath(String...)} instead. */ + @Deprecated public ClassGraph whitelistClasspathElementsContainingResourcePath(final String... resourcePaths) { + return acceptClasspathElementsContainingResourcePath(resourcePaths); + } + + /** + * Reject classpath elements based on resource paths. Classpath elements that contain resources with paths + * matching the reject will not be scanned. + * + * @param resourcePaths + * The resource paths which cause a classpath not to be scanned if any are present in a classpath + * element for the classpath element. May contain a wildcard glob ({@code '*'}). + * @return this (for method chaining). + */ + public ClassGraph rejectClasspathElementsContainingResourcePath(final String... resourcePaths) { for (final String resourcePath : resourcePaths) { - final String resourcePathNormalized = WhiteBlackList.normalizePath(resourcePath); - scanSpec.classpathElementResourcePathWhiteBlackList.addToWhitelist(resourcePathNormalized); + final String resourcePathNormalized = AcceptReject.normalizePath(resourcePath); + scanSpec.classpathElementResourcePathAcceptReject.addToReject(resourcePathNormalized); } return this; } /** - * Blacklist classpath elements based on resource paths. Classpath elements that contain resources with paths - * matching the blacklist will not be scanned. + * Use {@link #rejectClasspathElementsContainingResourcePath(String...)} instead. * * @param resourcePaths * The resource paths which cause a classpath not to be scanned if any are present in a classpath * element for the classpath element. May contain a wildcard glob ({@code '*'}). * @return this (for method chaining). + * @deprecated Use {@link #rejectClasspathElementsContainingResourcePath(String...)} instead. */ + @Deprecated public ClassGraph blacklistClasspathElementsContainingResourcePath(final String... resourcePaths) { - for (final String resourcePath : resourcePaths) { - final String resourcePathNormalized = WhiteBlackList.normalizePath(resourcePath); - scanSpec.classpathElementResourcePathWhiteBlackList.addToBlacklist(resourcePathNormalized); - } - return this; + return rejectClasspathElementsContainingResourcePath(resourcePaths); } /** - * Enable classpath elements to be fetched from remote http/https URLs to local temporary files and scanned. - * This option is disabled by default, as this may present a security vulnerability, since classes from - * downloaded jars can be subsequently loaded using {@link ClassInfo#loadClass}. + * Enable classpath elements to be fetched from remote ("http:"/"https:") URLs (or URLs with custom schemes). + * Equivalent to: + * + *

+ * {@code new ClassGraph().enableURLScheme("http").enableURLScheme("https");} + * + *

+ * Scanning from http(s) URLs is disabled by default, as this may present a security vulnerability, since + * classes from downloaded jars can be subsequently loaded using {@link ClassInfo#loadClass}. * * @return this (for method chaining). */ public ClassGraph enableRemoteJarScanning() { - scanSpec.enableRemoteJarScanning = true; + scanSpec.enableURLScheme("http"); + scanSpec.enableURLScheme("https"); + return this; + } + + /** + * Enable classpath elements to be fetched from {@link URL} connections with the specified URL scheme (also + * works for any custom URL schemes that have been defined, as long as they have more than two characters, in + * order to not conflict with Windows drive letters). + * + * @param scheme + * the URL scheme string, e.g. "resource" for a custom "resource:" URL scheme. + * @return this (for method chaining). + */ + public ClassGraph enableURLScheme(final String scheme) { + scanSpec.enableURLScheme(scheme); return this; } @@ -1023,6 +1376,82 @@ public ClassGraph enableSystemJarsAndModules() { return this; } + // ------------------------------------------------------------------------------------------------------------- + + /** + * The maximum size of an inner (nested) jar that has been deflated (i.e. compressed, not stored) within an + * outer jar, before it has to be spilled to disk rather than stored in a RAM-backed {@link ByteBuffer} when it + * is deflated, in order for the inner jar's entries to be read. (Note that this situation of having to deflate + * a nested jar to RAM or disk in order to read it is rare, because normally adding a jarfile to another jarfile + * will store the inner jar, rather than deflate it, because deflating a jarfile does not usually produce any + * further compression gains. If an inner jar is stored, not deflated, then its zip entries can be read directly + * using ClassGraph's own zipfile central directory parser, which can use file slicing to extract entries + * directly from stored nested jars.) + * + *

+ * This is also the maximum size of a jar downloaded from an {@code http://} or {@code https://} classpath + * {@link URL} to RAM. Once this many bytes have been read from the {@link URL}'s {@link InputStream}, then the + * RAM contents are spilled over to a temporary file on disk, and the rest of the content is downloaded to the + * temporary file. (This is also rare, because normally there are no {@code http://} or {@code https://} + * classpath entries.) + * + *

+ * Default: 64MB (i.e. writing to disk is avoided wherever possible). Setting a lower max RAM size value will + * decrease ClassGraph's memory usage if either of the above rare situations occurs. + * + * @param maxBufferedJarRAMSize + * The max RAM size to use for deflated inner jars or downloaded jars. This is the limit per jar, not + * for the whole classpath. + * @return this (for method chaining). + */ + public ClassGraph setMaxBufferedJarRAMSize(final int maxBufferedJarRAMSize) { + scanSpec.maxBufferedJarRAMSize = maxBufferedJarRAMSize; + return this; + } + + /** + * If true, use a {@link MappedByteBuffer} rather than the {@link FileChannel} API to open files, which may be + * faster for large classpaths consisting of many large jarfiles, but uses up virtual memory space. + * Not available on Java 24+ currently, because of the deprecation of the Unsafe API. + * + * @return this (for method chaining). + */ + public ClassGraph enableMemoryMapping() { + if (VersionFinder.JAVA_MAJOR_VERSION > 23) { + // See FileUtils.java + throw new IllegalArgumentException("enableMemoryMapping() is not supported on Java 24+"); + } + scanSpec.enableMemoryMapping = true; + return this; + } + + /** + * If true, provide all versions of a multi-release resource using their multi-release path prefix, instead of + * just the one the running JVM would select. Implicitly disables {@link #enableClassInfo()} and all features + * depending on it. + * + * @return this (for method chaining). + */ + public ClassGraph enableMultiReleaseVersions() { + scanSpec.enableMultiReleaseVersions = true; + + scanSpec.enableClassInfo = false; + scanSpec.ignoreClassVisibility = false; + scanSpec.enableMethodInfo = false; + scanSpec.ignoreMethodVisibility = false; + scanSpec.enableFieldInfo = false; + scanSpec.ignoreFieldVisibility = false; + scanSpec.enableStaticFinalFieldConstantInitializerValues = false; + scanSpec.enableAnnotationInfo = false; + scanSpec.enableInterClassDependencies = false; + scanSpec.disableRuntimeInvisibleAnnotations = false; + scanSpec.enableExternalClasses = false; + scanSpec.enableSystemJarsAndModules = false; + return this; + } + + // ------------------------------------------------------------------------------------------------------------- + /** * Enables logging by calling {@link #verbose()}, and then sets the logger to "realtime logging mode", where log * entries are written out immediately to stderr, rather than only after the scan has completed. Can help to @@ -1095,11 +1524,11 @@ public void scanAsync(final ExecutorService executorService, final int numParall @Override public void run() { try { - new Scanner(scanSpec, executorService, numParallelTasks, scanResultProcessor, failureHandler, - topLevelLog); - } catch (final InterruptedException e) { - // Interrupted during the Scanner constructor's execution (specifically, by getModuleOrder(), - // which is unlikely to ever actually be interrupted -- but this exception needs to be caught) + // Call scanner, but ignore the returned ScanResult + new Scanner(/* performScan = */ true, scanSpec, executorService, numParallelTasks, + scanResultProcessor, failureHandler, reflectionUtils, topLevelLog).call(); + } catch (final InterruptedException | CancellationException | ExecutionException e) { + // Call failure handler failureHandler.onFailure(e); } } @@ -1107,8 +1536,12 @@ public void run() { } /** - * Asynchronously scans the classpath for matching files, returning a {@code Future}. - * + * Asynchronously scans the classpath for matching files, returning a {@code Future}. You should + * assign the wrapped {@link ScanResult} in a try-with-resources statement, or manually close it when you are + * finished with it. + * + * @param performScan + * If true, performing a scan. If false, only fetching the classpath. * @param executorService * A custom {@link ExecutorService} to use for scheduling worker tasks. * @param numParallelTasks @@ -1117,10 +1550,11 @@ public void run() { * @return a {@code Future}, that when resolved using get() yields a new {@link ScanResult} object * representing the result of the scan. */ - public Future scanAsync(final ExecutorService executorService, final int numParallelTasks) { + private Future scanAsync(final boolean performScan, final ExecutorService executorService, + final int numParallelTasks) { try { - return executorService.submit(new Scanner(scanSpec, executorService, numParallelTasks, - /* scanResultProcessor = */ null, /* failureHandler = */ null, topLevelLog)); + return executorService.submit(new Scanner(performScan, scanSpec, executorService, numParallelTasks, + /* scanResultProcessor = */ null, /* failureHandler = */ null, reflectionUtils, topLevelLog)); } catch (final InterruptedException e) { // Interrupted during the Scanner constructor's execution (specifically, by getModuleOrder(), // which is unlikely to ever actually be interrupted -- but this exception needs to be caught). @@ -1133,9 +1567,27 @@ public ScanResult call() throws Exception { } } + /** + * Asynchronously scans the classpath for matching files, returning a {@code Future}. You should + * assign the wrapped {@link ScanResult} in a try-with-resources statement, or manually close it when you are + * finished with it. + * + * @param executorService + * A custom {@link ExecutorService} to use for scheduling worker tasks. + * @param numParallelTasks + * The number of parallel tasks to break the work into during the most CPU-intensive stage of + * classpath scanning. Ideally the ExecutorService will have at least this many threads available. + * @return a {@code Future}, that when resolved using get() yields a new {@link ScanResult} object + * representing the result of the scan. + */ + public Future scanAsync(final ExecutorService executorService, final int numParallelTasks) { + return scanAsync(/* performScan = */ true, executorService, numParallelTasks); + } + /** * Scans the classpath using the requested {@link ExecutorService} and the requested degree of parallelism, - * blocking until the scan is complete. + * blocking until the scan is complete. You should assign the returned {@link ScanResult} in a + * try-with-resources statement, or manually close it when you are finished with it. * * @param executorService * A custom {@link ExecutorService} to use for scheduling worker tasks. This {@link ExecutorService} @@ -1174,15 +1626,16 @@ public ScanResult scan(final ExecutorService executorService, final int numParal return scanResult; } catch (final InterruptedException | CancellationException e) { - throw ClassGraphException.newClassGraphException("Scan interrupted", e); + throw new ClassGraphException("Scan interrupted", e); } catch (final ExecutionException e) { - throw ClassGraphException.newClassGraphException("Uncaught exception during scan", - InterruptionChecker.getCause(e)); + throw new ClassGraphException("Uncaught exception during scan", InterruptionChecker.getCause(e)); } } /** - * Scans the classpath with the requested number of threads, blocking until the scan is complete. + * Scans the classpath with the requested number of threads, blocking until the scan is complete. You should + * assign the returned {@link ScanResult} in a try-with-resources statement, or manually close it when you are + * finished with it. * * @param numThreads * The number of worker threads to start up. @@ -1197,7 +1650,8 @@ public ScanResult scan(final int numThreads) { } /** - * Scans the classpath, blocking until the scan is complete. + * Scans the classpath, blocking until the scan is complete. You should assign the returned {@link ScanResult} + * in a try-with-resources statement, or manually close it when you are finished with it. * * @return a {@link ScanResult} object representing the result of the scan. * @throws ClassGraphException @@ -1209,6 +1663,34 @@ public ScanResult scan() { // ------------------------------------------------------------------------------------------------------------- + /** + * Get a {@link ScanResult} that can be used for determining the classpath. + * + * @param executorService + * The executor service. + * @return a {@link ScanResult} object representing the result of the scan (can only be used for determining + * classpath). + * @throws ClassGraphException + * if any of the worker threads throws an uncaught exception, or the scan was interrupted. + */ + ScanResult getClasspathScanResult(final AutoCloseableExecutorService executorService) { + try { + final ScanResult scanResult = scanAsync(/* performScan = */ false, executorService, + DEFAULT_NUM_WORKER_THREADS).get(); + + // The resulting scanResult cannot be null, but check for null to keep SpotBugs happy + if (scanResult == null) { + throw new NullPointerException(); + } + return scanResult; + + } catch (final InterruptedException | CancellationException e) { + throw new ClassGraphException("Scan interrupted", e); + } catch (final ExecutionException e) { + throw new ClassGraphException("Uncaught exception during scan", InterruptionChecker.getCause(e)); + } + } + /** * Returns the list of all unique File objects representing directories or zip/jarfiles on the classpath, in * classloader resolution order. Classpath elements that do not exist as a file or directory are not included in @@ -1220,8 +1702,8 @@ public ScanResult scan() { * if any of the worker threads throws an uncaught exception, or the scan was interrupted. */ public List getClasspathFiles() { - scanSpec.performScan = false; - try (ScanResult scanResult = scan()) { + try (AutoCloseableExecutorService executorService = new AutoCloseableExecutorService( + DEFAULT_NUM_WORKER_THREADS); ScanResult scanResult = getClasspathScanResult(executorService)) { return scanResult.getClasspathFiles(); } } @@ -1253,8 +1735,8 @@ public String getClasspath() { * if any of the worker threads throws an uncaught exception, or the scan was interrupted. */ public List getClasspathURIs() { - scanSpec.performScan = false; - try (ScanResult scanResult = scan()) { + try (AutoCloseableExecutorService executorService = new AutoCloseableExecutorService( + DEFAULT_NUM_WORKER_THREADS); ScanResult scanResult = getClasspathScanResult(executorService)) { return scanResult.getClasspathURIs(); } } @@ -1269,8 +1751,8 @@ public List getClasspathURIs() { * if any of the worker threads throws an uncaught exception, or the scan was interrupted. */ public List getClasspathURLs() { - scanSpec.performScan = false; - try (ScanResult scanResult = scan()) { + try (AutoCloseableExecutorService executorService = new AutoCloseableExecutorService( + DEFAULT_NUM_WORKER_THREADS); ScanResult scanResult = getClasspathScanResult(executorService)) { return scanResult.getClasspathURLs(); } } @@ -1283,8 +1765,8 @@ public List getClasspathURLs() { * if any of the worker threads throws an uncaught exception, or the scan was interrupted. */ public List getModules() { - scanSpec.performScan = false; - try (ScanResult scanResult = scan()) { + try (AutoCloseableExecutorService executorService = new AutoCloseableExecutorService( + DEFAULT_NUM_WORKER_THREADS); ScanResult scanResult = getClasspathScanResult(executorService)) { return scanResult.getModules(); } } @@ -1307,6 +1789,7 @@ public List getModules() { * @return The {@link ModulePathInfo}. */ public ModulePathInfo getModulePathInfo() { + scanSpec.modulePathInfo.getRuntimeInfo(reflectionUtils); return scanSpec.modulePathInfo; } } diff --git a/src/main/java/io/github/classgraph/ClassGraphClassLoader.java b/src/main/java/io/github/classgraph/ClassGraphClassLoader.java index 8b3fd533a..7067119e5 100644 --- a/src/main/java/io/github/classgraph/ClassGraphClassLoader.java +++ b/src/main/java/io/github/classgraph/ClassGraphClassLoader.java @@ -31,16 +31,41 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.net.URLClassLoader; +import java.security.ProtectionDomain; +import java.util.Arrays; +import java.util.Collections; import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.JarUtils; +import nonapi.io.github.classgraph.utils.VersionFinder; +import nonapi.io.github.classgraph.utils.VersionFinder.OperatingSystem; /** {@link ClassLoader} for classes found by ClassGraph during scanning. */ -class ClassGraphClassLoader extends ClassLoader { +public class ClassGraphClassLoader extends ClassLoader { /** The scan result. */ private final ScanResult scanResult; + /** Whether or not to initialize loaded classes. */ + private final boolean initializeLoadedClasses; + + /** The ordered set of environment classloaders to try delegating to. */ + private Set environmentClassLoaderDelegationOrder; + + /** Any override classloader(s). */ + private List overrideClassLoaders; + + /** A {@link URLClassLoader} consisting of URLs on the classpath. */ + private final ClassLoader classpathClassLoader; + + /** The ordered set of overridden or added classloaders to try delegating to. */ + private Set addedClassLoaderDelegationOrder; + /** * Constructor. * @@ -49,8 +74,60 @@ class ClassGraphClassLoader extends ClassLoader { */ ClassGraphClassLoader(final ScanResult scanResult) { super(null); - this.scanResult = scanResult; registerAsParallelCapable(); + + this.scanResult = scanResult; + final ScanSpec scanSpec = scanResult.scanSpec; + initializeLoadedClasses = scanSpec.initializeLoadedClasses; + + final boolean classpathOverridden = scanSpec.overrideClasspath != null + && !scanSpec.overrideClasspath.isEmpty(); + final boolean classloadersOverridden = scanSpec.overrideClassLoaders != null + && !scanSpec.overrideClassLoaders.isEmpty(); + final boolean clasloadersAdded = scanSpec.addedClassLoaders != null + && !scanSpec.addedClassLoaders.isEmpty(); + + // Only try environment classloaders if classpath and/or classloaders are not overridden + if (!classpathOverridden && !classloadersOverridden) { + // Try the null classloader first (this will default to the bootstrap class loader) + environmentClassLoaderDelegationOrder = new LinkedHashSet<>(); + environmentClassLoaderDelegationOrder.add(null); + + // Try environment classloaders + final ClassLoader[] envClassLoaderOrder = scanResult.getClassLoaderOrderRespectingParentDelegation(); + if (envClassLoaderOrder != null) { + // Try environment classloaders + environmentClassLoaderDelegationOrder.addAll(Arrays.asList(envClassLoaderOrder)); + } + } + + // Create classloader from URLs on classpath + final List classpathURLs = scanResult.getClasspathURLs(); + classpathClassLoader = classpathURLs.isEmpty() ? null + : new URLClassLoader(classpathURLs.toArray(new URL[0])); + + // If the classloaders were overridden, just use the override classloaders, and then fail if the + // class couldn't be found. + overrideClassLoaders = classloadersOverridden ? scanSpec.overrideClassLoaders : null; + + // If the classpath is overridden, and classloaders are not overridden, try loading class from + // classpath URLs, as the override classloader, then fail if the class couldn't be found. + // + // N.B. Some classpath URLs might be invalid if the ScanResult has been closed (e.g. in the rare + // case that an inner jar had to be extracted to a temporary file on disk). + if (overrideClassLoaders == null && classpathOverridden && classpathClassLoader != null) { + overrideClassLoaders = Collections.singletonList(classpathClassLoader); + } + + // If classloaders were added, try loading through those classloaders + if (clasloadersAdded) { + addedClassLoaderDelegationOrder = new LinkedHashSet<>(); + addedClassLoaderDelegationOrder.addAll(scanSpec.addedClassLoaders); + // Remove duplicates + if (environmentClassLoaderDelegationOrder != null) { + addedClassLoaderDelegationOrder.removeAll(environmentClassLoaderDelegationOrder); + } + } } /* (non-Javadoc) @@ -59,67 +136,181 @@ class ClassGraphClassLoader extends ClassLoader { @Override protected Class findClass(final String className) throws ClassNotFoundException, LinkageError, SecurityException { + // First delegate to outer nested ClassGraphClassLoader, if any (#485) + final ClassGraphClassLoader delegateClassGraphClassLoader = scanResult.classpathFinder + .getDelegateClassGraphClassLoader(); + LinkageError linkageError = null; + if (delegateClassGraphClassLoader != null) { + try { + return Class.forName(className, initializeLoadedClasses, delegateClassGraphClassLoader); + } catch (final ClassNotFoundException e) { + // Ignore + } catch (final LinkageError e) { + linkageError = e; + } + } - // Get ClassInfo for named class - final ClassInfo classInfo = scanResult.getClassInfo(className); + // If overrideClassLoaders is set, only use the override loaders + if (overrideClassLoaders != null) { + for (final ClassLoader overrideClassLoader : overrideClassLoaders) { + try { + return Class.forName(className, initializeLoadedClasses, overrideClassLoader); + } catch (final ClassNotFoundException e) { + // Ignore + } catch (final LinkageError e) { + if (linkageError == null) { + linkageError = e; + } + } + } + } - // Try environment classloaders first, if the classpath was not overridden, or the scan result - // came from deserialization (since in this case, a new URLClassLoader was created for the - // classpath entries that were found in the serialized JSON doc) - boolean triedClassInfoLoader = false; - if (scanResult.envClassLoaderOrder != null) { - // Try environment classloaders - for (final ClassLoader envClassLoader : scanResult.envClassLoaderOrder) { + // Try environment classloader(s) first, since this is the usual default + if (overrideClassLoaders == null && environmentClassLoaderDelegationOrder != null + && !environmentClassLoaderDelegationOrder.isEmpty()) { + for (final ClassLoader envClassLoader : environmentClassLoaderDelegationOrder) { try { - return Class.forName(className, scanResult.scanSpec.initializeLoadedClasses, envClassLoader); - } catch (ReflectiveOperationException | LinkageError e) { + return Class.forName(className, initializeLoadedClasses, envClassLoader); + } catch (final ClassNotFoundException e) { // Ignore + } catch (final LinkageError e) { + if (linkageError == null) { + linkageError = e; + } } - if (classInfo != null && envClassLoader == classInfo.classLoader) { - triedClassInfoLoader = true; + } + } + + // Try getting the ClassInfo for the named class, then the ClassLoader from the ClassInfo. + // This should still be valid if the ScanResult was closed, since ScanResult#close() leaves + // the classNameToClassInfo map intact, but still, this is only attempted if all the above + // efforts failed, to avoid accessing ClassInfo objects after the ScanResult is closed (#399). + ClassLoader classInfoClassLoader = null; + final ClassInfo classInfo = scanResult.classNameToClassInfo == null ? null + : scanResult.classNameToClassInfo.get(className); + if (classInfo != null) { + classInfoClassLoader = classInfo.classLoader; + // Try specific classloader for the classpath element that the classfile was obtained from, + // as long as it wasn't already tried + if (classInfoClassLoader != null && (environmentClassLoaderDelegationOrder == null + || !environmentClassLoaderDelegationOrder.contains(classInfoClassLoader))) { + try { + return Class.forName(className, initializeLoadedClasses, classInfoClassLoader); + } catch (final ClassNotFoundException e) { + // Ignore + } catch (final LinkageError e) { + if (linkageError == null) { + linkageError = e; + } } } + + // If class came from a module, and it was not able to be loaded by the environment classloader, + // then it is probable it was a non-public class, and ClassGraph found it by ignoring class visibility + // when reading the resources in exported packages directly. Force ClassGraph to respect JPMS + // encapsulation rules by refusing to load modular classes that the context/system classloaders + // could not load. (A SecurityException should be thrown above, but this is here for completeness.) + if (classInfo.classpathElement instanceof ClasspathElementModule && !classInfo.isPublic()) { + throw new ClassNotFoundException("Classfile for class " + className + " was found in a module, " + + "but the context and system classloaders could not load the class, probably because " + + "the class is not public."); + } } - // Try specific classloader for the classpath element that the classfile was obtained from - if (!triedClassInfoLoader && classInfo != null && classInfo.classLoader != null) { + // Try loading from classpath URLs + if (overrideClassLoaders == null && classpathClassLoader != null) { try { - return Class.forName(className, scanResult.scanSpec.initializeLoadedClasses, classInfo.classLoader); - } catch (final ReflectiveOperationException | LinkageError e) { + return Class.forName(className, initializeLoadedClasses, classpathClassLoader); + } catch (final ClassNotFoundException e) { // Ignore + } catch (final LinkageError e) { + if (linkageError == null) { + linkageError = e; + } } } - // If class came from a module, and it was not able to be loaded by the environment classloader, - // then it is possible it was a non-public class, and ClassGraph found it by ignoring class visibility - // when reading the resources in exported packages directly. Force ClassGraph to respect JPMS - // encapsulation rules by refusing to load modular classes that the context/system classloaders - // could not load. (A SecurityException should be thrown above, but this is here for completeness.) - if (classInfo != null && classInfo.classpathElement instanceof ClasspathElementModule - && !classInfo.isPublic()) { - throw new ClassNotFoundException("Classfile for class " + className + " was found in a module, " - + "but the context and system classloaders could not load the class, probably because " - + "the class is not public."); + // Try any added classloader(s) + if (addedClassLoaderDelegationOrder != null && !addedClassLoaderDelegationOrder.isEmpty()) { + for (final ClassLoader additionalClassLoader : addedClassLoaderDelegationOrder) { + if (additionalClassLoader != classInfoClassLoader) { + try { + return Class.forName(className, initializeLoadedClasses, additionalClassLoader); + } catch (final ClassNotFoundException e) { + // Ignore + } catch (final LinkageError e) { + if (linkageError == null) { + linkageError = e; + } + } + } + } } - // Try obtaining the classfile as a resource, and defining the class from the resource content + // As a last-ditch attempt, if the above efforts all failed, try obtaining the classfile as a + // resource, and define the class from the resource content. This should be performed after + // environment classloading is attempted, so that classes are not loaded by a mix of environment + // classloaders and direct manual classloading, otherwise class compatibility issues can arise. + // The ScanResult should only be accessed (to fetch resources) as a last resort, so that wherever + // possible, linked classes can be loaded after the ScanResult is closed. Otherwise if you load + // classes before a ScanResult is closed, then you close the ScanResult, then you try to access + // fields of the ScanResult that have a type that has not yet been loaded, this can trigger an + // exception that the ScanResult was accessed after it was closed (#399). final ResourceList classfileResources = scanResult .getResourcesWithPath(JarUtils.classNameToClassfilePath(className)); if (classfileResources != null) { for (final Resource resource : classfileResources) { // Iterate through resources (only loading of first resource in the list will be attempted) - try { - // Load the content of the resource, and define a class from it - final byte[] resourceContent = resource.load(); - return defineClass(className, resourceContent, 0, resourceContent.length); + // Load the content of the resource, and define a class from it + try (Resource resourceToClose = resource) { + // TODO: is there any need to try java.lang.invoke.MethodHandles.Lookup.defineClass + // via reflection (it's implemented in JDK 9), if the following fails? + // See: https://bugs.openjdk.java.net/browse/JDK-8202999 + return defineClass(className, resourceToClose.read(), (ProtectionDomain) null); } catch (final IOException e) { throw new ClassNotFoundException("Could not load classfile for class " + className + " : " + e); - } finally { - resource.close(); + } catch (final LinkageError e) { + if (linkageError == null) { + linkageError = e; + } + } + } + } + + if (linkageError != null) { + if (VersionFinder.OS == OperatingSystem.Windows) { + // LinkageError indicates that a classfile was found, but the class couldn't be loaded. + // Hackily detect the situation where there are two classfiles with the same case insensitive name + // on Windows filesystems (#494). + final String msg = linkageError.getMessage(); + if (msg != null) { + final String wrongName = "(wrong name: "; + final int wrongNameIdx = msg.indexOf(wrongName); + if (wrongNameIdx > -1) { + final String theWrongName = msg.substring(wrongNameIdx + wrongName.length(), + msg.length() - 1); + if (theWrongName.replace('/', '.').equalsIgnoreCase(className)) { + throw new LinkageError("You appear to have two classfiles with the same " + + "case-insensitive name in the same directory on a case-insensitive " + + "filesystem -- this is not allowed on Windows, and therefore your " + + "code is not portable. Class name: " + className, linkageError); + } + } } } + throw linkageError; } - throw new ClassNotFoundException("Could not load classfile for class " + className); + + throw new ClassNotFoundException("Could not find or load classfile for class " + className); + } + + /** + * Get classpath URLs. + * + * @return The classpath URLs in the {@link ScanResult} handled by this {@link ClassLoader}. + */ + public URL[] getURLs() { + return scanResult.getClasspathURLs().toArray(new URL[0]); } /* (non-Javadoc) @@ -127,6 +318,30 @@ protected Class findClass(final String className) */ @Override public URL getResource(final String path) { + // This order should match the order in findClass(String) + + // Try loading resource from environment classloader(s) + if (!environmentClassLoaderDelegationOrder.isEmpty()) { + for (final ClassLoader envClassLoader : environmentClassLoaderDelegationOrder) { + final URL resource = envClassLoader.getResource(path); + if (resource != null) { + return resource; + } + } + } + + // Try loading resource from overridden or added classloader(s) + if (!addedClassLoaderDelegationOrder.isEmpty()) { + for (final ClassLoader additionalClassLoader : addedClassLoaderDelegationOrder) { + final URL resource = additionalClassLoader.getResource(path); + if (resource != null) { + return resource; + } + } + } + + // If the above attempts fail, try retrieving resource from ScanResult. + // This will throw an exception if ScanResult has already been closed (#399). final ResourceList resourceList = scanResult.getResourcesWithPath(path); if (resourceList == null || resourceList.isEmpty()) { return super.getResource(path); @@ -140,9 +355,33 @@ public URL getResource(final String path) { */ @Override public Enumeration getResources(final String path) throws IOException { + // This order should match the order in findClass(String) + + // Try loading resources from environment classloader(s) + if (!environmentClassLoaderDelegationOrder.isEmpty()) { + for (final ClassLoader envClassLoader : environmentClassLoaderDelegationOrder) { + final Enumeration resources = envClassLoader.getResources(path); + if (resources != null && resources.hasMoreElements()) { + return resources; + } + } + } + + // Try loading resources from overridden or added classloader(s) + if (!addedClassLoaderDelegationOrder.isEmpty()) { + for (final ClassLoader additionalClassLoader : addedClassLoaderDelegationOrder) { + final Enumeration resources = additionalClassLoader.getResources(path); + if (resources != null && resources.hasMoreElements()) { + return resources; + } + } + } + + // If the above attempts fail, try retrieving resource from ScanResult. + // This will throw an exception if ScanResult has already been closed (#399). final ResourceList resourceList = scanResult.getResourcesWithPath(path); if (resourceList == null || resourceList.isEmpty()) { - return super.getResources(path); + return Collections.emptyEnumeration(); } else { return new Enumeration() { /** The idx. */ @@ -166,6 +405,30 @@ public URL nextElement() { */ @Override public InputStream getResourceAsStream(final String path) { + // This order should match the order in findClass(String) + + // Try opening resource from environment classloader(s) + if (!environmentClassLoaderDelegationOrder.isEmpty()) { + for (final ClassLoader envClassLoader : environmentClassLoaderDelegationOrder) { + final InputStream inputStream = envClassLoader.getResourceAsStream(path); + if (inputStream != null) { + return inputStream; + } + } + } + + // Try opening resource from overridden or added classloader(s) + if (!addedClassLoaderDelegationOrder.isEmpty()) { + for (final ClassLoader additionalClassLoader : addedClassLoaderDelegationOrder) { + final InputStream inputStream = additionalClassLoader.getResourceAsStream(path); + if (inputStream != null) { + return inputStream; + } + } + } + + // If the above attempts fail, try opening resource from ScanResult. + // This will throw an exception if ScanResult has already been closed (#399). final ResourceList resourceList = scanResult.getResourcesWithPath(path); if (resourceList == null || resourceList.isEmpty()) { return super.getResourceAsStream(path); diff --git a/src/main/java/io/github/classgraph/ClassGraphException.java b/src/main/java/io/github/classgraph/ClassGraphException.java index 5b7ef26e8..d88ea9732 100644 --- a/src/main/java/io/github/classgraph/ClassGraphException.java +++ b/src/main/java/io/github/classgraph/ClassGraphException.java @@ -46,7 +46,7 @@ public class ClassGraphException extends IllegalArgumentException { * @param message * the message */ - private ClassGraphException(final String message) { + ClassGraphException(final String message) { super(message); } @@ -58,32 +58,7 @@ private ClassGraphException(final String message) { * @param cause * the cause */ - private ClassGraphException(final String message, final Throwable cause) { + ClassGraphException(final String message, final Throwable cause) { super(message, cause); } - - /** - * Static factory method to stop IDEs from auto-completing ClassGraphException after "new ClassGraph". - * - * @param message - * the message - * @return the ClassGraphException - */ - public static ClassGraphException newClassGraphException(final String message) { - return new ClassGraphException(message); - } - - /** - * Static factory method to stop IDEs from auto-completing ClassGraphException after "new ClassGraph". - * - * @param message - * the message - * @param cause - * the cause - * @return the ClassGraphException - */ - public static ClassGraphException newClassGraphException(final String message, final Throwable cause) - throws ClassGraphException { - return new ClassGraphException(message, cause); - } } diff --git a/src/main/java/io/github/classgraph/ClassInfo.java b/src/main/java/io/github/classgraph/ClassInfo.java index 824395541..dfc691a2d 100644 --- a/src/main/java/io/github/classgraph/ClassInfo.java +++ b/src/main/java/io/github/classgraph/ClassInfo.java @@ -29,10 +29,12 @@ package io.github.classgraph; import java.io.File; +import java.lang.annotation.Annotation; import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.reflect.Modifier; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; @@ -47,25 +49,30 @@ import java.util.Map.Entry; import java.util.Set; -import nonapi.io.github.classgraph.ScanSpec; +import io.github.classgraph.Classfile.ClassContainment; +import io.github.classgraph.Classfile.ClassTypeAnnotationDecorator; +import io.github.classgraph.FieldInfoList.FieldInfoFilter; import nonapi.io.github.classgraph.json.Id; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.types.ParseException; +import nonapi.io.github.classgraph.types.Parser; import nonapi.io.github.classgraph.types.TypeUtils; import nonapi.io.github.classgraph.types.TypeUtils.ModifierType; +import nonapi.io.github.classgraph.utils.Assert; +import nonapi.io.github.classgraph.utils.LogNode; /** Holds metadata about a class encountered during a scan. */ public class ClassInfo extends ScanResultObject implements Comparable, HasName { /** The name of the class. */ - private @Id String name; + @Id + protected String name; /** Class modifier flags, e.g. Modifier.PUBLIC */ private int modifiers; - /** True if the classfile indicated this is an interface (or an annotation, which is an interface). */ - private boolean isInterface; - - /** True if the classfile indicated this is an annotation. */ - private boolean isAnnotation; + /** True if the class is a record. */ + private boolean isRecord; /** * This annotation has the {@link Inherited} meta-annotation, which means that any class that this annotation is @@ -73,36 +80,48 @@ public class ClassInfo extends ScanResultObject implements Comparable */ boolean isInherited; + /** The minor version of the classfile format for this class' classfile. */ + private int classfileMinorVersion; + + /** The major version of the classfile format for this class' classfile. */ + private int classfileMajorVersion; + /** The class type signature string. */ - private String typeSignatureStr; + protected String typeSignatureStr; /** The class type signature, parsed. */ private transient ClassTypeSignature typeSignature; + /** The synthetic class type descriptor. */ + private transient ClassTypeSignature typeDescriptor; + + /** The name of the source file this class has been compiled from */ + private String sourceFile; + /** The fully-qualified defining method name, for anonymous inner classes. */ private String fullyQualifiedDefiningMethodName; /** * If true, this class is only being referenced by another class' classfile as a superclass / implemented - * interface / annotation, but this class is not itself a whitelisted (non-blacklisted) class, or in a - * whitelisted (non-blacklisted) package. + * interface / annotation, but this class is not itself an accepted (non-rejected) class, or in a accepted + * (non-rejected) package. * * If false, this classfile was matched during scanning (i.e. its classfile contents read), i.e. this class is a - * whitelisted (and non-blacklisted) class in a whitelisted (and non-blacklisted) package. + * accepted (and non-rejected) class in an accepted (and non-rejected) package. */ - private boolean isExternalClass = true; + protected boolean isExternalClass = true; /** * Set to true when the class is actually scanned (as opposed to just referenced as a superclass, interface or * annotation of a scanned class). */ - private boolean isScannedClass; + protected boolean isScannedClass; /** The classpath element that this class was found within. */ transient ClasspathElement classpathElement; /** The {@link Resource} for the classfile of this class. */ - private transient Resource classfileResource; + protected transient Resource classfileResource; /** The classloader this class was obtained from. */ transient ClassLoader classLoader; @@ -125,13 +144,19 @@ public class ClassInfo extends ScanResultObject implements Comparable /** For annotations, the default values of parameters. */ AnnotationParameterValueList annotationDefaultParamValues; + /** The type annotation decorators for the {@link ClassTypeSignature} instance. */ + transient List typeAnnotationDecorators; + /** * Names of classes referenced by this class in class refs and type signatures in the constant pool of the * classfile. */ private Set referencedClassNames; - /** A list of ClassInfo objects for classes referenced by this class. */ + /** + * A list of ClassInfo objects for classes referenced by this class. Derived from {@link #referencedClassNames} + * when the relevant {@link ClassInfo} objects are created. + */ private ClassInfoList referencedClasses; /** @@ -149,6 +174,17 @@ public class ClassInfo extends ScanResultObject implements Comparable */ private transient List overrideOrder; + /** + * The override order for a class' methods (base class, followed by superclasses, followed by interfaces). + */ + private transient List methodOverrideOrder; + + /** The annotations, once they are loaded */ + private ClassInfoList annotationsRef; + + /** The annotation infos, once they are loaded */ + private AnnotationInfoList annotationInfoRef; + // ------------------------------------------------------------------------------------------------------------- /** The modifier bit for annotations. */ @@ -176,7 +212,8 @@ public class ClassInfo extends ScanResultObject implements Comparable * @param classfileResource * the classfile resource */ - private ClassInfo(final String name, final int classModifiers, final Resource classfileResource) { + @SuppressWarnings("null") + protected ClassInfo(final String name, final int classModifiers, final Resource classfileResource) { super(); this.name = name; if (name.endsWith(";")) { @@ -184,12 +221,6 @@ private ClassInfo(final String name, final int classModifiers, final Resource cl throw new IllegalArgumentException("Bad class name"); } setModifiers(classModifiers); - if ((classModifiers & ANNOTATION_CLASS_MODIFIER) != 0) { - isAnnotation = true; - } - if ((classModifiers & Modifier.INTERFACE) != 0) { - isInterface = true; - } this.classfileResource = classfileResource; this.relatedClasses = new EnumMap<>(RelType.class); } @@ -255,6 +286,12 @@ enum RelType { */ CLASSES_WITH_METHOD_ANNOTATION, + /** + * Classes that have one or more non-private (inherited) methods annotated with this annotation, if this is + * an annotation. + */ + CLASSES_WITH_NONPRIVATE_METHOD_ANNOTATION, + /** Annotations on one or more parameters of methods of this class. */ METHOD_PARAMETER_ANNOTATIONS, @@ -264,6 +301,12 @@ enum RelType { */ CLASSES_WITH_METHOD_PARAMETER_ANNOTATION, + /** + * Classes that have one or more non-private (inherited) methods that have one or more parameters annotated + * with this annotation, if this is an annotation. + */ + CLASSES_WITH_NONPRIVATE_METHOD_PARAMETER_ANNOTATION, + // Field annotations: /** Annotations on one or more fields of this class. */ @@ -273,6 +316,12 @@ enum RelType { * Classes that have one or more fields annotated with this annotation, if this is an annotation. */ CLASSES_WITH_FIELD_ANNOTATION, + + /** + * Classes that have one or more non-private (inherited) fields annotated with this annotation, if this is + * an annotation. + */ + CLASSES_WITH_NONPRIVATE_FIELD_ANNOTATION, } /** @@ -300,23 +349,82 @@ boolean addRelatedClass(final RelType relType, final ClassInfo classInfo) { * * @param className * the class name - * @param classModifiers - * the class modifiers * @param classNameToClassInfo * the map from class name to class info - * @return the or create class info + * @return the {@link ClassInfo} object. */ - static ClassInfo getOrCreateClassInfo(final String className, final int classModifiers, + static ClassInfo getOrCreateClassInfo(final String className, final Map classNameToClassInfo) { + // Look for array class names + int numArrayDims = 0; + String baseClassName = className; + while (baseClassName.endsWith("[]")) { + numArrayDims++; + baseClassName = baseClassName.substring(0, baseClassName.length() - 2); + } + // Be resilient to the use of class descriptors rather than class names (should not be needed) + while (baseClassName.startsWith("[")) { + numArrayDims++; + baseClassName = baseClassName.substring(1); + } + if (baseClassName.endsWith(";")) { + baseClassName = baseClassName.substring(baseClassName.length() - 1); + } + baseClassName = baseClassName.replace('/', '.'); + ClassInfo classInfo = classNameToClassInfo.get(className); if (classInfo == null) { - classNameToClassInfo.put(className, - classInfo = new ClassInfo(className, classModifiers, /* classfileResource = */ null)); + if (numArrayDims == 0) { + classInfo = new ClassInfo(baseClassName, /* classModifiers = */ 0, /* classfileResource = */ null); + } else { + final StringBuilder arrayTypeSigStrBuf = new StringBuilder(); + for (int i = 0; i < numArrayDims; i++) { + arrayTypeSigStrBuf.append('['); + } + TypeSignature elementTypeSignature; + final char baseTypeChar = BaseTypeSignature.getTypeChar(baseClassName); + if (baseTypeChar != '\0') { + // Element type is a base (primitive) type + arrayTypeSigStrBuf.append(baseTypeChar); + elementTypeSignature = new BaseTypeSignature(baseTypeChar); + } else { + // Element type is not a base (primitive) type -- create a type signature for element type + final String eltTypeSigStr = "L" + baseClassName.replace('.', '/') + ";"; + arrayTypeSigStrBuf.append(eltTypeSigStr); + try { + elementTypeSignature = ClassRefTypeSignature.parse(new Parser(eltTypeSigStr), + // No type variables to resolve for generic types + /* definingClassName = */ null); + if (elementTypeSignature == null) { + throw new IllegalArgumentException( + "Could not form array base type signature for class " + baseClassName); + } + } catch (final ParseException e) { + throw new IllegalArgumentException( + "Could not form array base type signature for class " + baseClassName); + } + } + classInfo = new ArrayClassInfo( + new ArrayTypeSignature(elementTypeSignature, numArrayDims, arrayTypeSigStrBuf.toString())); + } + classNameToClassInfo.put(className, classInfo); } - classInfo.setModifiers(classModifiers); return classInfo; } + /** + * Set classfile version. + * + * @param minorVersion + * the minor version of the classfile format for this class' classfile. + * @param majorVersion + * the major version of the classfile format for this class' classfile. + */ + void setClassfileVersion(final int minorVersion, final int majorVersion) { + this.classfileMinorVersion = minorVersion; + this.classfileMajorVersion = majorVersion; + } + /** * Set class modifiers. * @@ -325,12 +433,6 @@ static ClassInfo getOrCreateClassInfo(final String className, final int classMod */ void setModifiers(final int modifiers) { this.modifiers |= modifiers; - if ((modifiers & ANNOTATION_CLASS_MODIFIER) != 0) { - this.isAnnotation = true; - } - if ((modifiers & Modifier.INTERFACE) != 0) { - this.isInterface = true; - } } /** @@ -340,17 +442,56 @@ void setModifiers(final int modifiers) { * true if this is an interface */ void setIsInterface(final boolean isInterface) { - this.isInterface |= isInterface; + if (isInterface) { + this.modifiers |= Modifier.INTERFACE; + } } /** - * Set isInterface status. + * Set isAnnotation status. * * @param isAnnotation * true if this is an annotation */ void setIsAnnotation(final boolean isAnnotation) { - this.isAnnotation |= isAnnotation; + if (isAnnotation) { + this.modifiers |= ANNOTATION_CLASS_MODIFIER; + } + } + + /** + * Set isRecord status. + * + * @param isRecord + * true if this is a record + */ + void setIsRecord(final boolean isRecord) { + if (isRecord) { + this.isRecord = isRecord; + } + } + + /** + * Set source file. + * + * @param sourceFile + * the source file + */ + void setSourceFile(final String sourceFile) { + this.sourceFile = sourceFile; + } + + /** + * Add {@link ClassTypeAnnotationDecorator} instances. + * + * @param classTypeAnnotationDecorators + * {@link ClassTypeAnnotationDecorator} instances. + */ + void addTypeDecorators(final List classTypeAnnotationDecorators) { + if (typeAnnotationDecorators == null) { + typeAnnotationDecorators = new ArrayList<>(); + } + typeAnnotationDecorators.addAll(classTypeAnnotationDecorators); } // ------------------------------------------------------------------------------------------------------------- @@ -365,8 +506,7 @@ void setIsAnnotation(final boolean isAnnotation) { */ void addSuperclass(final String superclassName, final Map classNameToClassInfo) { if (superclassName != null && !superclassName.equals("java.lang.Object")) { - final ClassInfo superclassClassInfo = getOrCreateClassInfo(superclassName, /* classModifiers = */ 0, - classNameToClassInfo); + final ClassInfo superclassClassInfo = getOrCreateClassInfo(superclassName, classNameToClassInfo); this.addRelatedClass(RelType.SUPERCLASSES, superclassClassInfo); superclassClassInfo.addRelatedClass(RelType.SUBCLASSES, this); } @@ -381,10 +521,8 @@ void addSuperclass(final String superclassName, final Map cla * the map from class name to class info */ void addImplementedInterface(final String interfaceName, final Map classNameToClassInfo) { - final ClassInfo interfaceClassInfo = getOrCreateClassInfo(interfaceName, - /* classModifiers = */ Modifier.INTERFACE, classNameToClassInfo); - interfaceClassInfo.isInterface = true; - interfaceClassInfo.modifiers |= Modifier.INTERFACE; + final ClassInfo interfaceClassInfo = getOrCreateClassInfo(interfaceName, classNameToClassInfo); + interfaceClassInfo.setIsInterface(true); this.addRelatedClass(RelType.IMPLEMENTED_INTERFACES, interfaceClassInfo); interfaceClassInfo.addRelatedClass(RelType.CLASSES_IMPLEMENTING, this); } @@ -397,15 +535,14 @@ void addImplementedInterface(final String interfaceName, final Map> classContainmentEntries, + static void addClassContainment(final List classContainmentEntries, final Map classNameToClassInfo) { - for (final SimpleEntry ent : classContainmentEntries) { - final String innerClassName = ent.getKey(); - final ClassInfo innerClassInfo = ClassInfo.getOrCreateClassInfo(innerClassName, - /* classModifiers = */ 0, classNameToClassInfo); - final String outerClassName = ent.getValue(); - final ClassInfo outerClassInfo = ClassInfo.getOrCreateClassInfo(outerClassName, - /* classModifiers = */ 0, classNameToClassInfo); + for (final ClassContainment classContainment : classContainmentEntries) { + final ClassInfo innerClassInfo = ClassInfo.getOrCreateClassInfo(classContainment.innerClassName, + classNameToClassInfo); + innerClassInfo.setModifiers(classContainment.innerClassModifierBits); + final ClassInfo outerClassInfo = ClassInfo.getOrCreateClassInfo(classContainment.outerClassName, + classNameToClassInfo); innerClassInfo.addRelatedClass(RelType.CONTAINED_WITHIN_OUTER_CLASS, outerClassInfo); outerClassInfo.addRelatedClass(RelType.CONTAINS_INNER_CLASS, innerClassInfo); } @@ -432,7 +569,8 @@ void addFullyQualifiedDefiningMethodName(final String fullyQualifiedDefiningMeth void addClassAnnotation(final AnnotationInfo classAnnotationInfo, final Map classNameToClassInfo) { final ClassInfo annotationClassInfo = getOrCreateClassInfo(classAnnotationInfo.getName(), - ANNOTATION_CLASS_MODIFIER, classNameToClassInfo); + classNameToClassInfo); + annotationClassInfo.setModifiers(ANNOTATION_CLASS_MODIFIER); if (this.annotationInfo == null) { this.annotationInfo = new AnnotationInfoList(2); } @@ -454,21 +592,29 @@ void addClassAnnotation(final AnnotationInfo classAnnotationInfo, * the annotation info list * @param isField * the is field + * @param modifiers + * the field or method modifiers * @param classNameToClassInfo * the map from class name to class info */ private void addFieldOrMethodAnnotationInfo(final AnnotationInfoList annotationInfoList, final boolean isField, - final Map classNameToClassInfo) { + final int modifiers, final Map classNameToClassInfo) { if (annotationInfoList != null) { for (final AnnotationInfo fieldAnnotationInfo : annotationInfoList) { final ClassInfo annotationClassInfo = getOrCreateClassInfo(fieldAnnotationInfo.getName(), - ANNOTATION_CLASS_MODIFIER, classNameToClassInfo); + classNameToClassInfo); + annotationClassInfo.setModifiers(ANNOTATION_CLASS_MODIFIER); // Mark this class as having a field or method with this annotation this.addRelatedClass(isField ? RelType.FIELD_ANNOTATIONS : RelType.METHOD_ANNOTATIONS, annotationClassInfo); annotationClassInfo.addRelatedClass( isField ? RelType.CLASSES_WITH_FIELD_ANNOTATION : RelType.CLASSES_WITH_METHOD_ANNOTATION, this); + // For non-private methods/fields, also add to nonprivate (inherited) mapping + if (!Modifier.isPrivate(modifiers)) { + annotationClassInfo.addRelatedClass(isField ? RelType.CLASSES_WITH_NONPRIVATE_FIELD_ANNOTATION + : RelType.CLASSES_WITH_NONPRIVATE_METHOD_ANNOTATION, this); + } } } } @@ -483,7 +629,9 @@ private void addFieldOrMethodAnnotationInfo(final AnnotationInfoList annotationI */ void addFieldInfo(final FieldInfoList fieldInfoList, final Map classNameToClassInfo) { for (final FieldInfo fi : fieldInfoList) { - addFieldOrMethodAnnotationInfo(fi.annotationInfo, /* isField = */ true, classNameToClassInfo); + // Index field annotations + addFieldOrMethodAnnotationInfo(fi.annotationInfo, /* isField = */ true, fi.getModifiers(), + classNameToClassInfo); } if (this.fieldInfo == null) { this.fieldInfo = fieldInfoList; @@ -502,21 +650,27 @@ void addFieldInfo(final FieldInfoList fieldInfoList, final Map classNameToClassInfo) { for (final MethodInfo mi : methodInfoList) { - addFieldOrMethodAnnotationInfo(mi.annotationInfo, /* isField = */ false, classNameToClassInfo); + // Index method annotations + addFieldOrMethodAnnotationInfo(mi.annotationInfo, /* isField = */ false, mi.getModifiers(), + classNameToClassInfo); - // Currently it is not possible to find methods by method parameter annotation + // Index method parameter annotations if (mi.parameterAnnotationInfo != null) { for (int i = 0; i < mi.parameterAnnotationInfo.length; i++) { final AnnotationInfo[] paramAnnotationInfoArr = mi.parameterAnnotationInfo[i]; if (paramAnnotationInfoArr != null) { - for (int j = 0; j < paramAnnotationInfoArr.length; j++) { - final AnnotationInfo methodParamAnnotationInfo = paramAnnotationInfoArr[j]; + for (final AnnotationInfo methodParamAnnotationInfo : paramAnnotationInfoArr) { final ClassInfo annotationClassInfo = getOrCreateClassInfo( - methodParamAnnotationInfo.getName(), ANNOTATION_CLASS_MODIFIER, - classNameToClassInfo); + methodParamAnnotationInfo.getName(), classNameToClassInfo); + annotationClassInfo.setModifiers(ANNOTATION_CLASS_MODIFIER); + this.addRelatedClass(RelType.METHOD_PARAMETER_ANNOTATIONS, annotationClassInfo); annotationClassInfo.addRelatedClass(RelType.CLASSES_WITH_METHOD_PARAMETER_ANNOTATION, this); - this.addRelatedClass(RelType.METHOD_PARAMETER_ANNOTATIONS, annotationClassInfo); + // For non-private methods/fields, also add to nonprivate (inherited) mapping + if (!Modifier.isPrivate(mi.getModifiers())) { + annotationClassInfo.addRelatedClass( + RelType.CLASSES_WITH_NONPRIVATE_METHOD_PARAMETER_ANNOTATION, this); + } } } } @@ -547,6 +701,7 @@ void setTypeSignature(final String typeSignatureStr) { * the default param names and values, if this is an annotation */ void addAnnotationParamDefaultValues(final AnnotationParameterValueList paramNamesAndValues) { + setIsAnnotation(true); if (this.annotationDefaultParamValues == null) { this.annotationDefaultParamValues = paramNamesAndValues; } else { @@ -592,12 +747,18 @@ static ClassInfo addScannedClass(final String className, final int classModifier + " should not have been encountered more than once due to classpath masking --" + " please report this bug at: https://github.com/classgraph/classgraph/issues"); } + + // Set the classfileResource for the placeholder class + classInfo.classfileResource = classfileResource; + + // Add any additional modifier bits + classInfo.modifiers |= classModifiers; } // Mark the class as scanned classInfo.isScannedClass = true; - // Mark the class as non-external if it is a whitelisted class + // Mark the class as non-external if it is an accepted class classInfo.isExternalClass = isExternalClass; // Remember which classpath element (zipfile / classpath root directory / module) the class was found in @@ -626,6 +787,10 @@ private enum ClassType { ANNOTATION, /** An interface or annotation (used since you can actually implement an annotation). */ INTERFACE_OR_ANNOTATION, + /** An enum. */ + ENUM, + /** A record type. */ + RECORD } /** @@ -635,21 +800,23 @@ private enum ClassType { * the classes * @param scanSpec * the scan spec - * @param strictWhitelist - * If true, exclude class if it is is external, blacklisted, or a system class. + * @param strictAccept + * If true, exclude class if it is external, if external classes are not enabled * @param classTypes * the class types * @return the filtered classes. */ private static Set filterClassInfo(final Collection classes, final ScanSpec scanSpec, - final boolean strictWhitelist, final ClassType... classTypes) { + final boolean strictAccept, final ClassType... classTypes) { if (classes == null) { - return Collections. emptySet(); + return Collections.emptySet(); } boolean includeAllTypes = classTypes.length == 0; boolean includeStandardClasses = false; boolean includeImplementedInterfaces = false; boolean includeAnnotations = false; + boolean includeEnums = false; + boolean includeRecords = false; for (final ClassType classType : classTypes) { switch (classType) { case ALL: @@ -667,6 +834,12 @@ private static Set filterClassInfo(final Collection classe case INTERFACE_OR_ANNOTATION: includeImplementedInterfaces = includeAnnotations = true; break; + case ENUM: + includeEnums = true; + break; + case RECORD: + includeRecords = true; + break; default: throw new IllegalArgumentException("Unknown ClassType: " + classType); } @@ -677,17 +850,18 @@ private static Set filterClassInfo(final Collection classe final Set classInfoSetFiltered = new LinkedHashSet<>(classes.size()); for (final ClassInfo classInfo : classes) { // Check class type against requested type(s) - if ((includeAllTypes // - || includeStandardClasses && classInfo.isStandardClass() - || includeImplementedInterfaces && classInfo.isImplementedInterface() - || includeAnnotations && classInfo.isAnnotation()) // - // Always check blacklist - && !scanSpec.classOrPackageIsBlacklisted(classInfo.name) // - // Always return whitelisted classes, or external classes if enableExternalClasses is true - && (!classInfo.isExternalClass || scanSpec.enableExternalClasses - // Return external (non-whitelisted) classes if viewing class hierarchy "upwards" - || !strictWhitelist)) { - // Class passed strict whitelist criteria + final boolean includeType = includeAllTypes // + || includeStandardClasses && classInfo.isStandardClass() // + || includeImplementedInterfaces && classInfo.isImplementedInterface() // + || includeAnnotations && classInfo.isAnnotation() // + || includeEnums && classInfo.isEnum() // + || includeRecords && classInfo.isRecord(); + // Return external (non-accepted) classes if viewing class hierarchy "upwards" + final boolean acceptClass = !classInfo.isExternalClass || scanSpec.enableExternalClasses + || !strictAccept; + // If class is of correct type, and class is accepted, and class/package are not explicitly rejected + if (includeType && acceptClass && !scanSpec.classOrPackageIsRejected(classInfo.name)) { + // Class passed accept criteria classInfoSetFiltered.add(classInfo); } } @@ -726,14 +900,14 @@ private ReachableAndDirectlyRelatedClasses(final Set reachableClasses * directly related. * * @param relType - * the rel type - * @param strictWhitelist - * the strict whitelist + * the relationship type + * @param strictAccept + * If true, exclude class if it is external, if external classes are not enabled * @param classTypes - * the class types + * the class types to accept * @return the reachable and directly related classes */ - private ReachableAndDirectlyRelatedClasses filterClassInfo(final RelType relType, final boolean strictWhitelist, + private ReachableAndDirectlyRelatedClasses filterClassInfo(final RelType relType, final boolean strictAccept, final ClassType... classTypes) { Set directlyRelatedClasses = this.relatedClasses.get(relType); if (directlyRelatedClasses == null) { @@ -748,15 +922,18 @@ private ReachableAndDirectlyRelatedClasses filterClassInfo(final RelType relType // For method and field annotations, need to change the RelType when finding meta-annotations for (final ClassInfo annotation : directlyRelatedClasses) { reachableClasses.addAll( - annotation.filterClassInfo(RelType.CLASS_ANNOTATIONS, strictWhitelist).reachableClasses); + annotation.filterClassInfo(RelType.CLASS_ANNOTATIONS, strictAccept).reachableClasses); } } else if (relType == RelType.CLASSES_WITH_METHOD_ANNOTATION + || relType == RelType.CLASSES_WITH_NONPRIVATE_METHOD_ANNOTATION || relType == RelType.CLASSES_WITH_METHOD_PARAMETER_ANNOTATION - || relType == RelType.CLASSES_WITH_FIELD_ANNOTATION) { + || relType == RelType.CLASSES_WITH_NONPRIVATE_METHOD_PARAMETER_ANNOTATION + || relType == RelType.CLASSES_WITH_FIELD_ANNOTATION + || relType == RelType.CLASSES_WITH_NONPRIVATE_FIELD_ANNOTATION) { // If looking for meta-annotated methods or fields, need to find all meta-annotated annotations, then // look for the methods or fields that they annotate - for (final ClassInfo subAnnotation : this.filterClassInfo(RelType.CLASSES_WITH_ANNOTATION, - strictWhitelist, ClassType.ANNOTATION).reachableClasses) { + for (final ClassInfo subAnnotation : this.filterClassInfo(RelType.CLASSES_WITH_ANNOTATION, strictAccept, + ClassType.ANNOTATION).reachableClasses) { final Set annotatedClasses = subAnnotation.relatedClasses.get(relType); if (annotatedClasses != null) { reachableClasses.addAll(annotatedClasses); @@ -804,8 +981,8 @@ private ReachableAndDirectlyRelatedClasses filterClassInfo(final RelType relType } return new ReachableAndDirectlyRelatedClasses( - filterClassInfo(reachableClasses, scanResult.scanSpec, strictWhitelist, classTypes), - filterClassInfo(directlyRelatedClasses, scanResult.scanSpec, strictWhitelist, classTypes)); + filterClassInfo(reachableClasses, scanResult.scanSpec, strictAccept, classTypes), + filterClassInfo(directlyRelatedClasses, scanResult.scanSpec, strictAccept, classTypes)); } @@ -822,7 +999,37 @@ private ReachableAndDirectlyRelatedClasses filterClassInfo(final RelType relType */ static ClassInfoList getAllClasses(final Collection classes, final ScanSpec scanSpec) { return new ClassInfoList( - ClassInfo.filterClassInfo(classes, scanSpec, /* strictWhitelist = */ true, ClassType.ALL), + ClassInfo.filterClassInfo(classes, scanSpec, /* strictAccept = */ true, ClassType.ALL), + /* sortByName = */ true); + } + + /** + * Get all {@link Enum} classes found during the scan. + * + * @param classes + * the classes + * @param scanSpec + * the scan spec + * @return A list of all {@link Enum} classes found during the scan, or the empty list if none. + */ + static ClassInfoList getAllEnums(final Collection classes, final ScanSpec scanSpec) { + return new ClassInfoList( + ClassInfo.filterClassInfo(classes, scanSpec, /* strictAccept = */ true, ClassType.ENUM), + /* sortByName = */ true); + } + + /** + * Get all {@code record} classes found during the scan. + * + * @param classes + * the classes + * @param scanSpec + * the scan spec + * @return A list of all {@code record} classes found during the scan, or the empty list if none. + */ + static ClassInfoList getAllRecords(final Collection classes, final ScanSpec scanSpec) { + return new ClassInfoList( + ClassInfo.filterClassInfo(classes, scanSpec, /* strictAccept = */ true, ClassType.RECORD), /* sortByName = */ true); } @@ -836,8 +1043,9 @@ static ClassInfoList getAllClasses(final Collection classes, final Sc * @return A list of all standard classes found during the scan, or the empty list if none. */ static ClassInfoList getAllStandardClasses(final Collection classes, final ScanSpec scanSpec) { - return new ClassInfoList(ClassInfo.filterClassInfo(classes, scanSpec, /* strictWhitelist = */ true, - ClassType.STANDARD_CLASS), /* sortByName = */ true); + return new ClassInfoList( + ClassInfo.filterClassInfo(classes, scanSpec, /* strictAccept = */ true, ClassType.STANDARD_CLASS), + /* sortByName = */ true); } /** @@ -851,7 +1059,7 @@ static ClassInfoList getAllStandardClasses(final Collection classes, */ static ClassInfoList getAllImplementedInterfaceClasses(final Collection classes, final ScanSpec scanSpec) { - return new ClassInfoList(ClassInfo.filterClassInfo(classes, scanSpec, /* strictWhitelist = */ true, + return new ClassInfoList(ClassInfo.filterClassInfo(classes, scanSpec, /* strictAccept = */ true, ClassType.IMPLEMENTED_INTERFACE), /* sortByName = */ true); } @@ -867,7 +1075,7 @@ static ClassInfoList getAllImplementedInterfaceClasses(final Collection classes, final ScanSpec scanSpec) { return new ClassInfoList( - ClassInfo.filterClassInfo(classes, scanSpec, /* strictWhitelist = */ true, ClassType.ANNOTATION), + ClassInfo.filterClassInfo(classes, scanSpec, /* strictAccept = */ true, ClassType.ANNOTATION), /* sortByName = */ true); } @@ -879,11 +1087,11 @@ static ClassInfoList getAllAnnotationClasses(final Collection classes * the classes * @param scanSpec * the scan spec - * @return A list of all whitelisted interfaces found during the scan, or the empty list if none. + * @return A list of all accepted interfaces found during the scan, or the empty list if none. */ static ClassInfoList getAllInterfacesOrAnnotationClasses(final Collection classes, final ScanSpec scanSpec) { - return new ClassInfoList(ClassInfo.filterClassInfo(classes, scanSpec, /* strictWhitelist = */ true, + return new ClassInfoList(ClassInfo.filterClassInfo(classes, scanSpec, /* strictAccept = */ true, ClassType.INTERFACE_OR_ANNOTATION), /* sortByName = */ true); } @@ -901,16 +1109,22 @@ public String getName() { } /** - * Get simple name from fully-qualified class name. + * Get simple name from fully-qualified class name. Returns everything after the last '.' or the last '$' in the + * class name, or the whole string if the class is in the root package. (Note that this is not the same as the + * result of {@link Class#getSimpleName()}, which returns "" for anonymous classes.) * + * @param className + * the class name * @return The simple name of the class. */ static String getSimpleName(final String className) { - return className.substring(className.lastIndexOf('.') + 1); + return className.substring(Math.max(className.lastIndexOf('.'), className.lastIndexOf('$')) + 1); } /** - * Get the simple name of the class. + * Get the simple name of the class. Returns everything after the last '.' in the class name, or the whole + * string if the class is in the root package. (Note that this is not the same as the result of + * {@link Class#getSimpleName()}, which returns "" for anonymous classes.) * * @return The simple name of the class. */ @@ -948,13 +1162,33 @@ public String getPackageName() { /** * Checks if this is an external class. * - * @return true if this class is an external class, i.e. was referenced by a whitelisted class as a superclass, - * interface, or annotation, but is not itself a whitelisted class. + * @return true if this class is an external class, i.e. was referenced by an accepted class as a superclass, + * interface, or annotation, but is not itself an accepted class. */ public boolean isExternalClass() { return isExternalClass; } + /** + * Get the minor version of the classfile format for this class' classfile. + * + * @return The minor version of the classfile format for this class' classfile, or 0 if this {@link ClassInfo} + * object is a placeholder for a referenced class that was not found or not accepted during the scan. + */ + public int getClassfileMinorVersion() { + return classfileMinorVersion; + } + + /** + * Get the major version of the classfile format for this class' classfile. + * + * @return The major version of the classfile format for this class' classfile, or 0 if this {@link ClassInfo} + * object is a placeholder for a referenced class that was not found or not accepted during the scan. + */ + public int getClassfileMajorVersion() { + return classfileMajorVersion; + } + /** * Get the class modifier bits. * @@ -982,7 +1216,34 @@ public String getModifiersStr() { * @return true if this class is a public class. */ public boolean isPublic() { - return (modifiers & Modifier.PUBLIC) != 0; + return Modifier.isPublic(modifiers); + } + + /** + * Checks if the class is private. + * + * @return true if this class is a private class. + */ + public boolean isPrivate() { + return Modifier.isPrivate(modifiers); + } + + /** + * Checks if the class is protected. + * + * @return true if this class is a protected class. + */ + public boolean isProtected() { + return Modifier.isProtected(modifiers); + } + + /** + * Checks if the class has default (package) visibility. + * + * @return true if this class is only visible within its package. + */ + public boolean isPackageVisible() { + return !isPublic() && !isPrivate() && !isProtected(); } /** @@ -991,7 +1252,7 @@ public boolean isPublic() { * @return true if this class is an abstract class. */ public boolean isAbstract() { - return (modifiers & 0x400) != 0; + return Modifier.isAbstract(modifiers); } /** @@ -1009,7 +1270,7 @@ public boolean isSynthetic() { * @return true if this class is a final class. */ public boolean isFinal() { - return (modifiers & Modifier.FINAL) != 0; + return Modifier.isFinal(modifiers); } /** @@ -1027,7 +1288,7 @@ public boolean isStatic() { * @return true if this class is an annotation class. */ public boolean isAnnotation() { - return isAnnotation; + return (modifiers & ANNOTATION_CLASS_MODIFIER) != 0; } /** @@ -1037,7 +1298,7 @@ public boolean isAnnotation() { * implemented). */ public boolean isInterface() { - return isInterface && !isAnnotation; + return isInterfaceOrAnnotation() && !isAnnotation(); } /** @@ -1047,7 +1308,7 @@ public boolean isInterface() { * implemented). */ public boolean isInterfaceOrAnnotation() { - return isInterface; + return (modifiers & Modifier.INTERFACE) != 0; } /** @@ -1059,13 +1320,43 @@ public boolean isEnum() { return (modifiers & 0x4000) != 0; } + /** + * Checks if is the class is a record (JDK 14+). + * + * @return true if this class is a record. + */ + public boolean isRecord() { + return isRecord; + } + /** * Checks if this class is a standard class. * * @return true if this class is a standard class (i.e. is not an annotation or interface). */ public boolean isStandardClass() { - return !(isAnnotation || isInterface); + return !(isAnnotation() || isInterface()); + } + + /** + * Checks if this class is an array class. Returns false unless this {@link ClassInfo} is an instance of + * {@link ArrayClassInfo}. + * + * @return true if this is an array class. + */ + public boolean isArrayClass() { + return this instanceof ArrayClassInfo; + } + + /** + * Checks if this class extends the superclass. + * + * @param superclass + * A superclass. + * @return true if this class extends the superclass. + */ + public boolean extendsSuperclass(final Class superclass) { + return extendsSuperclass(superclass.getName()); } /** @@ -1076,7 +1367,8 @@ public boolean isStandardClass() { * @return true if this class extends the named superclass. */ public boolean extendsSuperclass(final String superclassName) { - return getSuperclasses().containsName(superclassName); + return (superclassName.equals("java.lang.Object") && isStandardClass()) + || getSuperclasses().containsName(superclassName); } /** @@ -1121,7 +1413,19 @@ public boolean isAnonymousInnerClass() { * @return true if this class is an implemented interface. */ public boolean isImplementedInterface() { - return relatedClasses.get(RelType.CLASSES_IMPLEMENTING) != null || (isInterface && !isAnnotation); + return relatedClasses.get(RelType.CLASSES_IMPLEMENTING) != null || isInterface(); + } + + /** + * Checks whether this class implements the interface. + * + * @param interfaceClazz + * An interface. + * @return true if this class implements the interface. + */ + public boolean implementsInterface(final Class interfaceClazz) { + Assert.isInterface(interfaceClazz); + return implementsInterface(interfaceClazz.getName()); } /** @@ -1135,6 +1439,18 @@ public boolean implementsInterface(final String interfaceName) { return getInterfaces().containsName(interfaceName); } + /** + * Checks whether this class has the annotation. + * + * @param annotation + * An annotation. + * @return true if this class has the annotation. + */ + public boolean hasAnnotation(final Class annotation) { + Assert.isAnnotation(annotation); + return hasAnnotation(annotation.getName()); + } + /** * Checks whether this class has the named annotation. * @@ -1165,7 +1481,7 @@ public boolean hasDeclaredField(final String fieldName) { * @return true if this class or one of its superclasses declares a field of the given name. */ public boolean hasField(final String fieldName) { - for (final ClassInfo ci : getOverrideOrder()) { + for (final ClassInfo ci : getFieldOverrideOrder()) { if (ci.hasDeclaredField(fieldName)) { return true; } @@ -1173,6 +1489,18 @@ public boolean hasField(final String fieldName) { return false; } + /** + * Checks whether this class declares a field with the annotation. + * + * @param annotation + * A field annotation. + * @return true if this class declares a field with the annotation. + */ + public boolean hasDeclaredFieldAnnotation(final Class annotation) { + Assert.isAnnotation(annotation); + return hasDeclaredFieldAnnotation(annotation.getName()); + } + /** * Checks whether this class declares a field with the named annotation. * @@ -1189,6 +1517,18 @@ public boolean hasDeclaredFieldAnnotation(final String fieldAnnotationName) { return false; } + /** + * Checks whether this class or one of its superclasses declares a field with the annotation. + * + * @param fieldAnnotation + * A field annotation. + * @return true if this class or one of its superclasses declares a field with the annotation. + */ + public boolean hasFieldAnnotation(final Class fieldAnnotation) { + Assert.isAnnotation(fieldAnnotation); + return hasFieldAnnotation(fieldAnnotation.getName()); + } + /** * Checks whether this class or one of its superclasses declares a field with the named annotation. * @@ -1197,7 +1537,7 @@ public boolean hasDeclaredFieldAnnotation(final String fieldAnnotationName) { * @return true if this class or one of its superclasses declares a field with the named annotation. */ public boolean hasFieldAnnotation(final String fieldAnnotationName) { - for (final ClassInfo ci : getOverrideOrder()) { + for (final ClassInfo ci : getFieldOverrideOrder()) { if (ci.hasDeclaredFieldAnnotation(fieldAnnotationName)) { return true; } @@ -1206,11 +1546,11 @@ public boolean hasFieldAnnotation(final String fieldAnnotationName) { } /** - * Checks whether this class declares a field of the given name. + * Checks whether this class declares a method of the given name. * * @param methodName * The name of a method. - * @return true if this class declares a field of the given name. + * @return true if this class declares a method of the given name. */ public boolean hasDeclaredMethod(final String methodName) { return getDeclaredMethodInfo().containsName(methodName); @@ -1224,7 +1564,7 @@ public boolean hasDeclaredMethod(final String methodName) { * @return true if this class or one of its superclasses or interfaces declares a method of the given name. */ public boolean hasMethod(final String methodName) { - for (final ClassInfo ci : getOverrideOrder()) { + for (final ClassInfo ci : getMethodOverrideOrder()) { if (ci.hasDeclaredMethod(methodName)) { return true; } @@ -1232,6 +1572,18 @@ public boolean hasMethod(final String methodName) { return false; } + /** + * Checks whether this class declares a method with the annotation. + * + * @param methodAnnotation + * A method annotation. + * @return true if this class declares a method with the annotation. + */ + public boolean hasDeclaredMethodAnnotation(final Class methodAnnotation) { + Assert.isAnnotation(methodAnnotation); + return hasDeclaredMethodAnnotation(methodAnnotation.getName()); + } + /** * Checks whether this class declares a method with the named annotation. * @@ -1248,6 +1600,18 @@ public boolean hasDeclaredMethodAnnotation(final String methodAnnotationName) { return false; } + /** + * Checks whether this class or one of its superclasses or interfaces declares a method with the annotation. + * + * @param methodAnnotation + * A method annotation. + * @return true if this class or one of its superclasses or interfaces declares a method with the annotation. + */ + public boolean hasMethodAnnotation(final Class methodAnnotation) { + Assert.isAnnotation(methodAnnotation); + return hasMethodAnnotation(methodAnnotation.getName()); + } + /** * Checks whether this class or one of its superclasses or interfaces declares a method with the named * annotation. @@ -1258,7 +1622,7 @@ public boolean hasDeclaredMethodAnnotation(final String methodAnnotationName) { * annotation. */ public boolean hasMethodAnnotation(final String methodAnnotationName) { - for (final ClassInfo ci : getOverrideOrder()) { + for (final ClassInfo ci : getMethodOverrideOrder()) { if (ci.hasDeclaredMethodAnnotation(methodAnnotationName)) { return true; } @@ -1266,6 +1630,19 @@ public boolean hasMethodAnnotation(final String methodAnnotationName) { return false; } + /** + * Checks whether this class declares a method with the annotation. + * + * @param methodParameterAnnotation + * A method annotation. + * @return true if this class declares a method with the annotation. + */ + public boolean hasDeclaredMethodParameterAnnotation( + final Class methodParameterAnnotation) { + Assert.isAnnotation(methodParameterAnnotation); + return hasDeclaredMethodParameterAnnotation(methodParameterAnnotation.getName()); + } + /** * Checks whether this class declares a method with the named annotation. * @@ -1282,6 +1659,18 @@ public boolean hasDeclaredMethodParameterAnnotation(final String methodParameter return false; } + /** + * Checks whether this class or one of its superclasses or interfaces has a method with the annotation. + * + * @param methodParameterAnnotation + * A method annotation. + * @return true if this class or one of its superclasses or interfaces has a method with the annotation. + */ + public boolean hasMethodParameterAnnotation(final Class methodParameterAnnotation) { + Assert.isAnnotation(methodParameterAnnotation); + return hasMethodParameterAnnotation(methodParameterAnnotation.getName()); + } + /** * Checks whether this class or one of its superclasses or interfaces has a method with the named annotation. * @@ -1290,7 +1679,7 @@ public boolean hasDeclaredMethodParameterAnnotation(final String methodParameter * @return true if this class or one of its superclasses or interfaces has a method with the named annotation. */ public boolean hasMethodParameterAnnotation(final String methodParameterAnnotationName) { - for (final ClassInfo ci : getOverrideOrder()) { + for (final ClassInfo ci : getMethodOverrideOrder()) { if (ci.hasDeclaredMethodParameterAnnotation(methodParameterAnnotationName)) { return true; } @@ -1301,7 +1690,7 @@ public boolean hasMethodParameterAnnotation(final String methodParameterAnnotati // ------------------------------------------------------------------------------------------------------------- /** - * Recurse to interfaces and superclasses to get the order that fields and methods are overridden in. + * Recurse to interfaces and superclasses to get the order that fields are overridden in. * * @param visited * visited @@ -1309,61 +1698,140 @@ public boolean hasMethodParameterAnnotation(final String methodParameterAnnotati * the override order * @return the override order */ - private List getOverrideOrder(final Set visited, final List overrideOrderOut) { + private List getFieldOverrideOrder(final Set visited, + final List overrideOrderOut) { if (visited.add(this)) { overrideOrderOut.add(this); for (final ClassInfo iface : getInterfaces()) { - iface.getOverrideOrder(visited, overrideOrderOut); + iface.getFieldOverrideOrder(visited, overrideOrderOut); } final ClassInfo superclass = getSuperclass(); if (superclass != null) { - superclass.getOverrideOrder(visited, overrideOrderOut); + superclass.getFieldOverrideOrder(visited, overrideOrderOut); } } return overrideOrderOut; } /** - * Get the order that fields and methods are overridden in (base class first). + * Get the order that fields are overridden in (base class first). * * @return the override order */ - private List getOverrideOrder() { + private List getFieldOverrideOrder() { if (overrideOrder == null) { - overrideOrder = getOverrideOrder(new HashSet(), new ArrayList()); + overrideOrder = getFieldOverrideOrder(new HashSet(), new ArrayList()); } return overrideOrder; } + /** + * Recurse to collect classes and interfaces in the order of overridden methods, in descending priority. + *

+ * First collects all direct super classes, as their methods always have a higher priority than any method + * declared by an interface. Iterates over interfaces and inserts those extending already found interfaces + * before them in the output. The order of unrelated interfaces is unspecified. + *

+ * See Java Language Specification 8.4.8 for details. + * + * @param visited + * non-null set of already visited ClassInfos + * @param overrideOrderOut + * non-null outgoing list of ClassInfos in descending override order. + * @return the overrideOrderOut instance + */ + private List getMethodOverrideOrder(final Set visited, + final List overrideOrderOut) { + if (!visited.add(this)) { + return overrideOrderOut; + } + //collect concrete super classes first, simply add to overrideOrder + if (!isInterfaceOrAnnotation()) { + overrideOrderOut.add(this); + //iterate over direct super classes first, they have the highest priority regarding method overrides + final ClassInfo superclass = getSuperclass(); + if (superclass != null) { + superclass.getMethodOverrideOrder(visited, overrideOrderOut); + } + for (final ClassInfo iface : getInterfaces()) { + iface.getMethodOverrideOrder(visited, overrideOrderOut); + } + return overrideOrderOut; + } + // overrideOrderOut already contains all concrete classes now. + // This is an interface. If one of the extended interfaces is already in the output, then this needs to be + // added before it. + // Otherwise, this is unrelated to all collected ClassInfo so far and can simply be added to the result. + // The compiler should've prevented inheriting unrelated interfaces with methods having the same signature. + // Can still happen thanks to dynamically linking a different interface during runtime, for which the + // returned order is undefined. + final ClassInfoList interfaces = getInterfaces(); + int minIndex = Integer.MAX_VALUE; + for (final ClassInfo iface : interfaces) { + if (!visited.contains(iface)) { + continue; + } + final int currIdx = overrideOrderOut.indexOf(iface); + minIndex = currIdx >= 0 && currIdx < minIndex ? currIdx : minIndex; + } + if (minIndex == Integer.MAX_VALUE) { + overrideOrderOut.add(this); + } else { + overrideOrderOut.add(minIndex, this); + } + // Add interfaces to end of override order + for (final ClassInfo iface : interfaces) { + iface.getMethodOverrideOrder(visited, overrideOrderOut); + } + return overrideOrderOut; + } + + /** + * Get the order that methods are overridden in. + * + * @return the override order + */ + private List getMethodOverrideOrder() { + if (methodOverrideOrder == null) { + methodOverrideOrder = getMethodOverrideOrder(new HashSet(), new ArrayList()); + } + return methodOverrideOrder; + } + // ------------------------------------------------------------------------------------------------------------- // Standard classes /** * Get the subclasses of this class, sorted in order of name. Call {@link ClassInfoList#directOnly()} to get * direct subclasses. + * + * If this class represents {@link Object}, then returns only standard classes, not interfaces, since interfaces + * don't extend {@link Object}. * * @return the list of subclasses of this class, or the empty list if none. */ public ClassInfoList getSubclasses() { if (getName().equals("java.lang.Object")) { // Make an exception for querying all subclasses of java.lang.Object - return scanResult.getAllClasses(); + return scanResult.getAllStandardClasses(); } else { return new ClassInfoList( - this.filterClassInfo(RelType.SUBCLASSES, /* strictWhitelist = */ !isExternalClass), + this.filterClassInfo(RelType.SUBCLASSES, /* strictAccept = */ !isExternalClass), /* sortByName = */ true); } } /** - * Get all superclasses of this class, in ascending order in the class hierarchy. Does not include - * superinterfaces, if this is an interface (use {@link #getInterfaces()} to get superinterfaces of an - * interface.} + * Get all superclasses of this class, in ascending order in the class hierarchy, not including {@link Object} + * for simplicity, since that is the superclass of all classes. + * + * Also does not include superinterfaces, if this is an interface (use {@link #getInterfaces()} to get + * superinterfaces of an interface.} * * @return the list of all superclasses of this class, or the empty list if none. */ public ClassInfoList getSuperclasses() { - return new ClassInfoList(this.filterClassInfo(RelType.SUPERCLASSES, /* strictWhitelist = */ false), + return new ClassInfoList(this.filterClassInfo(RelType.SUPERCLASSES, /* strictAccept = */ false), /* sortByName = */ false); } @@ -1398,7 +1866,7 @@ public ClassInfo getSuperclass() { */ public ClassInfoList getOuterClasses() { return new ClassInfoList( - this.filterClassInfo(RelType.CONTAINED_WITHIN_OUTER_CLASS, /* strictWhitelist = */ false), + this.filterClassInfo(RelType.CONTAINED_WITHIN_OUTER_CLASS, /* strictAccept = */ false), /* sortByName = */ false); } @@ -1408,7 +1876,7 @@ public ClassInfoList getOuterClasses() { * @return A list of the inner classes contained within this class, or the empty list if none. */ public ClassInfoList getInnerClasses() { - return new ClassInfoList(this.filterClassInfo(RelType.CONTAINS_INNER_CLASS, /* strictWhitelist = */ false), + return new ClassInfoList(this.filterClassInfo(RelType.CONTAINS_INNER_CLASS, /* strictAccept = */ false), /* sortByName = */ true); } @@ -1437,16 +1905,17 @@ public String getFullyQualifiedDefiningMethodName() { public ClassInfoList getInterfaces() { // Classes also implement the interfaces of their superclasses final ReachableAndDirectlyRelatedClasses implementedInterfaces = this - .filterClassInfo(RelType.IMPLEMENTED_INTERFACES, /* strictWhitelist = */ false); + .filterClassInfo(RelType.IMPLEMENTED_INTERFACES, /* strictAccept = */ false); final Set allInterfaces = new LinkedHashSet<>(implementedInterfaces.reachableClasses); for (final ClassInfo superclass : this.filterClassInfo(RelType.SUPERCLASSES, - /* strictWhitelist = */ false).reachableClasses) { - final Set superclassImplementedInterfaces = superclass.filterClassInfo( - RelType.IMPLEMENTED_INTERFACES, /* strictWhitelist = */ false).reachableClasses; + /* strictAccept = */ false).reachableClasses) { + final Set superclassImplementedInterfaces = superclass + .filterClassInfo(RelType.IMPLEMENTED_INTERFACES, /* strictAccept = */ false).reachableClasses; allInterfaces.addAll(superclassImplementedInterfaces); } + // Can't sort interfaces by name, since their order is significant in the definition of inheritance return new ClassInfoList(allInterfaces, implementedInterfaces.directlyRelatedClasses, - /* sortByName = */ true); + /* sortByName = */ false); } /** @@ -1456,16 +1925,13 @@ public ClassInfoList getInterfaces() { * interface, otherwise returns the empty list. */ public ClassInfoList getClassesImplementing() { - if (!isInterface) { - throw new IllegalArgumentException("Class is not an interface: " + getName()); - } // Subclasses of implementing classes also implement the interface final ReachableAndDirectlyRelatedClasses implementingClasses = this - .filterClassInfo(RelType.CLASSES_IMPLEMENTING, /* strictWhitelist = */ !isExternalClass); + .filterClassInfo(RelType.CLASSES_IMPLEMENTING, /* strictAccept = */ !isExternalClass); final Set allImplementingClasses = new LinkedHashSet<>(implementingClasses.reachableClasses); for (final ClassInfo implementingClass : implementingClasses.reachableClasses) { final Set implementingSubclasses = implementingClass.filterClassInfo(RelType.SUBCLASSES, - /* strictWhitelist = */ !implementingClass.isExternalClass).reachableClasses; + /* strictAccept = */ !implementingClass.isExternalClass).reachableClasses; allImplementingClasses.addAll(implementingSubclasses); } return new ClassInfoList(allImplementingClasses, implementingClasses.directlyRelatedClasses, @@ -1489,38 +1955,45 @@ public ClassInfoList getClassesImplementing() { * @return the list of annotations and meta-annotations on this class. */ public ClassInfoList getAnnotations() { - if (!scanResult.scanSpec.enableAnnotationInfo) { - throw new IllegalArgumentException("Please call ClassGraph#enableAnnotationInfo() before #scan()"); - } + synchronized (this) { + if (annotationsRef != null) { + return annotationsRef; + } + + if (!scanResult.scanSpec.enableAnnotationInfo) { + throw new IllegalArgumentException("Please call ClassGraph#enableAnnotationInfo() before #scan()"); + } - // Get all annotations on this class - final ReachableAndDirectlyRelatedClasses annotationClasses = this.filterClassInfo(RelType.CLASS_ANNOTATIONS, - /* strictWhitelist = */ false); - // Check for any @Inherited annotations on superclasses - Set inheritedSuperclassAnnotations = null; - for (final ClassInfo superclass : getSuperclasses()) { - for (final ClassInfo superclassAnnotation : superclass.filterClassInfo(RelType.CLASS_ANNOTATIONS, - /* strictWhitelist = */ false).reachableClasses) { - // Check if any of the meta-annotations on this annotation are @Inherited, - // which causes an annotation to annotate a class and all of its subclasses. - if (superclassAnnotation != null && superclassAnnotation.isInherited) { - // superclassAnnotation has an @Inherited meta-annotation - if (inheritedSuperclassAnnotations == null) { - inheritedSuperclassAnnotations = new LinkedHashSet<>(); + // Get all annotations on this class + final ReachableAndDirectlyRelatedClasses annotationClasses = this + .filterClassInfo(RelType.CLASS_ANNOTATIONS, /* strictAccept = */ false); + // Check for any @Inherited annotations on superclasses + Set inheritedSuperclassAnnotations = null; + for (final ClassInfo superclass : getSuperclasses()) { + for (final ClassInfo superclassAnnotation : superclass.filterClassInfo(RelType.CLASS_ANNOTATIONS, + /* strictAccept = */ false).reachableClasses) { + // Check if any of the meta-annotations on this annotation are @Inherited, + // which causes an annotation to annotate a class and all of its subclasses. + if (superclassAnnotation != null && superclassAnnotation.isInherited) { + // superclassAnnotation has an @Inherited meta-annotation + if (inheritedSuperclassAnnotations == null) { + inheritedSuperclassAnnotations = new LinkedHashSet<>(); + } + inheritedSuperclassAnnotations.add(superclassAnnotation); } - inheritedSuperclassAnnotations.add(superclassAnnotation); } } - } - if (inheritedSuperclassAnnotations == null) { - // No inherited superclass annotations - return new ClassInfoList(annotationClasses, /* sortByName = */ true); - } else { - // Merge inherited superclass annotations and annotations on this class - inheritedSuperclassAnnotations.addAll(annotationClasses.reachableClasses); - return new ClassInfoList(inheritedSuperclassAnnotations, annotationClasses.directlyRelatedClasses, - /* sortByName = */ true); + if (inheritedSuperclassAnnotations == null) { + // No inherited superclass annotations + annotationsRef = new ClassInfoList(annotationClasses, /* sortByName = */ true); + } else { + // Merge inherited superclass annotations and annotations on this class + inheritedSuperclassAnnotations.addAll(annotationClasses.reachableClasses); + annotationsRef = new ClassInfoList(inheritedSuperclassAnnotations, + annotationClasses.directlyRelatedClasses, /* sortByName = */ true); + } + return annotationsRef; } } @@ -1543,7 +2016,7 @@ private ClassInfoList getFieldOrMethodAnnotations(final RelType relType) { + "Info() and " + "#enableAnnotationInfo() before #scan()"); } final ReachableAndDirectlyRelatedClasses fieldOrMethodAnnotations = this.filterClassInfo(relType, - /* strictWhitelist = */ false, ClassType.ANNOTATION); + /* strictAccept = */ false, ClassType.ANNOTATION); final Set fieldOrMethodAnnotationsAndMetaAnnotations = new LinkedHashSet<>( fieldOrMethodAnnotations.reachableClasses); return new ClassInfoList(fieldOrMethodAnnotationsAndMetaAnnotations, @@ -1555,22 +2028,26 @@ private ClassInfoList getFieldOrMethodAnnotations(final RelType relType) { * * @param relType * One of {@link RelType#CLASSES_WITH_FIELD_ANNOTATION}, - * {@link RelType#CLASSES_WITH_METHOD_ANNOTATION} or - * {@link RelType#CLASSES_WITH_METHOD_PARAMETER_ANNOTATION}. + * {@link RelType#CLASSES_WITH_NONPRIVATE_FIELD_ANNOTATION}, + * {@link RelType#CLASSES_WITH_METHOD_ANNOTATION}, + * {@link RelType#CLASSES_WITH_NONPRIVATE_METHOD_ANNOTATION}, + * {@link RelType#CLASSES_WITH_METHOD_PARAMETER_ANNOTATION}, or + * {@link RelType#CLASSES_WITH_NONPRIVATE_METHOD_PARAMETER_ANNOTATION}. * @return A list of classes that have a declared method with this annotation or meta-annotation, or the empty * list if none. */ private ClassInfoList getClassesWithFieldOrMethodAnnotation(final RelType relType) { - final boolean isField = relType == RelType.CLASSES_WITH_FIELD_ANNOTATION; + final boolean isField = relType == RelType.CLASSES_WITH_FIELD_ANNOTATION + || relType == RelType.CLASSES_WITH_NONPRIVATE_FIELD_ANNOTATION; if (!(isField ? scanResult.scanSpec.enableFieldInfo : scanResult.scanSpec.enableMethodInfo) || !scanResult.scanSpec.enableAnnotationInfo) { throw new IllegalArgumentException("Please call ClassGraph#enable" + (isField ? "Field" : "Method") + "Info() and " + "#enableAnnotationInfo() before #scan()"); } final ReachableAndDirectlyRelatedClasses classesWithDirectlyAnnotatedFieldsOrMethods = this - .filterClassInfo(relType, /* strictWhitelist = */ !isExternalClass); + .filterClassInfo(relType, /* strictAccept = */ !isExternalClass); final ReachableAndDirectlyRelatedClasses annotationsWithThisMetaAnnotation = this.filterClassInfo( - RelType.CLASSES_WITH_ANNOTATION, /* strictWhitelist = */ !isExternalClass, ClassType.ANNOTATION); + RelType.CLASSES_WITH_ANNOTATION, /* strictAccept = */ !isExternalClass, ClassType.ANNOTATION); if (annotationsWithThisMetaAnnotation.reachableClasses.isEmpty()) { // This annotation does not meta-annotate another annotation that annotates a method return new ClassInfoList(classesWithDirectlyAnnotatedFieldsOrMethods, /* sortByName = */ true); @@ -1582,7 +2059,7 @@ private ClassInfoList getClassesWithFieldOrMethodAnnotation(final RelType relTyp for (final ClassInfo metaAnnotatedAnnotation : annotationsWithThisMetaAnnotation.reachableClasses) { allClassesWithAnnotatedOrMetaAnnotatedFieldsOrMethods .addAll(metaAnnotatedAnnotation.filterClassInfo(relType, - /* strictWhitelist = */ !metaAnnotatedAnnotation.isExternalClass).reachableClasses); + /* strictAccept = */ !metaAnnotatedAnnotation.isExternalClass).reachableClasses); } return new ClassInfoList(allClassesWithAnnotatedOrMetaAnnotatedFieldsOrMethods, classesWithDirectlyAnnotatedFieldsOrMethods.directlyRelatedClasses, /* sortByName = */ true); @@ -1600,25 +2077,56 @@ private ClassInfoList getClassesWithFieldOrMethodAnnotation(final RelType relTyp * none. */ public AnnotationInfoList getAnnotationInfo() { - if (!scanResult.scanSpec.enableAnnotationInfo) { - throw new IllegalArgumentException("Please call ClassGraph#enableAnnotationInfo() before #scan()"); + synchronized (this) { + if (annotationInfoRef != null) { + return annotationInfoRef; + } + + if (!scanResult.scanSpec.enableAnnotationInfo) { + throw new IllegalArgumentException("Please call ClassGraph#enableAnnotationInfo() before #scan()"); + } + + annotationInfoRef = AnnotationInfoList.getIndirectAnnotations(annotationInfo, this); + return annotationInfoRef; } - return AnnotationInfoList.getIndirectAnnotations(annotationInfo, this); } /** - * Get a the named non-{@link Repeatable} annotation on this class, or null if the class does not have the named - * annotation. (Use {@link #getAnnotationInfoRepeatable(String)} for {@link Repeatable} annotations.) + * Get a the non-{@link Repeatable} annotation on this class, or null if the class does not have the annotation. + * (Use {@link #getAnnotationInfoRepeatable(String)} for {@link Repeatable} annotations.) * *

* Also handles the {@link Inherited} meta-annotation, which causes an annotation to annotate a class and all of * its subclasses. * *

+ * Note that if you need to get multiple annotations, it is faster to call {@link #getAnnotationInfo()}, and + * then get the annotations from the returned {@link AnnotationInfoList}, so that the returned list doesn't have + * to be built multiple times. + * + * @param annotation + * The annotation. + * @return An {@link AnnotationInfo} object representing the annotation on this class, or null if the class does + * not have the annotation. + */ + public AnnotationInfo getAnnotationInfo(final Class annotation) { + Assert.isAnnotation(annotation); + return getAnnotationInfo(annotation.getName()); + } + + /** + * Get a the named non-{@link Repeatable} annotation on this class, or null if the class does not have the named + * annotation. (Use {@link #getAnnotationInfoRepeatable(String)} for {@link Repeatable} annotations.) + * + *

+ * Also handles the {@link Inherited} meta-annotation, which causes an annotation to annotate a class and all of + * its subclasses. + * + *

* Note that if you need to get multiple named annotations, it is faster to call {@link #getAnnotationInfo()}, * and then get the named annotations from the returned {@link AnnotationInfoList}, so that the returned list * doesn't have to be built multiple times. - * + * * @param annotationName * The annotation name. * @return An {@link AnnotationInfo} object representing the named annotation on this class, or null if the @@ -1629,18 +2137,41 @@ public AnnotationInfo getAnnotationInfo(final String annotationName) { } /** - * Get a the named {@link Repeatable} annotation on this class, or the empty list if the class does not have the - * named annotation. + * Get a the {@link Repeatable} annotation on this class, or the empty list if the class does not have the + * annotation. * *

* Also handles the {@link Inherited} meta-annotation, which causes an annotation to annotate a class and all of * its subclasses. * *

+ * Note that if you need to get multiple annotations, it is faster to call {@link #getAnnotationInfo()}, and + * then get the annotations from the returned {@link AnnotationInfoList}, so that the returned list doesn't have + * to be built multiple times. + * + * @param annotation + * The annotation. + * @return An {@link AnnotationInfoList} of all instances of the annotation on this class, or the empty list if + * the class does not have the annotation. + */ + public AnnotationInfoList getAnnotationInfoRepeatable(final Class annotation) { + Assert.isAnnotation(annotation); + return getAnnotationInfoRepeatable(annotation.getName()); + } + + /** + * Get a the named {@link Repeatable} annotation on this class, or the empty list if the class does not have the + * named annotation. + * + *

+ * Also handles the {@link Inherited} meta-annotation, which causes an annotation to annotate a class and all of + * its subclasses. + * + *

* Note that if you need to get multiple named annotations, it is faster to call {@link #getAnnotationInfo()}, * and then get the named annotations from the returned {@link AnnotationInfoList}, so that the returned list * doesn't have to be built multiple times. - * + * * @param annotationName * The annotation name. * @return An {@link AnnotationInfoList} of all instances of the named annotation on this class, or the empty @@ -1660,17 +2191,19 @@ public AnnotationParameterValueList getAnnotationDefaultParameterValues() { if (!scanResult.scanSpec.enableAnnotationInfo) { throw new IllegalArgumentException("Please call ClassGraph#enableAnnotationInfo() before #scan()"); } - if (!isAnnotation) { + if (!isAnnotation()) { throw new IllegalArgumentException("Class is not an annotation: " + getName()); } - if (annotationDefaultParamValues == null) { - return AnnotationParameterValueList.EMPTY_LIST; - } - if (!annotationDefaultParamValuesHasBeenConvertedToPrimitive) { - annotationDefaultParamValues.convertWrapperArraysToPrimitiveArrays(this); - annotationDefaultParamValuesHasBeenConvertedToPrimitive = true; + synchronized (this) { + if (annotationDefaultParamValues == null) { + return AnnotationParameterValueList.EMPTY_LIST; + } + if (!annotationDefaultParamValuesHasBeenConvertedToPrimitive) { + annotationDefaultParamValues.convertWrapperArraysToPrimitiveArrays(this); + annotationDefaultParamValuesHasBeenConvertedToPrimitive = true; + } + return annotationDefaultParamValues; } - return annotationDefaultParamValues; } /** @@ -1684,13 +2217,10 @@ public ClassInfoList getClassesWithAnnotation() { if (!scanResult.scanSpec.enableAnnotationInfo) { throw new IllegalArgumentException("Please call ClassGraph#enableAnnotationInfo() before #scan()"); } - if (!isAnnotation) { - throw new IllegalArgumentException("Class is not an annotation: " + getName()); - } // Get classes that have this annotation final ReachableAndDirectlyRelatedClasses classesWithAnnotation = this - .filterClassInfo(RelType.CLASSES_WITH_ANNOTATION, /* strictWhitelist = */ !isExternalClass); + .filterClassInfo(RelType.CLASSES_WITH_ANNOTATION, /* strictAccept = */ !isExternalClass); if (isInherited) { // If this is an inherited annotation, add into the result all subclasses of the annotated classes. @@ -1715,7 +2245,7 @@ public ClassInfoList getClassesWithAnnotation() { */ ClassInfoList getClassesWithAnnotationDirectOnly() { return new ClassInfoList( - this.filterClassInfo(RelType.CLASSES_WITH_ANNOTATION, /* strictWhitelist = */ !isExternalClass), + this.filterClassInfo(RelType.CLASSES_WITH_ANNOTATION, /* strictAccept = */ !isExternalClass), /* sortByName = */ true); } @@ -1801,13 +2331,14 @@ private MethodInfoList getMethodInfo(final String methodName, final boolean getN // Implement method/constructor overriding final MethodInfoList methodInfoList = new MethodInfoList(); final Set> nameAndTypeDescriptorSet = new HashSet<>(); - for (final ClassInfo ci : getOverrideOrder()) { - for (final MethodInfo mi : ci.getDeclaredMethodInfo(methodName, getNormalMethods, getConstructorMethods, + for (final ClassInfo ci : getMethodOverrideOrder()) { + // Constructors are not inherited from superclasses + boolean shouldGetConstructorMethods = ci == this && getConstructorMethods; + for (final MethodInfo mi : ci.getDeclaredMethodInfo(methodName, getNormalMethods, shouldGetConstructorMethods, getStaticInitializerMethods)) { - // If method/constructor has not been overridden by method of same name and type descriptor - if (nameAndTypeDescriptorSet - .add(new SimpleEntry<>(mi.getName(), mi.getTypeDescriptor().toString()))) { - // Add method/constructor to output order + // If method has not been overridden by method of same name and type descriptor + if (nameAndTypeDescriptorSet.add(new SimpleEntry<>(mi.getName(), mi.getTypeDescriptorStr()))) { + // Add method to output order methodInfoList.add(mi); } } @@ -2129,23 +2660,46 @@ public ClassInfoList getMethodParameterAnnotations() { } /** - * Get all classes that have this class as a method annotation. + * Get all classes that have this class as a method annotation, and their subclasses, if the method is + * non-private. * * @return A list of classes that have a declared method with this annotation or meta-annotation, or the empty * list if none. */ public ClassInfoList getClassesWithMethodAnnotation() { - return getClassesWithFieldOrMethodAnnotation(RelType.CLASSES_WITH_METHOD_ANNOTATION); + // Get all classes that have a method annotated or meta-annotated with this annotation + final Set classesWithMethodAnnotation = new HashSet<>( + getClassesWithFieldOrMethodAnnotation(RelType.CLASSES_WITH_METHOD_ANNOTATION)); + // Add subclasses of all classes with a method that is non-privately annotated or meta-annotated with + // this annotation (non-private methods are inherited) + for (final ClassInfo classWithNonprivateMethodAnnotationOrMetaAnnotation : // + getClassesWithFieldOrMethodAnnotation(RelType.CLASSES_WITH_NONPRIVATE_METHOD_ANNOTATION)) { + classesWithMethodAnnotation.addAll(classWithNonprivateMethodAnnotationOrMetaAnnotation.getSubclasses()); + } + return new ClassInfoList(classesWithMethodAnnotation, + new HashSet<>(getClassesWithMethodAnnotationDirectOnly()), /* sortByName = */ true); } /** - * Get all classes that have this class as a method parameter annotation. + * Get all classes that have this class as a method parameter annotation, and their subclasses, if the method is + * non-private. * * @return A list of classes that have a declared method with a parameter that is annotated with this annotation * or meta-annotation, or the empty list if none. */ public ClassInfoList getClassesWithMethodParameterAnnotation() { - return getClassesWithFieldOrMethodAnnotation(RelType.CLASSES_WITH_METHOD_PARAMETER_ANNOTATION); + // Get all classes that have a method annotated or meta-annotated with this annotation + final Set classesWithMethodParameterAnnotation = new HashSet<>( + getClassesWithFieldOrMethodAnnotation(RelType.CLASSES_WITH_METHOD_PARAMETER_ANNOTATION)); + // Add subclasses of all classes with a method that is non-privately annotated or meta-annotated with + // this annotation (non-private methods are inherited) + for (final ClassInfo classWithNonprivateMethodParameterAnnotationOrMetaAnnotation : // + getClassesWithFieldOrMethodAnnotation(RelType.CLASSES_WITH_NONPRIVATE_METHOD_PARAMETER_ANNOTATION)) { + classesWithMethodParameterAnnotation + .addAll(classWithNonprivateMethodParameterAnnotationOrMetaAnnotation.getSubclasses()); + } + return new ClassInfoList(classesWithMethodParameterAnnotation, + new HashSet<>(getClassesWithMethodParameterAnnotationDirectOnly()), /* sortByName = */ true); } /** @@ -2155,8 +2709,20 @@ public ClassInfoList getClassesWithMethodParameterAnnotation() { * the requested method annotation, or the empty list if none. */ ClassInfoList getClassesWithMethodAnnotationDirectOnly() { - return new ClassInfoList(this.filterClassInfo(RelType.CLASSES_WITH_METHOD_ANNOTATION, - /* strictWhitelist = */ !isExternalClass), /* sortByName = */ true); + return new ClassInfoList( + this.filterClassInfo(RelType.CLASSES_WITH_METHOD_ANNOTATION, /* strictAccept = */ !isExternalClass), + /* sortByName = */ true); + } + + /** + * Get the classes that have this class as a direct method parameter annotation. + * + * @return A list of classes that declare methods with parameters that are directly annotated (i.e. are not + * meta-annotated) with the requested method annotation, or the empty list if none. + */ + ClassInfoList getClassesWithMethodParameterAnnotationDirectOnly() { + return new ClassInfoList(this.filterClassInfo(RelType.CLASSES_WITH_METHOD_PARAMETER_ANNOTATION, + /* strictAccept = */ !isExternalClass), /* sortByName = */ true); } // ------------------------------------------------------------------------------------------------------------- @@ -2176,7 +2742,7 @@ ClassInfoList getClassesWithMethodAnnotationDirectOnly() { * {@link IllegalArgumentException}. * *

- * By default only returns information for public methods, unless {@link ClassGraph#ignoreFieldVisibility()} was + * By default only returns information for public fields, unless {@link ClassGraph#ignoreFieldVisibility()} was * called before the scan. * * @return the list of FieldInfo objects for visible fields declared by this class, or the empty list if no @@ -2205,7 +2771,7 @@ public FieldInfoList getDeclaredFieldInfo() { * {@link IllegalArgumentException}. * *

- * By default only returns information for public methods, unless {@link ClassGraph#ignoreFieldVisibility()} was + * By default only returns information for public fields, unless {@link ClassGraph#ignoreFieldVisibility()} was * called before the scan. * * @return the list of FieldInfo objects for visible fields of this class or its superclases, or the empty list @@ -2220,7 +2786,7 @@ public FieldInfoList getFieldInfo() { // Implement field overriding final FieldInfoList fieldInfoList = new FieldInfoList(); final Set fieldNameSet = new HashSet<>(); - for (final ClassInfo ci : getOverrideOrder()) { + for (final ClassInfo ci : getFieldOverrideOrder()) { for (final FieldInfo fi : ci.getDeclaredFieldInfo()) { // If field has not been overridden by field of same name if (fieldNameSet.add(fi.getName())) { @@ -2232,6 +2798,48 @@ public FieldInfoList getFieldInfo() { return fieldInfoList; } + /** + * Get the enum constants of an enum class. + * + * @return All enum constants of an enum class as a list of {@link FieldInfo} objects (enum constants are stored + * as fields in Java classes). + */ + public FieldInfoList getEnumConstants() { + if (!isEnum()) { + throw new IllegalArgumentException("Class " + getName() + " is not an enum"); + } + return getFieldInfo().filter(new FieldInfoFilter() { + @Override + public boolean accept(final FieldInfo fieldInfo) { + return fieldInfo.isEnum(); + } + }); + } + + /** + * Get the enum constants of an enum class. + * + * @return All enum constants of an enum class as a list of objects of the same type as the enum. + */ + public List getEnumConstantObjects() { + if (!isEnum()) { + throw new IllegalArgumentException("Class " + getName() + " is not an enum"); + } + final Class enumClass = loadClass(); + final FieldInfoList consts = getEnumConstants(); + final List constObjs = new ArrayList<>(consts.size()); + final ReflectionUtils reflectionUtils = scanResult == null ? new ReflectionUtils() + : scanResult.reflectionUtils; + for (final FieldInfo constFieldInfo : consts) { + final Object constObj = reflectionUtils.getStaticFieldVal(true, enumClass, constFieldInfo.getName()); + if (constObj == null) { + throw new IllegalArgumentException("Could not read enum constant objects"); + } + constObjs.add(constObj); + } + return constObjs; + } + /** * Returns information on the named field declared by the class, but not by its superclasses. See also: * @@ -2285,7 +2893,7 @@ public FieldInfo getDeclaredFieldInfo(final String fieldName) { * {@link IllegalArgumentException}. * *

- * By default only returns information for public methods, unless {@link ClassGraph#ignoreFieldVisibility()} was + * By default only returns information for public fields, unless {@link ClassGraph#ignoreFieldVisibility()} was * called before the scan. * * @param fieldName @@ -2300,7 +2908,7 @@ public FieldInfo getFieldInfo(final String fieldName) { throw new IllegalArgumentException("Please call ClassGraph#enableFieldInfo() before #scan()"); } // Implement field overriding - for (final ClassInfo ci : getOverrideOrder()) { + for (final ClassInfo ci : getFieldOverrideOrder()) { final FieldInfo fi = ci.getDeclaredFieldInfo(fieldName); if (fi != null) { return fi; @@ -2327,7 +2935,17 @@ public ClassInfoList getFieldAnnotations() { * none. */ public ClassInfoList getClassesWithFieldAnnotation() { - return getClassesWithFieldOrMethodAnnotation(RelType.CLASSES_WITH_FIELD_ANNOTATION); + // Get all classes that have a field annotated or meta-annotated with this annotation + final Set classesWithMethodAnnotation = new HashSet<>( + getClassesWithFieldOrMethodAnnotation(RelType.CLASSES_WITH_FIELD_ANNOTATION)); + // Add subclasses of all classes with a field that is non-privately annotated or meta-annotated with + // this annotation (non-private fields are inherited) + for (final ClassInfo classWithNonprivateMethodAnnotationOrMetaAnnotation : // + getClassesWithFieldOrMethodAnnotation(RelType.CLASSES_WITH_NONPRIVATE_FIELD_ANNOTATION)) { + classesWithMethodAnnotation.addAll(classWithNonprivateMethodAnnotationOrMetaAnnotation.getSubclasses()); + } + return new ClassInfoList(classesWithMethodAnnotation, + new HashSet<>(getClassesWithMethodAnnotationDirectOnly()), /* sortByName = */ true); } /** @@ -2337,43 +2955,142 @@ public ClassInfoList getClassesWithFieldAnnotation() { * the requested method annotation, or the empty list if none. */ ClassInfoList getClassesWithFieldAnnotationDirectOnly() { - return new ClassInfoList(this.filterClassInfo(RelType.CLASSES_WITH_FIELD_ANNOTATION, - /* strictWhitelist = */ !isExternalClass), /* sortByName = */ true); + return new ClassInfoList( + this.filterClassInfo(RelType.CLASSES_WITH_FIELD_ANNOTATION, /* strictAccept = */ !isExternalClass), + /* sortByName = */ true); } // ------------------------------------------------------------------------------------------------------------- /** - * Get the type signature of the class. + * Get the parsed type signature for the class. * - * @return The class type signature, if available, otherwise returns null. + * @return The parsed type signature for the class, including any generic type parameters, or null if not + * available (probably indicating the class is not generic). + * @throws IllegalArgumentException + * if the class type signature cannot be parsed (this should only be thrown in the case of classfile + * corruption, or a compiler bug that causes an invalid type signature to be written to the + * classfile). */ public ClassTypeSignature getTypeSignature() { - if (typeSignatureStr == null) { - return null; - } - if (typeSignature == null) { - try { - typeSignature = ClassTypeSignature.parse(typeSignatureStr, this); - typeSignature.setScanResult(scanResult); - } catch (final ParseException e) { - throw new IllegalArgumentException(e); + synchronized (this) { + if (typeSignatureStr == null) { + return null; + } + if (typeSignature == null) { + try { + typeSignature = ClassTypeSignature.parse(typeSignatureStr, this); + typeSignature.setScanResult(scanResult); + if (typeAnnotationDecorators != null) { + for (final ClassTypeAnnotationDecorator decorator : typeAnnotationDecorators) { + decorator.decorate(typeSignature); + } + } + } catch (final ParseException e) { + throw new IllegalArgumentException("Invalid type signature for class " + getName() + + " in classpath element " + getClasspathElementURI() + " : " + typeSignatureStr, e); + } } } return typeSignature; } + /** + * Get the type signature string for the class. + * + * @return The type signature string for the class, including any generic type parameters, or null if not + * available (probably indicating the class is not generic). + */ + public String getTypeSignatureStr() { + return typeSignatureStr; + } + + /** + * Returns the parsed type signature for this class, possibly including type parameters. If the type signature + * is not present for this class, indicating that this is not a generic class, then a type descriptor will be + * synthesized and returned, as if there were a type descriptor (classfiles may have a type signature but do not + * contain a type descriptor). May include type annotations on the superclass or interface(s). + * + * @return The parsed generic type signature for the class, or if not available, the synthetic type descriptor + * for the class. + */ + public ClassTypeSignature getTypeSignatureOrTypeDescriptor() { + ClassTypeSignature typeSig = null; + try { + typeSig = getTypeSignature(); + if (typeSig != null) { + return typeSig; + } + } catch (final Exception e) { + // Ignore + } + return getTypeDescriptor(); + } + + /** + * Returns a synthetic type descriptor for the method, created from the class name, superclass name, and + * implemented interfaces. May include type annotations on the superclass or interface(s). + * + * @return The synthetic type descriptor for the class. + */ + public ClassTypeSignature getTypeDescriptor() { + synchronized (this) { + if (typeDescriptor == null) { + typeDescriptor = new ClassTypeSignature(this, getSuperclass(), getInterfaces()); + typeDescriptor.setScanResult(scanResult); + if (typeAnnotationDecorators != null) { + for (final ClassTypeAnnotationDecorator decorator : typeAnnotationDecorators) { + decorator.decorate(typeDescriptor); + } + } + } + } + return typeDescriptor; + } + + /** + * Returns the name of the source file this class has been compiled from, such as {@code ClassInfo.java} or + * {@code KClass.kt}. + * + *

+ * This field may be {@code null}. + * + * @return The name of the source file of this class, or {@code null} if not available + */ + public String getSourceFile() { + return sourceFile; + } + // ------------------------------------------------------------------------------------------------------------- /** - * Get the {@link URL} of the classpath element that this class was found within. + * Get the {@link URI} of the classpath element that this class was found within. + * + * @return The {@link URI} of the classpath element that this class was found within. + * @throws IllegalArgumentException + * if the classpath element does not have a valid URI (e.g. for modules whose location URI is null). + */ + public URI getClasspathElementURI() { + // Calling classfileResource.getClasspathElementURI() rather than classpathElement.getURI() will append + // any automatically-stripped package root prefix + return classfileResource.getClasspathElementURI(); + } + + /** + * Get the {@link URL} of the classpath element or module that this class was found within. Use + * {@link #getClasspathElementURI()} instead if the resource may have come from a system module, or if this is a + * jlink'd runtime image, since "jrt:" URI schemes used by system modules and jlink'd runtime images are not + * suppored by {@link URL}, and this will cause {@link IllegalArgumentException} to be thrown. * * @return The {@link URL} of the classpath element that this class was found within. + * @throws IllegalArgumentException + * if the classpath element URI cannot be converted to a {@link URL} (in particular, if the URI has + * a {@code jrt:/} scheme). */ public URL getClasspathElementURL() { try { - return classpathElement.getURI().toURL(); - } catch (final MalformedURLException e) { + return getClasspathElementURI().toURL(); + } catch (final IllegalArgumentException | MalformedURLException e) { throw new IllegalArgumentException("Could not get classpath element URL", e); } } @@ -2383,9 +3100,14 @@ public URL getClasspathElementURL() { * null if this class was found in a module. (See also {@link #getModuleRef}.) * * @return The {@link File} for the classpath element package root dir or jar that this class was found within, - * or null if this class was found in a module. (See also {@link #getModuleRef}.) + * or null if this class was found in a module (see {@link #getModuleRef}). May also return null if the + * classpath element was an http/https URL, and the jar was downloaded directly to RAM, rather than to a + * temp file on disk (e.g. if the temp dir is not writeable). */ public File getClasspathElementFile() { + if (classpathElement == null) { + throw new IllegalArgumentException("Classpath element is not known for this classpath element"); + } return classpathElement.getFile(); } @@ -2397,6 +3119,9 @@ public File getClasspathElementFile() { * in a directory or jar in the classpath. (See also {@link #getClasspathElementFile()}.) */ public ModuleRef getModuleRef() { + if (classpathElement == null) { + throw new IllegalArgumentException("Classpath element is not known for this classpath element"); + } return classpathElement instanceof ClasspathElementModule ? ((ClasspathElementModule) classpathElement).getModuleRef() : null; @@ -2405,9 +3130,9 @@ public ModuleRef getModuleRef() { /** * The {@link Resource} for the classfile of this class. * - * @return The {@link Resource} for the classfile of this class, or null if this is an "external" class (a - * blacklisted class, or a class in a blacklisted package, or a class that was referenced as a - * superclass, interface or annotation, but that wasn't in the scanned path). + * @return The {@link Resource} for the classfile of this class. Returns null if the classfile for this class + * was not actually read during the scan, e.g. because this class was not itself accepted, but was + * referenced by an accepted class. */ public Resource getResource() { return classfileResource; @@ -2557,7 +3282,7 @@ void setScanResult(final ScanResult scanResult) { void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames) { if (annotationInfo != null) { annotationInfo.handleRepeatableAnnotations(allRepeatableAnnotationNames, this, - RelType.CLASS_ANNOTATIONS, RelType.CLASSES_WITH_ANNOTATION); + RelType.CLASS_ANNOTATIONS, RelType.CLASSES_WITH_ANNOTATION, null); } if (fieldInfo != null) { for (final FieldInfo fi : fieldInfo) { @@ -2588,26 +3313,44 @@ void addReferencedClassNames(final Set refdClassNames) { } /** - * Get the names of any classes referenced in this class' type descriptor, or the type descriptors of fields, - * methods or annotations. + * Get {@link ClassInfo} objects for any classes referenced in this class' type descriptor, or the type + * descriptors of fields, methods or annotations. * - * @param referencedClassNames - * the referenced class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info + * @param log + * the log */ @Override - protected void findReferencedClassNames(final Set referencedClassNames) { - getMethodInfo().findReferencedClassNames(referencedClassNames); - getFieldInfo().findReferencedClassNames(referencedClassNames); - getAnnotationInfo().findReferencedClassNames(referencedClassNames); + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + // Add this class to the set of references + super.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + if (this.referencedClassNames != null) { + for (final String refdClassName : this.referencedClassNames) { + final ClassInfo classInfo = ClassInfo.getOrCreateClassInfo(refdClassName, classNameToClassInfo); + classInfo.setScanResult(scanResult); + refdClassInfo.add(classInfo); + } + } + getMethodInfo().findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + getFieldInfo().findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + getAnnotationInfo().findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); if (annotationDefaultParamValues != null) { - annotationDefaultParamValues.findReferencedClassNames(referencedClassNames); + annotationDefaultParamValues.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } - final ClassTypeSignature classSig = getTypeSignature(); - if (classSig != null) { - classSig.findReferencedClassNames(referencedClassNames); + try { + final ClassTypeSignature classSig = getTypeSignature(); + if (classSig != null) { + classSig.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + } + } catch (final IllegalArgumentException e) { + if (log != null) { + log.log("Illegal type signature for class " + getClassName() + ": " + getTypeSignatureStr()); + } } - // Remove any self-references - referencedClassNames.remove(name); } // ------------------------------------------------------------------------------------------------------------- @@ -2628,8 +3371,8 @@ void setReferencedClasses(final ClassInfoList refdClasses) { * @return A {@link ClassInfoList} of {@link ClassInfo} objects for all classes referenced by this class. Note * that you need to call {@link ClassGraph#enableInterClassDependencies()} before * {@link ClassGraph#scan()} for this method to work. You should also call - * {@link ClassGraph#enableExternalClasses()} before {@link ClassGraph#scan()} if you want - * non-whitelisted classes to appear in the result. + * {@link ClassGraph#enableExternalClasses()} before {@link ClassGraph#scan()} if you want non-accepted + * classes to appear in the result. */ public ClassInfoList getClassDependencies() { if (!scanResult.scanSpec.enableInterClassDependencies) { @@ -2662,13 +3405,9 @@ public int compareTo(final ClassInfo o) { */ @Override public boolean equals(final Object obj) { - if (this == obj) { + if (obj == this) { return true; - } - if (obj == null) { - return false; - } - if (this.getClass() != obj.getClass()) { + } else if (!(obj instanceof ClassInfo)) { return false; } final ClassInfo other = (ClassInfo) obj; @@ -2685,59 +3424,87 @@ public int hashCode() { return name == null ? 0 : name.hashCode(); } + // ------------------------------------------------------------------------------------------------------------- + /** * To string. * - * @param typeNameOnly - * if true, convert type name to string only. - * @return the string + * @param useSimpleNames + * use simple names + * @param buf + * the buf */ - private String toString(final boolean typeNameOnly) { - final ClassTypeSignature typeSig = getTypeSignature(); + @Override + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + final boolean initialBufEmpty = buf.length() == 0; + if (annotationInfo != null) { + for (final AnnotationInfo annotation : annotationInfo) { + if (buf.length() > 0 && buf.charAt(buf.length() - 1) != ' ' + && buf.charAt(buf.length() - 1) != '(') { + buf.append(' '); + } + annotation.toString(useSimpleNames, buf); + } + } + ClassTypeSignature typeSig = null; + try { + typeSig = getTypeSignature(); + } catch (final Exception e) { + // Ignore + } if (typeSig != null) { // Generic classes - return typeSig.toString(name, typeNameOnly, modifiers, isAnnotation, isInterface); + typeSig.toStringInternal(useSimpleNames ? ClassInfo.getSimpleName(name) : name, + /* useSimpleNames = */ false, modifiers, isAnnotation(), isInterface(), + /* annotationsToExclude = */ annotationInfo, buf); } else { // Non-generic classes - final StringBuilder buf = new StringBuilder(); - if (typeNameOnly) { - buf.append(name); - } else { - TypeUtils.modifiersToString(modifiers, ModifierType.CLASS, /* ignored */ false, buf); - if (buf.length() > 0) { - buf.append(' '); - } - buf.append(isAnnotation ? "@interface " - : isInterface ? "interface " : (modifiers & 0x4000) != 0 ? "enum " : "class "); - buf.append(name); - final ClassInfo superclass = getSuperclass(); - if (superclass != null && !superclass.getName().equals("java.lang.Object")) { - buf.append(" extends ").append(superclass.toString(/* typeNameOnly = */ true)); + TypeUtils.modifiersToString(modifiers, ModifierType.CLASS, /* ignored */ false, buf); + if (buf.length() > 0 && buf.charAt(buf.length() - 1) != ' ' && buf.charAt(buf.length() - 1) != '(') { + buf.append(' '); + } + // Don't put class type in extends/implements clauses + if (initialBufEmpty) { + buf.append(isRecord() ? "record " // + : isEnum() ? "enum " // + : isAnnotation() ? "@interface " // + : isInterface() ? "interface " // + : "class "); + } + buf.append(useSimpleNames ? ClassInfo.getSimpleName(name) : name); + if (isRecord) { + // Add params, if this is a record class + buf.append('('); + boolean isFirstParam = true; + for (final FieldInfo fieldInfo : getFieldInfo()) { + if (!isFirstParam) { + buf.append(", "); + } else { + isFirstParam = false; + } + fieldInfo.toString(/* useModifiers = */ false, /* useSimpleNames = */ false, buf); } - final Set interfaces = this.filterClassInfo(RelType.IMPLEMENTED_INTERFACES, - /* strictWhitelist = */ false).directlyRelatedClasses; - if (!interfaces.isEmpty()) { - buf.append(isInterface ? " extends " : " implements "); - boolean first = true; - for (final ClassInfo iface : interfaces) { - if (first) { - first = false; - } else { - buf.append(", "); - } - buf.append(iface.toString(/* typeNameOnly = */ true)); + buf.append(')'); + } + final ClassInfo superclass = getSuperclass(); + if (superclass != null && !superclass.getName().equals("java.lang.Object")) { + buf.append(" extends "); + superclass.toString(useSimpleNames, buf); + } + final Set interfaces = this.filterClassInfo(RelType.IMPLEMENTED_INTERFACES, + /* strictAccept = */ false).directlyRelatedClasses; + if (!interfaces.isEmpty()) { + buf.append(isInterface() ? " extends " : " implements "); + boolean first = true; + for (final ClassInfo iface : interfaces) { + if (first) { + first = false; + } else { + buf.append(", "); } + iface.toString(useSimpleNames, buf); } } - return buf.toString(); } } - - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return toString(false); - } } diff --git a/src/main/java/io/github/classgraph/ClassInfoList.java b/src/main/java/io/github/classgraph/ClassInfoList.java index 316cd9584..794af1659 100644 --- a/src/main/java/io/github/classgraph/ClassInfoList.java +++ b/src/main/java/io/github/classgraph/ClassInfoList.java @@ -41,19 +41,22 @@ import java.util.Set; import io.github.classgraph.ClassInfo.ReachableAndDirectlyRelatedClasses; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.CollectionUtils; /** - * A list of {@link ClassInfo} objects, which stores both reachable classes (obtained through a given class - * relationship, either by direct relationship or through an indirect path), and directly related classes (classes - * reachable through a direct relationship only). + * A uniquified (deduplicated) list of {@link ClassInfo} objects, which stores both reachable classes + * (obtained through a given class relationship, either by direct relationship or through an indirect path), and + * directly related classes (classes reachable through a direct relationship only). (By default, accessing a + * {@link ClassInfoList} as a {@link List} returns only reachable classes; by calling {@link #directOnly()}, you can + * get the directly related classes.) * *

- * By default, this list returns reachable classes. By calling {@link #directOnly()}, you can get the directly - * related classes. + * Most {@link ClassInfoList} objects returned by ClassGraph are sorted into lexicographical order by the value of + * {@link ClassInfo#getName()}. One exception to this is the classes returned by + * {@link ClassInfo#getSuperclasses()}, which are in ascending order of the class hierarchy. */ public class ClassInfoList extends MappableInfoList { - /** Directly related classes. */ // N.B. this is marked transient to keep Scrutinizer happy, since thi class extends ArrayList, which is // Serializable, so all fields must be serializable (and Set is an interface, so is not Serializable). @@ -64,9 +67,27 @@ public class ClassInfoList extends MappableInfoList { /** Whether to sort by name. */ private final boolean sortByName; + /** serialVersionUID */ + private static final long serialVersionUID = 1L; + + /** An unmodifiable empty {@link ClassInfoList}. */ + static final ClassInfoList EMPTY_LIST = new ClassInfoList(); + static { + EMPTY_LIST.makeUnmodifiable(); + } + + /** + * Return an unmodifiable empty {@link ClassInfoList}. + * + * @return the unmodifiable empty {@link ClassInfoList}. + */ + public static ClassInfoList emptyList() { + return EMPTY_LIST; + } + /** - * Construct a list of {@link ClassInfo} objects, consisting of reachable classes (obtained through the - * transitive closure) and directly related classes (one step away in the graph). + * Construct a modifiable list of {@link ClassInfo} objects, consisting of reachable classes (obtained through + * the transitive closure) and directly related classes (one step away in the graph). * * @param reachableClasses * reachable classes @@ -80,16 +101,16 @@ public class ClassInfoList extends MappableInfoList { super(reachableClasses); this.sortByName = sortByName; if (sortByName) { - // It's a bit dicey calling Collections.sort(this) from within a constructor, but the super-constructor - // has been called, so it should be fine :-) - Collections.sort(this); + // It's a bit dicey calling CollectionUtils.sortIfNotEmpty(this) from within a constructor, + // but the super-constructor has been called, so it should be fine :-) + CollectionUtils.sortIfNotEmpty(this); } // If directlyRelatedClasses was not provided, then assume all reachable classes were directly related this.directlyRelatedClasses = directlyRelatedClasses == null ? reachableClasses : directlyRelatedClasses; } /** - * Construct a list of {@link ClassInfo} objects. + * Construct a modifiable list of {@link ClassInfo} objects. * * @param reachableAndDirectlyRelatedClasses * reachable and directly related classes @@ -103,7 +124,7 @@ public class ClassInfoList extends MappableInfoList { } /** - * Construct a list of {@link ClassInfo} objects, where each class is directly related. + * Construct a modifiable list of {@link ClassInfo} objects, where each class is directly related. * * @param reachableClasses * reachable classes @@ -111,70 +132,47 @@ public class ClassInfoList extends MappableInfoList { * whether to sort by name */ ClassInfoList(final Set reachableClasses, final boolean sortByName) { - this(reachableClasses, null, sortByName); + this(reachableClasses, /* directlyRelatedClasses = */ null, sortByName); } /** - * Constructor. + * Construct a new empty modifiable list of {@link ClassInfo} objects. */ - private ClassInfoList() { + public ClassInfoList() { super(1); this.sortByName = false; - directlyRelatedClasses = Collections.emptySet(); + directlyRelatedClasses = new HashSet<>(2); } - /** An unmodifiable empty {@link ClassInfoList}. */ - static final ClassInfoList EMPTY_LIST = new ClassInfoList() { - @Override - public boolean add(final ClassInfo e) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void add(final int index, final ClassInfo element) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean remove(final Object o) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public ClassInfo remove(final int index) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final int index, final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean removeAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean retainAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void clear() { - throw new IllegalArgumentException("List is immutable"); - } + /** + * Construct a new empty modifiable list of {@link ClassInfo} objects, given a size hint. + * + * @param sizeHint + * the size hint. + */ + public ClassInfoList(final int sizeHint) { + super(sizeHint); + this.sortByName = false; + directlyRelatedClasses = new HashSet<>(2); + } - @Override - public ClassInfo set(final int index, final ClassInfo element) { - throw new IllegalArgumentException("List is immutable"); - } - }; + /** + * Construct a new modifiable empty {@link ClassInfoList}, given an initial list of {@link ClassInfo} objects. + * + *

+ * If the passed {@link Collection} is not a {@link Set}, then the {@link ClassInfo} objects will be uniquified + * (by adding them to a set) before they are added to the returned list. {@link ClassInfo} objects in the + * returned list will be sorted by name. + * + * @param classInfoCollection + * the initial collection of {@link ClassInfo} objects to add to the {@link ClassInfoList}. + */ + public ClassInfoList(final Collection classInfoCollection) { + this(classInfoCollection instanceof Set // + ? (Set) classInfoCollection + : new HashSet<>(classInfoCollection), // + /* directlyRelatedClasses = */ null, /* sortByName = */ true); + } // ------------------------------------------------------------------------------------------------------------- @@ -497,6 +495,20 @@ public boolean accept(final ClassInfo ci) { }); } + /** + * Filter this {@link ClassInfoList} to include only {@code record} classes. + * + * @return The filtered list, containing only {@code record} classes. + */ + public ClassInfoList getRecords() { + return filter(new ClassInfoFilter() { + @Override + public boolean accept(final ClassInfo ci) { + return ci.isRecord(); + } + }); + } + /** * Filter this {@link ClassInfoList} to include only classes that are assignable to the requested class, * assignableToClass (i.e. where assignableToClass is a superclass or implemented interface of the list @@ -547,7 +559,7 @@ public boolean accept(final ClassInfo ci) { * The GraphViz layout width in inches. * @param includeExternalClasses * If true, and if {@link ClassGraph#enableExternalClasses()} was called before scanning, show - * "external classes" (non-whitelisted classes) within the dependency graph. + * "external classes" (non-accepted classes) within the dependency graph. * @return the GraphViz file contents. * @throws IllegalArgumentException * if this {@link ClassInfoList} is empty or {@link ClassGraph#enableInterClassDependencies()} was @@ -578,12 +590,45 @@ public String generateGraphVizDotFileFromInterClassDependencies(final float size * parameters of (10.5f, 8f, scanSpec.enableExternalClasses), where scanSpec.enableExternalClasses is true if * {@link ClassGraph#enableExternalClasses()} was called before scanning. * + * @param sizeX + * The GraphViz layout width in inches. + * @param sizeY + * The GraphViz layout width in inches. * @return the GraphViz file contents. * @throws IllegalArgumentException * if this {@link ClassInfoList} is empty or {@link ClassGraph#enableInterClassDependencies()} was * not called before scanning (since there would be nothing to graph). */ - public String generateGraphVizDotFileFromClassDependencies() { + public String generateGraphVizDotFileFromInterClassDependencies(final float sizeX, final float sizeY) { + if (isEmpty()) { + throw new IllegalArgumentException("List is empty"); + } + final ScanSpec scanSpec = get(0).scanResult.scanSpec; + if (!scanSpec.enableInterClassDependencies) { + throw new IllegalArgumentException( + "Please call ClassGraph#enableInterClassDependencies() before #scan()"); + } + return GraphvizDotfileGenerator.generateGraphVizDotFileFromInterClassDependencies(this, sizeX, sizeY, + scanSpec.enableExternalClasses); + } + + /** + * Generate a .dot file which can be fed into GraphViz for layout and visualization of the class graph. The + * returned graph shows inter-class dependencies only. The sizeX and sizeY parameters are the image output size + * to use (in inches) when GraphViz is asked to render the .dot file. You must have called + * {@link ClassGraph#enableInterClassDependencies()} before scanning to use this method. + * + *

+ * Equivalent to calling {@link #generateGraphVizDotFileFromInterClassDependencies(float, float, boolean)} with + * parameters of (10.5f, 8f, scanSpec.enableExternalClasses), where scanSpec.enableExternalClasses is true if + * {@link ClassGraph#enableExternalClasses()} was called before scanning. + * + * @return the GraphViz file contents. + * @throws IllegalArgumentException + * if this {@link ClassInfoList} is empty or {@link ClassGraph#enableInterClassDependencies()} was + * not called before scanning (since there would be nothing to graph). + */ + public String generateGraphVizDotFileFromInterClassDependencies() { if (isEmpty()) { throw new IllegalArgumentException("List is empty"); } @@ -596,6 +641,20 @@ public String generateGraphVizDotFileFromClassDependencies() { scanSpec.enableExternalClasses); } + /** + * Deprecated: use {@link #generateGraphVizDotFileFromInterClassDependencies()} instead. + * + * @deprecated Use {@link #generateGraphVizDotFileFromInterClassDependencies()} instead. + * @return the GraphViz file contents. + * @throws IllegalArgumentException + * if this {@link ClassInfoList} is empty or {@link ClassGraph#enableInterClassDependencies()} was + * not called before scanning (since there would be nothing to graph). + */ + @Deprecated + public String generateGraphVizDotFileFromClassDependencies() { + return generateGraphVizDotFileFromInterClassDependencies(); + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -791,14 +850,13 @@ public void generateGraphVizDotFile(final File file) throws IOException { * @see java.util.ArrayList#equals(java.lang.Object) */ @Override - public boolean equals(final Object o) { - if (this == o) { + public boolean equals(final Object obj) { + if (this == obj) { return true; - } - if (!(o instanceof ClassInfoList)) { + } else if (!(obj instanceof ClassInfoList)) { return false; } - final ClassInfoList other = (ClassInfoList) o; + final ClassInfoList other = (ClassInfoList) obj; if ((directlyRelatedClasses == null) != (other.directlyRelatedClasses == null)) { return false; } diff --git a/src/main/java/io/github/classgraph/ClassMemberInfo.java b/src/main/java/io/github/classgraph/ClassMemberInfo.java new file mode 100644 index 000000000..c0d71ab1c --- /dev/null +++ b/src/main/java/io/github/classgraph/ClassMemberInfo.java @@ -0,0 +1,381 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.Modifier; + +import nonapi.io.github.classgraph.utils.Assert; + +/** + * Holds metadata about class members of a class encountered during a scan. All values are taken directly out of the + * classfile for the class. + */ +public abstract class ClassMemberInfo extends ScanResultObject implements HasName { + /** Defining class name. */ + protected String declaringClassName; + + /** The name of the class member. */ + protected String name; + + /** Class member modifiers. */ + protected int modifiers; + + /** + * The JVM-internal type descriptor (missing type parameters, but including types for synthetic and mandated + * class member parameters). + */ + protected String typeDescriptorStr; + + /** + * The type signature (may have type parameter information included, if present and available). Class member + * parameter types are unaligned. + */ + protected String typeSignatureStr; + + /** The annotation on the class member, if any. */ + protected AnnotationInfoList annotationInfo; + + /** The annotation infos, once they are loaded */ + private AnnotationInfoList annotationInfoRef; + + /** Default constructor for deserialization. */ + ClassMemberInfo() { + super(); + } + + /** + * Constructor. + * + * @param definingClassName + * The class the member is defined within. + * @param memberName + * The name of the class member. + * @param modifiers + * The class member modifiers. + * @param typeDescriptorStr + * The class member type descriptor. + * @param typeSignatureStr + * The class member type signature. + * @param annotationInfo + * {@link AnnotationInfo} for any annotations on the class member. + */ + public ClassMemberInfo(final String definingClassName, final String memberName, final int modifiers, + final String typeDescriptorStr, final String typeSignatureStr, + final AnnotationInfoList annotationInfo) { + super(); + this.declaringClassName = definingClassName; + this.name = memberName; + this.modifiers = modifiers; + this.typeDescriptorStr = typeDescriptorStr; + this.typeSignatureStr = typeSignatureStr; + this.annotationInfo = annotationInfo == null || annotationInfo.isEmpty() ? null : annotationInfo; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get the {@link ClassInfo} object for the class that declares this class member. + * + * @return The {@link ClassInfo} object for the declaring class. + * + * @see #getClassName() + */ + @Override + public ClassInfo getClassInfo() { + return super.getClassInfo(); + } + + /** + * Get the name of the class that declares this member. + * + * @return The name of the declaring class. + * + * @see #getClassInfo() + */ + @Override + public String getClassName() { + return declaringClassName; + } + + /** + * Get the name of the class member. + * + * @return The name of the class member. + */ + @Override + public String getName() { + return name; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Returns the modifier bits for the class member. + * + * @return The modifier bits for the class member. + */ + public int getModifiers() { + return modifiers; + } + + /** + * Get the modifiers as a string, e.g. "public static final". For the modifier bits, call getModifiers(). + * + * @return The modifiers modifiers, as a string. + */ + public abstract String getModifiersStr(); + + /** + * Returns true if this class member is public. + * + * @return True if the class member is public. + */ + public boolean isPublic() { + return Modifier.isPublic(modifiers); + } + + /** + * Returns true if this class member is private. + * + * @return True if the class member is private. + */ + public boolean isPrivate() { + return Modifier.isPrivate(modifiers); + } + + /** + * Returns true if this class member is protected. + * + * @return True if the class member is protected. + */ + public boolean isProtected() { + return Modifier.isProtected(modifiers); + } + + /** + * Returns true if this class member is static. + * + * @return True if the class member is static. + */ + public boolean isStatic() { + return Modifier.isStatic(modifiers); + } + + /** + * Returns true if this class member is final. + * + * @return True if the class member is final. + */ + public boolean isFinal() { + return Modifier.isFinal(modifiers); + } + + /** + * Returns true if this class member is synthetic. + * + * @return True if the class member is synthetic. + */ + public boolean isSynthetic() { + return (modifiers & 0x1000) != 0; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Returns the parsed type descriptor for the class member, which will not include type parameters. If you need + * generic type parameters, call {@link #getTypeSignature()} instead. + * + * @return The parsed type descriptor string for the class member. + */ + public abstract HierarchicalTypeSignature getTypeDescriptor(); + + /** + * Returns the type descriptor string for the class member, which will not include type parameters. If you need + * generic type parameters, call {@link #getTypeSignatureStr()} instead. + * + * @return The type descriptor string for the class member. + */ + public String getTypeDescriptorStr() { + return typeDescriptorStr; + } + + /** + * Returns the parsed type signature for the class member, possibly including type parameters. If this returns + * null, that no type signature information is available for this class member, call + * {@link #getTypeDescriptor()} instead. + * + * @return The parsed type signature for the class member, or null if not available. + * @throws IllegalArgumentException + * if the class member type signature cannot be parsed (this should only be thrown in the case of + * classfile corruption, or a compiler bug that causes an invalid type signature to be written to + * the classfile). + */ + public abstract HierarchicalTypeSignature getTypeSignature(); + + /** + * Returns the type signature string for the class member, possibly including type parameters. If this returns + * null, indicating that no type signature information is available for this class member, call + * {@link #getTypeDescriptorStr()} instead. + * + * @return The type signature string for the class member, or null if not available. + */ + public String getTypeSignatureStr() { + return typeSignatureStr; + } + + /** + * Returns the type signature for the class member, possibly including type parameters. If the type signature is + * null, indicating that no type signature information is available for this class member, returns the type + * descriptor instead. + * + * @return The parsed type signature for the class member, or if not available, the parsed type descriptor for + * the class member. + */ + public abstract HierarchicalTypeSignature getTypeSignatureOrTypeDescriptor(); + + /** + * Returns the type signature string for the class member, possibly including type parameters. If the type + * signature string is null, indicating that no type signature information is available for this class member, + * returns the type descriptor string instead. + * + * @return The type signature string for the class member, or if not available, the type descriptor string for + * the class member. + */ + public String getTypeSignatureOrTypeDescriptorStr() { + if (typeSignatureStr != null) { + return typeSignatureStr; + } + return typeDescriptorStr; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get a list of annotations on this class member, along with any annotation parameter values, wrapped in + * {@link AnnotationInfo} objects. + * + * @return A list of annotations on this class member, along with any annotation parameter values, wrapped in + * {@link AnnotationInfo} objects, or the empty list if none. + */ + public AnnotationInfoList getAnnotationInfo() { + synchronized (this) { + if (annotationInfoRef != null) { + return annotationInfoRef; + } + + if (!scanResult.scanSpec.enableAnnotationInfo) { + throw new IllegalArgumentException("Please call ClassGraph#enableAnnotationInfo() before #scan()"); + } + + annotationInfoRef = annotationInfo == null ? AnnotationInfoList.EMPTY_LIST + : AnnotationInfoList.getIndirectAnnotations(annotationInfo, /* annotatedClass = */ null); + return annotationInfoRef; + } + } + + /** + * Get a the non-{@link Repeatable} annotation on this class member, or null if the class member does not have + * the annotation. (Use {@link #getAnnotationInfoRepeatable(Class)} for {@link Repeatable} annotations.) + * + * @param annotation + * The annotation. + * @return An {@link AnnotationInfo} object representing the annotation on this class member, or null if the + * class member does not have the annotation. + */ + public AnnotationInfo getAnnotationInfo(final Class annotation) { + Assert.isAnnotation(annotation); + return getAnnotationInfo(annotation.getName()); + } + + /** + * Get a the named non-{@link Repeatable} annotation on this class member, or null if the class member does not + * have the named annotation. (Use {@link #getAnnotationInfoRepeatable(String)} for {@link Repeatable} + * annotations.) + * + * @param annotationName + * The annotation name. + * @return An {@link AnnotationInfo} object representing the named annotation on this class member, or null if + * the class member does not have the named annotation. + */ + public AnnotationInfo getAnnotationInfo(final String annotationName) { + return getAnnotationInfo().get(annotationName); + } + + /** + * Get a the {@link Repeatable} annotation on this class member, or the empty list if the class member does not + * have the annotation. + * + * @param annotation + * The annotation. + * @return An {@link AnnotationInfoList} of all instances of the annotation on this class member, or the empty + * list if the class member does not have the annotation. + */ + public AnnotationInfoList getAnnotationInfoRepeatable(final Class annotation) { + Assert.isAnnotation(annotation); + return getAnnotationInfoRepeatable(annotation.getName()); + } + + /** + * Get a the named {@link Repeatable} annotation on this class member, or the empty list if the class member + * does not have the named annotation. + * + * @param annotationName + * The annotation name. + * @return An {@link AnnotationInfoList} of all instances of the named annotation on this class member, or the + * empty list if the class member does not have the named annotation. + */ + public AnnotationInfoList getAnnotationInfoRepeatable(final String annotationName) { + return getAnnotationInfo().getRepeatable(annotationName); + } + + /** + * Check if the class member has a given annotation. + * + * @param annotation + * The annotation. + * @return true if this class member has the annotation. + */ + public boolean hasAnnotation(final Class annotation) { + Assert.isAnnotation(annotation); + return hasAnnotation(annotation.getName()); + } + + /** + * Check if the class member has a given named annotation. + * + * @param annotationName + * The name of an annotation. + * @return true if this class member has the named annotation. + */ + public boolean hasAnnotation(final String annotationName) { + return getAnnotationInfo().containsName(annotationName); + } +} diff --git a/src/main/java/io/github/classgraph/ClassRefTypeSignature.java b/src/main/java/io/github/classgraph/ClassRefTypeSignature.java index 620b75000..081d36f45 100644 --- a/src/main/java/io/github/classgraph/ClassRefTypeSignature.java +++ b/src/main/java/io/github/classgraph/ClassRefTypeSignature.java @@ -31,8 +31,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; +import io.github.classgraph.Classfile.TypePathNode; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.Parser; import nonapi.io.github.classgraph.types.TypeUtils; @@ -42,21 +44,18 @@ public final class ClassRefTypeSignature extends ClassRefOrTypeVariableSignature /** The class name. */ final String className; - /** The class name and suffixes, without type arguments. */ - private String fullyQualifiedClassName; - /** The class type arguments. */ private final List typeArguments; - /** The class type signature suffix(es), or the empty list if no suffixes. */ + /** Type suffixes. */ private final List suffixes; - /** - * The suffix type arguments, one per suffix, or the empty list if no suffixes. The element value will be the - * empty list if there is no type argument for a given suffix. - */ + /** The suffix type arguments. */ private final List> suffixTypeArguments; + /** The suffix type annotations. */ + private List suffixTypeAnnotations; + // ------------------------------------------------------------------------------------------------------------- /** @@ -83,9 +82,10 @@ private ClassRefTypeSignature(final String className, final List t // ------------------------------------------------------------------------------------------------------------- /** - * Get the name of the base class. + * Get the name of the class, without any suffixes. * - * @return The name of the base class. + * @see #getFullyQualifiedClassName() + * @return The name of the class. */ public String getBaseClassName() { return className; @@ -105,16 +105,17 @@ public String getBaseClassName() { * @return The fully-qualified name of the class, including suffixes but without type arguments. */ public String getFullyQualifiedClassName() { - if (fullyQualifiedClassName == null) { + if (suffixes.isEmpty()) { + return className; + } else { final StringBuilder buf = new StringBuilder(); buf.append(className); for (final String suffix : suffixes) { buf.append('$'); buf.append(suffix); } - fullyQualifiedClassName = buf.toString(); + return buf.toString(); } - return fullyQualifiedClassName; } /** @@ -127,23 +128,126 @@ public List getTypeArguments() { } /** - * Get any suffixes of the class (typically nested inner class names). + * Get all nested suffixes of the class (typically nested inner class names). * - * @return The class suffixes (for inner classes). + * @return The class suffixes (for inner classes), or the empty list if none. */ public List getSuffixes() { return suffixes; } /** - * Get any type arguments for any suffixes of the class, one list per suffix. + * Get a list of type arguments for all nested suffixes of the class, one list per suffix. * - * @return The type arguments for the inner classes, one list per suffix. + * @return The list of type arguments for the suffixes (nested inner classes), one list per suffix, or the empty + * list if none. */ public List> getSuffixTypeArguments() { return suffixTypeArguments; } + /** + * Get a list of lists of type annotations for all nested suffixes of the class, one list per suffix. + * + * @return The list of lists of type annotations for the suffixes (nested inner classes), one list per suffix, + * or null if none. + */ + public List getSuffixTypeAnnotationInfo() { + return suffixTypeAnnotations; + } + + private void addSuffixTypeAnnotation(final int suffixIdx, final AnnotationInfo annotationInfo) { + if (suffixTypeAnnotations == null) { + suffixTypeAnnotations = new ArrayList<>(suffixes.size()); + for (int i = 0; i < suffixes.size(); i++) { + suffixTypeAnnotations.add(new AnnotationInfoList(1)); + } + } + suffixTypeAnnotations.get(suffixIdx).add(annotationInfo); + } + + @Override + protected void addTypeAnnotation(final List typePath, final AnnotationInfo annotationInfo) { + // Find how many deeper nested levels to descend to + int numDeeperNestedLevels = 0; + int nextTypeArgIdx = -1; + for (final TypePathNode typePathNode : typePath) { + if (typePathNode.typePathKind == 1) { + // Annotation is deeper in a nested type + // (can handle this iteratively) + numDeeperNestedLevels++; + } else if (typePathNode.typePathKind == 3) { + // Annotation is on a type argument of a parameterized type + // (need to handle this recursively) + nextTypeArgIdx = typePathNode.typeArgumentIdx; + break; + } else { + // Not valid here: + // 0 => Annotation is deeper in an array type + // 2 => Annotation is on the bound of a wildcard type argument of a parameterized type + throw new IllegalArgumentException("Bad typePathKind: " + typePathNode.typePathKind); + } + } + + // Figure out whether to index the base type or a suffix, skipping over non-nested class pairs + int suffixIdx = -1; + int nestingLevel = -1; + String typePrefix = className; + for (;;) { + boolean skipSuffix; + if (suffixIdx >= suffixes.size()) { + throw new IllegalArgumentException("Ran out of nested types while trying to add type annotation"); + } else if (suffixIdx == suffixes.size() - 1) { + // The suffix to the right cannot be static, because there are no suffixes to the right, + // so this suffix doesn't need to be skipped + skipSuffix = false; + } else { + // For suffix path X.Y, classes are not nested if Y is static + final ClassInfo outerClassInfo = scanResult.getClassInfo(typePrefix); + typePrefix = typePrefix + '$' + suffixes.get(suffixIdx + 1); + final ClassInfo innerClassInfo = scanResult.getClassInfo(typePrefix); + skipSuffix = outerClassInfo == null || innerClassInfo == null + || outerClassInfo.isInterfaceOrAnnotation() // + || innerClassInfo.isInterfaceOrAnnotation() // + || innerClassInfo.isStatic() // + || !outerClassInfo.getInnerClasses().contains(innerClassInfo); + } + if (!skipSuffix) { + // Found nested classes + nestingLevel++; + if (nestingLevel >= numDeeperNestedLevels) { + break; + } + } + suffixIdx++; + } + + if (nextTypeArgIdx == -1) { + // Reached end of path -- add type annotation + if (suffixIdx == -1) { + // Add type annotation to base type + addTypeAnnotation(annotationInfo); + } else { + // Add type annotation to suffix type + addSuffixTypeAnnotation(suffixIdx, annotationInfo); + } + } else { + final List typeArgumentList = suffixIdx == -1 ? typeArguments + : suffixTypeArguments.get(suffixIdx); + // For type descriptors (as opposed to type signatures), typeArguments is the empty list, + // so need to bounds-check nextTypeArgIdx + if (nextTypeArgIdx < typeArgumentList.size()) { + // type_path_kind == 3 can be followed by type_path_kind == 2, for an annotation on the + // bound of a nested type, and this has to be handled recursively on the remaining + // part of the type path + final List remainingTypePath = typePath.subList(numDeeperNestedLevels + 1, + typePath.size()); + // Add type annotation to type argument + typeArgumentList.get(nextTypeArgIdx).addTypeAnnotation(remainingTypePath, annotationInfo); + } + } + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -176,11 +280,7 @@ public Class loadClass() { // ------------------------------------------------------------------------------------------------------------- - /** - * Get the fully qualified class name (used by {@link #getClassInfo()} and {@link #loadClass()}. - * - * @return The fully qualified name of the class. - */ + /** @return the fully-qualified class name, for classloading. */ @Override protected String getClassName() { return getFullyQualifiedClassName(); @@ -205,29 +305,32 @@ public ClassInfo getClassInfo() { @Override void setScanResult(final ScanResult scanResult) { super.setScanResult(scanResult); - if (typeArguments != null) { - for (final TypeArgument typeArgument : typeArguments) { - typeArgument.setScanResult(scanResult); - } + for (final TypeArgument typeArgument : typeArguments) { + typeArgument.setScanResult(scanResult); } - if (suffixTypeArguments != null) { - for (final List list : suffixTypeArguments) { - for (final TypeArgument typeArgument : list) { - typeArgument.setScanResult(scanResult); - } + for (final List typeArgumentList : suffixTypeArguments) { + for (final TypeArgument typeArgument : typeArgumentList) { + typeArgument.setScanResult(scanResult); } } } - /* (non-Javadoc) - * @see io.github.classgraph.HierarchicalTypeSignature#findReferencedClassNames(java.util.Set) + /** + * Get the names of any classes referenced in the type signature. + * + * @param refdClassNames + * the referenced class names. */ @Override - void findReferencedClassNames(final Set classNameListOut) { - classNameListOut.add(className); - classNameListOut.add(getFullyQualifiedClassName()); + protected void findReferencedClassNames(final Set refdClassNames) { + refdClassNames.add(getFullyQualifiedClassName()); for (final TypeArgument typeArgument : typeArguments) { - typeArgument.findReferencedClassNames(classNameListOut); + typeArgument.findReferencedClassNames(refdClassNames); + } + for (final List typeArgumentList : suffixTypeArguments) { + for (final TypeArgument typeArgument : typeArgumentList) { + typeArgument.findReferencedClassNames(refdClassNames); + } } } @@ -238,7 +341,15 @@ void findReferencedClassNames(final Set classNameListOut) { */ @Override public int hashCode() { - return className.hashCode() + 7 * typeArguments.hashCode() + 15 * suffixes.hashCode(); + return className.hashCode() + 7 * typeArguments.hashCode() + 15 * suffixTypeArguments.hashCode() + + 31 * (typeAnnotationInfo == null ? 0 : typeAnnotationInfo.hashCode()) + + 64 * (suffixTypeAnnotations == null ? 0 : suffixTypeAnnotations.hashCode()); + } + + private static boolean suffixesMatch(final ClassRefTypeSignature a, final ClassRefTypeSignature b) { + return a.suffixes.equals(b.suffixes) // + && a.suffixTypeArguments.equals(b.suffixTypeArguments) // + && Objects.equals(a.suffixTypeAnnotations, b.suffixTypeAnnotations); } /* (non-Javadoc) @@ -246,15 +357,14 @@ public int hashCode() { */ @Override public boolean equals(final Object obj) { - if (this == obj) { + if (obj == this) { return true; - } - if (!(obj instanceof ClassRefTypeSignature)) { + } else if (!(obj instanceof ClassRefTypeSignature)) { return false; } final ClassRefTypeSignature o = (ClassRefTypeSignature) obj; return o.className.equals(this.className) && o.typeArguments.equals(this.typeArguments) - && o.suffixes.equals(this.suffixes); + && Objects.equals(this.typeAnnotationInfo, o.typeAnnotationInfo) && suffixesMatch(o, this); } /* (non-Javadoc) @@ -271,58 +381,77 @@ public boolean equalsIgnoringTypeParams(final TypeSignature other) { return false; } final ClassRefTypeSignature o = (ClassRefTypeSignature) other; - if (o.suffixes.equals(this.suffixes)) { - return o.className.equals(this.className); - } else { - return o.getFullyQualifiedClassName().equals(this.getFullyQualifiedClassName()); - } + return o.className.equals(this.className) && Objects.equals(this.typeAnnotationInfo, o.typeAnnotationInfo) + && suffixesMatch(o, this); } - /* (non-Javadoc) - * @see io.github.classgraph.TypeSignature#toStringInternal(boolean) - */ + // ------------------------------------------------------------------------------------------------------------- + @Override - protected String toStringInternal(final boolean useSimpleNames) { - final StringBuilder buf = new StringBuilder(); - // Only append the base class name if not using simple names, or if there are no suffixes - if (!useSimpleNames || suffixes.size() == 0) { + protected void toStringInternal(final boolean useSimpleNames, final AnnotationInfoList annotationsToExclude, + final StringBuilder buf) { + // Only render the base class if not using simple names, or if there are no suffixes + if (!useSimpleNames || suffixes.isEmpty()) { + // Append type annotations + if (typeAnnotationInfo != null) { + for (final AnnotationInfo annotationInfo : typeAnnotationInfo) { + if (annotationsToExclude == null || !annotationsToExclude.contains(annotationInfo)) { + annotationInfo.toString(useSimpleNames, buf); + buf.append(' '); + } + } + } + // Append base class name buf.append(useSimpleNames ? ClassInfo.getSimpleName(className) : className); + // Append base class type arguments if (!typeArguments.isEmpty()) { buf.append('<'); for (int i = 0; i < typeArguments.size(); i++) { if (i > 0) { buf.append(", "); } - buf.append(useSimpleNames ? typeArguments.get(i).toStringWithSimpleNames() - : typeArguments.get(i).toString()); + typeArguments.get(i).toString(useSimpleNames, buf); } buf.append('>'); } } - // Only use the last suffix if using simple names - for (int i = useSimpleNames && suffixes.size() > 0 ? suffixes.size() - 1 : 0; i < suffixes.size(); i++) { - if (!useSimpleNames) { - // Use '.' before each suffix in the toString() representation, since that is - // how the class name will be shown in Java, e.g. OuterClass.InnerClass - buf.append('.'); - } - buf.append(suffixes.get(i)); - final List suffixTypeArgs = suffixTypeArguments.get(i); - if (!suffixTypeArgs.isEmpty()) { - buf.append('<'); - for (int j = 0; j < suffixTypeArgs.size(); j++) { - if (j > 0) { - buf.append(", "); + + // Append suffixes + if (!suffixes.isEmpty()) { + for (int i = useSimpleNames ? suffixes.size() - 1 : 0; i < suffixes.size(); i++) { + if (!useSimpleNames) { + // Use '$' rather than '.' as separator for suffixes, since that is what Class.getName() does. + buf.append('$'); + } + final AnnotationInfoList typeAnnotations = suffixTypeAnnotations == null ? null + : suffixTypeAnnotations.get(i); + // Append type annotations for this suffix + if (typeAnnotations != null && !typeAnnotations.isEmpty()) { + for (final AnnotationInfo annotationInfo : typeAnnotations) { + annotationInfo.toString(useSimpleNames, buf); + buf.append(' '); } - buf.append(useSimpleNames ? suffixTypeArgs.get(j).toStringWithSimpleNames() - : suffixTypeArgs.get(j).toString()); } - buf.append('>'); + // Append suffix name + buf.append(suffixes.get(i)); + // Append suffix type arguments + final List suffixTypeArgumentsList = suffixTypeArguments.get(i); + if (!suffixTypeArgumentsList.isEmpty()) { + buf.append('<'); + for (int j = 0; j < suffixTypeArgumentsList.size(); j++) { + if (j > 0) { + buf.append(", "); + } + suffixTypeArgumentsList.get(j).toString(useSimpleNames, buf); + } + buf.append('>'); + } } } - return buf.toString(); } + // ------------------------------------------------------------------------------------------------------------- + /** * Parse a class type signature. * @@ -337,24 +466,37 @@ protected String toStringInternal(final boolean useSimpleNames) { static ClassRefTypeSignature parse(final Parser parser, final String definingClassName) throws ParseException { if (parser.peek() == 'L') { parser.next(); - if (!TypeUtils.getIdentifierToken(parser, /* separator = */ '/', /* separatorReplace = */ '.')) { + final int startParserPosition = parser.getPosition(); + if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ true, /* stopAtDot = */ true)) { throw new ParseException(parser, "Could not parse identifier token"); } - final String className = parser.currToken(); + String className = parser.currToken(); final List typeArguments = TypeArgument.parseList(parser, definingClassName); List suffixes; List> suffixTypeArguments; - if (parser.peek() == '.') { + boolean dropSuffixes = false; + if (parser.peek() == '.' || parser.peek() == '$') { suffixes = new ArrayList<>(); suffixTypeArguments = new ArrayList<>(); - while (parser.peek() == '.') { - parser.expect('.'); - if (!TypeUtils.getIdentifierToken(parser, /* separator = */ '/', - /* separatorReplace = */ '.')) { - throw new ParseException(parser, "Could not parse identifier token"); + while (parser.peek() == '.' || parser.peek() == '$') { + parser.advance(1); + if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ true, + /* stopAtDot = */ true)) { + // Got the empty string as the next token after '$', i.e. found an empty suffix. + suffixes.add(""); + suffixTypeArguments.add(Collections. emptyList()); + dropSuffixes = true; + } else { + suffixes.add(parser.currToken()); + suffixTypeArguments.add(TypeArgument.parseList(parser, definingClassName)); } - suffixes.add(parser.currToken()); - suffixTypeArguments.add(TypeArgument.parseList(parser, definingClassName)); + } + if (dropSuffixes) { + // Got an empty suffix -- either "$$", or a class name ending in a '$' (which Scala uses). + // In this case, take the whole class reference as a single class name without suffixes. + className = parser.getSubstring(startParserPosition, parser.getPosition()).replace('/', '.'); + suffixes = Collections.emptyList(); + suffixTypeArguments = Collections.emptyList(); } } else { suffixes = Collections.emptyList(); diff --git a/src/main/java/io/github/classgraph/ClassTypeSignature.java b/src/main/java/io/github/classgraph/ClassTypeSignature.java index 9ce063745..1bfdd8f0c 100644 --- a/src/main/java/io/github/classgraph/ClassTypeSignature.java +++ b/src/main/java/io/github/classgraph/ClassTypeSignature.java @@ -30,14 +30,18 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import io.github.classgraph.Classfile.TypePathNode; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.Parser; import nonapi.io.github.classgraph.types.TypeUtils; import nonapi.io.github.classgraph.types.TypeUtils.ModifierType; -import nonapi.io.github.classgraph.utils.Join; +import nonapi.io.github.classgraph.utils.LogNode; /** A class type signature (called "ClassSignature" in the classfile documentation). */ public final class ClassTypeSignature extends HierarchicalTypeSignature { @@ -54,6 +58,12 @@ public final class ClassTypeSignature extends HierarchicalTypeSignature { /** The superinterface signatures. */ private final List superinterfaceSignatures; + /** + * The throws signatures (usually null). These are only present in Scala classes, if the class is marked up with + * {@code @throws}, and they violate the classfile spec (#495), but we parse them anyway. + */ + private final List throwsSignatures; + // ------------------------------------------------------------------------------------------------------------- /** @@ -67,15 +77,59 @@ public final class ClassTypeSignature extends HierarchicalTypeSignature { * The superclass signature. * @param superinterfaceSignatures * The superinterface signature(s). + * @param throwsSignatures + * the throws signatures (these are actually invalid, but can be added by Scala: #495). Usually null. */ private ClassTypeSignature(final ClassInfo classInfo, final List typeParameters, final ClassRefTypeSignature superclassSignature, - final List superinterfaceSignatures) { + final List superinterfaceSignatures, + final List throwsSignatures) { super(); this.classInfo = classInfo; this.typeParameters = typeParameters; this.superclassSignature = superclassSignature; this.superinterfaceSignatures = superinterfaceSignatures; + this.throwsSignatures = throwsSignatures; + } + + /** + * Constructor used to create synthetic class type descriptor (#662). + * + * @param classInfo + * The class. + * @param superclass + * The superclass. + * @param interfaces + * The implemented interfaces. + */ + ClassTypeSignature(final ClassInfo classInfo, final ClassInfo superclass, final ClassInfoList interfaces) { + super(); + this.classInfo = classInfo; + this.typeParameters = Collections.emptyList(); + ClassRefTypeSignature superclassSignature = null; + try { + superclassSignature = superclass == null ? null + : (ClassRefTypeSignature) TypeSignature + .parse("L" + superclass.getName().replace('.', '/') + ";", classInfo.getName()); + } catch (final ParseException e) { + // Silently fail (should not happen) + } + this.superclassSignature = superclassSignature; + this.superinterfaceSignatures = interfaces == null || interfaces.isEmpty() + ? Collections. emptyList() + : new ArrayList(interfaces.size()); + if (interfaces != null) { + for (final ClassInfo iface : interfaces) { + try { + final ClassRefTypeSignature ifaceSignature = (ClassRefTypeSignature) TypeSignature + .parse("L" + iface.getName().replace('.', '/') + ";", classInfo.getName()); + this.superinterfaceSignatures.add(ifaceSignature); + } catch (final ParseException e) { + // Silently fail (should not happen) + } + } + } + this.throwsSignatures = null; } // ------------------------------------------------------------------------------------------------------------- @@ -108,46 +162,21 @@ public List getSuperinterfaceSignatures() { return superinterfaceSignatures; } - // ------------------------------------------------------------------------------------------------------------- - /** - * Parse a class type signature or class type descriptor. + * Gets the throws signatures. These are invalid according to the classfile spec (so this method is currently + * non-public), but may be added by the Scala compiler. (See bug #495.) * - * @param typeDescriptor - * The class type signature or class type descriptor to parse. - * @param classInfo - * the class info - * @return The parsed class type signature or class type descriptor. - * @throws ParseException - * If the class type signature could not be parsed. + * @return the throws signatures */ - static ClassTypeSignature parse(final String typeDescriptor, final ClassInfo classInfo) throws ParseException { - final Parser parser = new Parser(typeDescriptor); - // The defining class name is used to resolve type variables using the defining class' type descriptor. - // But here we are parsing the defining class' type descriptor, so it can't contain variables that - // point to itself => just use null as the defining class name. - final String definingClassNameNull = null; - final List typeParameters = TypeParameter.parseList(parser, definingClassNameNull); - final ClassRefTypeSignature superclassSignature = ClassRefTypeSignature.parse(parser, - definingClassNameNull); - List superinterfaceSignatures; - if (parser.hasMore()) { - superinterfaceSignatures = new ArrayList<>(); - while (parser.hasMore()) { - final ClassRefTypeSignature superinterfaceSignature = ClassRefTypeSignature.parse(parser, - definingClassNameNull); - if (superinterfaceSignature == null) { - throw new ParseException(parser, "Could not parse superinterface signature"); - } - superinterfaceSignatures.add(superinterfaceSignature); - } - } else { - superinterfaceSignatures = Collections.emptyList(); - } - if (parser.hasMore()) { - throw new ParseException(parser, "Extra characters at end of type descriptor"); - } - return new ClassTypeSignature(classInfo, typeParameters, superclassSignature, superinterfaceSignatures); + List getThrowsSignatures() { + return throwsSignatures; + } + + @Override + protected void addTypeAnnotation(final List typePath, final AnnotationInfo annotationInfo) { + // Individual parts of a class' type each have their own addTypeAnnotation methods + throw new IllegalArgumentException( + "Cannot call this method on " + ClassTypeSignature.class.getSimpleName()); } // ------------------------------------------------------------------------------------------------------------- @@ -189,19 +218,48 @@ void setScanResult(final ScanResult scanResult) { } } - /* (non-Javadoc) - * @see io.github.classgraph.HierarchicalTypeSignature#findReferencedClassNames(java.util.Set) + /** + * Get the names of any classes referenced in the type signature. + * + * @param refdClassNames + * the referenced class names. */ - @Override - void findReferencedClassNames(final Set classNameListOut) { + protected void findReferencedClassNames(final Set refdClassNames) { for (final TypeParameter typeParameter : typeParameters) { - typeParameter.findReferencedClassNames(classNameListOut); + typeParameter.findReferencedClassNames(refdClassNames); } if (superclassSignature != null) { - superclassSignature.findReferencedClassNames(classNameListOut); + superclassSignature.findReferencedClassNames(refdClassNames); + } + if (superinterfaceSignatures != null) { + for (final ClassRefTypeSignature typeSignature : superinterfaceSignatures) { + typeSignature.findReferencedClassNames(refdClassNames); + } } - for (final ClassRefTypeSignature typeSignature : superinterfaceSignatures) { - typeSignature.findReferencedClassNames(classNameListOut); + if (throwsSignatures != null) { + for (final ClassRefOrTypeVariableSignature typeSignature : throwsSignatures) { + typeSignature.findReferencedClassNames(refdClassNames); + } + } + } + + /** + * Get {@link ClassInfo} objects for any classes referenced in the type descriptor or type signature. + * + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info + */ + @Override + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + final Set refdClassNames = new HashSet<>(); + findReferencedClassNames(refdClassNames); + for (final String refdClassName : refdClassNames) { + final ClassInfo clsInfo = ClassInfo.getOrCreateClassInfo(refdClassName, classNameToClassInfo); + clsInfo.scanResult = scanResult; + refdClassInfo.add(clsInfo); } } @@ -212,8 +270,8 @@ void findReferencedClassNames(final Set classNameListOut) { */ @Override public int hashCode() { - return typeParameters.hashCode() + superclassSignature.hashCode() * 7 - + superinterfaceSignatures.hashCode() * 15; + return typeParameters.hashCode() + (superclassSignature == null ? 1 : superclassSignature.hashCode()) * 7 + + (superinterfaceSignatures == null ? 1 : superinterfaceSignatures.hashCode()) * 15; } /* (non-Javadoc) @@ -221,77 +279,186 @@ public int hashCode() { */ @Override public boolean equals(final Object obj) { - if (this == obj) { + if (obj == this) { return true; - } - if (!(obj instanceof ClassTypeSignature)) { + } else if (!(obj instanceof ClassTypeSignature)) { return false; } final ClassTypeSignature o = (ClassTypeSignature) obj; - return o.typeParameters.equals(this.typeParameters) - && o.superclassSignature.equals(this.superclassSignature) - && o.superinterfaceSignatures.equals(this.superinterfaceSignatures); + return Objects.equals(o.typeParameters, this.typeParameters) + && Objects.equals(o.superclassSignature, this.superclassSignature) + && Objects.equals(o.superinterfaceSignatures, this.superinterfaceSignatures); } // ------------------------------------------------------------------------------------------------------------- /** * Render into String form. - * + * * @param className * The class name - * @param typeNameOnly - * If true, only return the type name (and generic type parameters). + * @param useSimpleNames + * the use simple names * @param modifiers * The class modifiers. * @param isAnnotation * True if the class is an annotation. * @param isInterface * True if the class is an interface. - * @return The String representation. + * @param annotationsToExclude + * the annotations to exclude + * @param buf + * the buf */ - String toString(final String className, final boolean typeNameOnly, final int modifiers, - final boolean isAnnotation, final boolean isInterface) { - final StringBuilder buf = new StringBuilder(); - if (!typeNameOnly) { - if (modifiers != 0) { - TypeUtils.modifiersToString(modifiers, ModifierType.CLASS, /* ignored */ false, buf); + void toStringInternal(final String className, final boolean useSimpleNames, final int modifiers, + final boolean isAnnotation, final boolean isInterface, final AnnotationInfoList annotationsToExclude, + final StringBuilder buf) { + if (throwsSignatures != null) { + for (final ClassRefOrTypeVariableSignature throwsSignature : throwsSignatures) { + if (buf.length() > 0) { + buf.append(' '); + } + buf.append("@throws(").append(throwsSignature).append(")"); } + } + if (modifiers != 0) { if (buf.length() > 0) { buf.append(' '); } - buf.append(isAnnotation ? "@interface" - : isInterface ? "interface" : (modifiers & 0x4000) != 0 ? "enum" : "class"); + TypeUtils.modifiersToString(modifiers, ModifierType.CLASS, /* ignored */ false, buf); + } + if (buf.length() > 0) { buf.append(' '); } + buf.append(isAnnotation ? "@interface" + : isInterface ? "interface" : (modifiers & 0x4000) != 0 ? "enum" : "class"); + buf.append(' '); if (className != null) { - buf.append(className); + buf.append(useSimpleNames ? ClassInfo.getSimpleName(className) : className); } if (!typeParameters.isEmpty()) { - Join.join(buf, "<", ", ", ">", typeParameters); - } - if (!typeNameOnly) { - if (superclassSignature != null) { - final String superSig = superclassSignature.toString(); - if (!superSig.equals("java.lang.Object")) { - buf.append(" extends "); - buf.append(superSig); + buf.append('<'); + for (int i = 0; i < typeParameters.size(); i++) { + if (i > 0) { + buf.append(", "); } + typeParameters.get(i).toStringInternal(useSimpleNames, null, buf); + } + buf.append('>'); + } + if (superclassSignature != null) { + final String superSig = superclassSignature.toString(useSimpleNames); + // superSig could have a class type annotation even if the superclass is Object + if (!superSig.equals("java.lang.Object") + && !(superSig.equals("Object") && superclassSignature.className.equals("java.lang.Object"))) { + buf.append(" extends "); + buf.append(superSig); } - if (!superinterfaceSignatures.isEmpty()) { - buf.append(isInterface ? " extends " : " implements "); - Join.join(buf, "", ", ", "", superinterfaceSignatures); + } + if (superinterfaceSignatures != null && !superinterfaceSignatures.isEmpty()) { + buf.append(isInterface ? " extends " : " implements "); + for (int i = 0; i < superinterfaceSignatures.size(); i++) { + if (i > 0) { + buf.append(", "); + } + superinterfaceSignatures.get(i).toStringInternal(useSimpleNames, null, buf); } } - return buf.toString(); } - /* (non-Javadoc) - * @see java.lang.Object#toString() + /** + * To string internal. + * + * @param useSimpleNames + * the use simple names + * @param annotationsToExclude + * the annotations to exclude + * @param buf + * the buf */ @Override - public String toString() { - return toString(classInfo.getName(), /* typeNameOnly = */ false, classInfo.getModifiers(), - classInfo.isAnnotation(), classInfo.isInterface()); + protected void toStringInternal(final boolean useSimpleNames, final AnnotationInfoList annotationsToExclude, + final StringBuilder buf) { + toStringInternal(classInfo.getName(), useSimpleNames, classInfo.getModifiers(), classInfo.isAnnotation(), + classInfo.isInterface(), annotationsToExclude, buf); + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Parse a class type signature or class type descriptor. + * + * @param typeDescriptor + * The class type signature or class type descriptor to parse. + * @param classInfo + * the class info + * @return The parsed class type signature or class type descriptor. + * @throws ParseException + * If the class type signature could not be parsed. + */ + static ClassTypeSignature parse(final String typeDescriptor, final ClassInfo classInfo) throws ParseException { + final Parser parser = new Parser(typeDescriptor); + // The defining class name is used to resolve type variables using the defining class' type descriptor. + // But here we are parsing the defining class' type descriptor, so it can't contain variables that + // point to itself => just use null as the defining class name. + final String definingClassNameNull = null; + final List typeParameters = TypeParameter.parseList(parser, definingClassNameNull); + final ClassRefTypeSignature superclassSignature = ClassRefTypeSignature.parse(parser, + definingClassNameNull); + List superinterfaceSignatures; + if (parser.hasMore()) { + superinterfaceSignatures = new ArrayList<>(); + while (parser.hasMore()) { + if (parser.peek() == '^') { + // Illegal "throws" suffix in class type signature -- fall through + break; + } + final ClassRefTypeSignature superinterfaceSignature = ClassRefTypeSignature.parse(parser, + definingClassNameNull); + if (superinterfaceSignature == null) { + throw new ParseException(parser, "Could not parse superinterface signature"); + } + superinterfaceSignatures.add(superinterfaceSignature); + } + } else { + superinterfaceSignatures = Collections.emptyList(); + } + List throwsSignatures; + if (parser.peek() == '^') { + // There is an illegal "throws" suffix at the end of this class type signature. + // Scala adds these if you tag a class with "@throws" (#495). + // Classes with this sort of type signature are rejected by javac and javap, and they will throw + // GenericSignatureFormatError if you call getClass().getGenericSuperclass() on a subclass. + // But the JVM ignores type signatures due to type erasure, and Scala seems to rely on this + // -- or at the very least, the Scala team never noticed the issue, because the classes work + // fine at runtime if you live in a Scala-only world. + // Since this issue is probably widespread in the Scala world, it's probably better to accept + // these invalid type signatures, and actually parse out any "throws" suffixes, rather than + // throwing an exception and refusing to parse the type signature. + throwsSignatures = new ArrayList<>(); + while (parser.peek() == '^') { + parser.expect('^'); + final ClassRefTypeSignature classTypeSignature = ClassRefTypeSignature.parse(parser, + classInfo.getName()); + if (classTypeSignature != null) { + throwsSignatures.add(classTypeSignature); + } else { + final TypeVariableSignature typeVariableSignature = TypeVariableSignature.parse(parser, + classInfo.getName()); + if (typeVariableSignature != null) { + throwsSignatures.add(typeVariableSignature); + } else { + throw new ParseException(parser, "Missing type variable signature"); + } + } + } + } else { + throwsSignatures = null; + } + if (parser.hasMore()) { + throw new ParseException(parser, "Extra characters at end of type descriptor"); + } + return new ClassTypeSignature(classInfo, typeParameters, superclassSignature, superinterfaceSignatures, + throwsSignatures); } } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/Classfile.java b/src/main/java/io/github/classgraph/Classfile.java index 24ca349b4..ef16f3029 100644 --- a/src/main/java/io/github/classgraph/Classfile.java +++ b/src/main/java/io/github/classgraph/Classfile.java @@ -29,8 +29,8 @@ package io.github.classgraph; import java.io.IOException; +import java.lang.reflect.Array; import java.lang.reflect.Modifier; -import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -38,24 +38,29 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import io.github.classgraph.Scanner.ClassfileScanWorkUnit; -import nonapi.io.github.classgraph.ScanSpec; import nonapi.io.github.classgraph.concurrency.WorkQueue; +import nonapi.io.github.classgraph.fileslice.reader.ClassfileReader; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.types.ParseException; -import nonapi.io.github.classgraph.utils.InputStreamOrByteBufferAdapter; +import nonapi.io.github.classgraph.utils.CollectionUtils; import nonapi.io.github.classgraph.utils.JarUtils; -import nonapi.io.github.classgraph.utils.Join; import nonapi.io.github.classgraph.utils.LogNode; +import nonapi.io.github.classgraph.utils.StringUtils; /** * A classfile binary format parser. Implements its own buffering to avoid the overhead of using DataInputStream. * This class should only be used by a single thread at a time, but can be re-used to scan multiple classfiles in * sequence, to avoid re-allocating buffer memory. + * + *

+ * See the class file format spec. */ class Classfile { - /** The InputStream or ByteBuffer for the current classfile. */ - private InputStreamOrByteBufferAdapter inputStreamOrByteBuffer; + /** The {@link ClassfileReader} for the current classfile. */ + private ClassfileReader reader; /** The classpath element that contains this classfile. */ private final ClasspathElement classpathElement; @@ -69,9 +74,18 @@ class Classfile { /** The classfile resource. */ private final Resource classfileResource; + /** The string intern map. */ + private final ConcurrentHashMap stringInternMap; + /** The name of the class. */ private String className; + /** The minor version of the classfile format. */ + private int minorVersion; + + /** The major version of the classfile format. */ + private int majorVersion; + /** Whether this is an external class. */ private final boolean isExternalClass; @@ -81,10 +95,13 @@ class Classfile { /** Whether this class is an interface. */ private boolean isInterface; + /** Whether this class is a record. */ + private boolean isRecord; + /** Whether this class is an annotation. */ private boolean isAnnotation; - /** The superclass name. (can be null if no superclass, or if superclass is blacklisted.) */ + /** The superclass name. (can be null if no superclass, or if superclass is rejected.) */ private String superclassName; /** The implemented interfaces. */ @@ -97,7 +114,7 @@ class Classfile { private String fullyQualifiedDefiningMethodName; /** Class containment entries. */ - private List> classContainmentEntries; + private List classContainmentEntries; /** Annotation default parameter values. */ private AnnotationParameterValueList annotationParamDefaultValues; @@ -112,13 +129,22 @@ class Classfile { private MethodInfoList methodInfoList; /** The type signature. */ - private String typeSignature; + private String typeSignatureStr; + + /** The source file, such as Classfile.java */ + private String sourceFile; + + /** The type annotation decorators for the {@link ClassTypeSignature} instance. */ + private List classTypeAnnotationDecorators; + + /** The names of accepted classes found in the classpath while scanning paths within classpath elements. */ + private final Set acceptedClassNamesFound; /** - * Class names already scheduled for scanning. If a class name is not in this list, the class is external, and - * has not yet been scheduled for scanning. + * The names of external (non-accepted) classes scheduled for extended scanning (where scanning is extended + * upwards to superclasses, interfaces and annotations). */ - private final Set classNamesScheduledForScanning; + private final Set classNamesScheduledForExtendedScanning; /** Any additional work units scheduled for scanning. */ private List additionalWorkUnits; @@ -126,9 +152,6 @@ class Classfile { /** The scan spec. */ private final ScanSpec scanSpec; - /** The log. */ - private final LogNode log; - // ------------------------------------------------------------------------------------------------------------- /** The number of constant pool entries plus one. */ @@ -150,8 +173,41 @@ class Classfile { // ------------------------------------------------------------------------------------------------------------- + /** + * Class containment. + */ + static class ClassContainment { + /** The inner class name. */ + public final String innerClassName; + + /** The inner class modifier bits. */ + public final int innerClassModifierBits; + + /** The outer class name. */ + public final String outerClassName; + + /** + * Constructor. + * + * @param innerClassName + * the inner class name. + * @param innerClassModifierBits + * the inner class modifier bits. + * @param outerClassName + * the outer class name. + */ + public ClassContainment(final String innerClassName, final int innerClassModifierBits, + final String outerClassName) { + this.innerClassName = innerClassName; + this.innerClassModifierBits = innerClassModifierBits; + this.outerClassName = outerClassName; + } + } + + // ------------------------------------------------------------------------------------------------------------- + /** Thrown when a classfile's contents are not in the correct format. */ - class ClassfileFormatException extends IOException { + static class ClassfileFormatException extends IOException { /** serialVersionUID. */ static final long serialVersionUID = 1L; @@ -189,7 +245,7 @@ public synchronized Throwable fillInStackTrace() { } /** Thrown when a classfile needs to be skipped. */ - class SkipClassException extends IOException { + static class SkipClassException extends IOException { /** serialVersionUID. */ static final long serialVersionUID = 1L; @@ -223,73 +279,133 @@ public synchronized Throwable fillInStackTrace() { * the class name * @param relationship * the relationship type + * @param log + * the log */ - private void scheduleScanningIfExternalClass(final String className, final String relationship) { + private void scheduleScanningIfExternalClass(final String className, final String relationship, + final LogNode log) { // Don't scan Object if (className != null && !className.equals("java.lang.Object") - // Only schedule each external class once for scanning, across all threads - && classNamesScheduledForScanning.add(className)) { - // Search for the named class' classfile among classpath elements, in classpath order (this is O(N) - // for each class, but there shouldn't be too many cases of extending scanning upwards) - final String classfilePath = JarUtils.classNameToClassfilePath(className); - // First check current classpath element, to avoid iterating through other classpath elements - Resource classResource = classpathElement.getResource(classfilePath); - ClasspathElement foundInClasspathElt = null; - if (classResource != null) { - // Found the classfile in the current classpath element - foundInClasspathElt = classpathElement; + // Don't schedule a class for scanning that was already found to be accepted + && !acceptedClassNamesFound.contains(className) + // Only schedule each external class once for scanning, across all threads + && classNamesScheduledForExtendedScanning.add(className)) { + if (scanSpec.classAcceptReject.isRejected(className)) { + if (log != null) { + log.log("Cannot extend scanning upwards to external " + relationship + " " + className + + ", since it is rejected"); + } } else { - // Didn't find the classfile in the current classpath element -- iterate through other elements - for (final ClasspathElement classpathOrderElt : classpathOrder) { - if (classpathOrderElt != classpathElement) { - classResource = classpathOrderElt.getResource(classfilePath); - if (classResource != null) { - foundInClasspathElt = classpathOrderElt; - break; + // Search for the named class' classfile among classpath elements, in classpath order (this is O(N) + // for each class, but there shouldn't be too many cases of extending scanning upwards) + final String classfilePath = JarUtils.classNameToClassfilePath(className); + // First check current classpath element, to avoid iterating through other classpath elements + Resource classResource = classpathElement.getResource(classfilePath); + ClasspathElement foundInClasspathElt = null; + if (classResource != null) { + // Found the classfile in the current classpath element + foundInClasspathElt = classpathElement; + } else { + // Didn't find the classfile in the current classpath element -- iterate through other elements + for (final ClasspathElement classpathOrderElt : classpathOrder) { + if (classpathOrderElt != classpathElement) { + classResource = classpathOrderElt.getResource(classfilePath); + if (classResource != null) { + foundInClasspathElt = classpathOrderElt; + break; + } } } } - } - if (classResource != null) { - // Found class resource - if (log != null) { - log.log("Scheduling external class for scanning: " + relationship + " " + className - + (foundInClasspathElt == classpathElement ? "" - : " -- found in classpath element " + foundInClasspathElt)); - } - if (additionalWorkUnits == null) { - additionalWorkUnits = new ArrayList<>(); - } - // Schedule class resource for scanning - additionalWorkUnits.add(new ClassfileScanWorkUnit(foundInClasspathElt, classResource, - /* isExternalClass = */ true)); - } else { - if (log != null) { - log.log("External " + relationship + " " + className + " was not found in " - + "non-blacklisted packages -- cannot extend scanning to this class"); + if (classResource != null) { + // Found class resource + if (log != null) { + // Log the extended scan as a child LogNode of the current class' scan log, since the + // external class is not scanned at the regular place in the classpath element hierarchy + // traversal + classResource.scanLog = log + .log("Extending scanning to external " + relationship + + (foundInClasspathElt == classpathElement ? " in same classpath element" + : " in classpath element " + foundInClasspathElt) + + ": " + className); + } + if (additionalWorkUnits == null) { + additionalWorkUnits = new ArrayList<>(); + } + // Schedule class resource for scanning + additionalWorkUnits.add(new ClassfileScanWorkUnit(foundInClasspathElt, classResource, + /* isExternalClass = */ true)); + } else { + if (log != null) { + log.log("External " + relationship + " " + className + " was not found in " + + "non-rejected packages -- cannot extend scanning to this class"); + } } } } } + /** + * Check if scanning needs to be extended upwards from an annotation parameter value. + * + * @param annotationParamVal + * the {@link AnnotationInfo} object for an annotation, or for an annotation parameter value. + * @param log + * the log + */ + private void extendScanningUpwardsFromAnnotationParameterValues(final Object annotationParamVal, + final LogNode log) { + if (annotationParamVal == null) { + // Should not be possible -- ignore + } else if (annotationParamVal instanceof AnnotationInfo) { + final AnnotationInfo annotationInfo = (AnnotationInfo) annotationParamVal; + scheduleScanningIfExternalClass(annotationInfo.getClassName(), "annotation class", log); + for (final AnnotationParameterValue apv : annotationInfo.getParameterValues()) { + extendScanningUpwardsFromAnnotationParameterValues(apv.getValue(), log); + } + } else if (annotationParamVal instanceof AnnotationEnumValue) { + scheduleScanningIfExternalClass(((AnnotationEnumValue) annotationParamVal).getClassName(), "enum class", + log); + } else if (annotationParamVal instanceof AnnotationClassRef) { + scheduleScanningIfExternalClass(((AnnotationClassRef) annotationParamVal).getClassName(), "class ref", + log); + } else if (annotationParamVal.getClass().isArray()) { + for (int i = 0, n = Array.getLength(annotationParamVal); i < n; i++) { + extendScanningUpwardsFromAnnotationParameterValues(Array.get(annotationParamVal, i), log); + } + } else { + // String etc. -- ignore + } + } + /** * Check if scanning needs to be extended upwards to an external superclass, interface or annotation. + * + * @param log + * the log */ - private void extendScanningUpwards() { + private void extendScanningUpwards(final LogNode log) { // Check superclass if (superclassName != null) { - scheduleScanningIfExternalClass(superclassName, "superclass"); + scheduleScanningIfExternalClass(superclassName, "superclass", log); } // Check implemented interfaces if (implementedInterfaces != null) { for (final String interfaceName : implementedInterfaces) { - scheduleScanningIfExternalClass(interfaceName, "interface"); + scheduleScanningIfExternalClass(interfaceName, "interface", log); } } // Check class annotations if (classAnnotations != null) { for (final AnnotationInfo annotationInfo : classAnnotations) { - scheduleScanningIfExternalClass(annotationInfo.getName(), "class annotation"); + scheduleScanningIfExternalClass(annotationInfo.getName(), "class annotation", log); + extendScanningUpwardsFromAnnotationParameterValues(annotationInfo, log); + } + } + // Check annotation default parameter values + if (annotationParamDefaultValues != null) { + for (final AnnotationParameterValue apv : annotationParamDefaultValues) { + extendScanningUpwardsFromAnnotationParameterValues(apv.getValue(), log); } } // Check method annotations and method parameter annotations @@ -297,20 +413,27 @@ private void extendScanningUpwards() { for (final MethodInfo methodInfo : methodInfoList) { if (methodInfo.annotationInfo != null) { for (final AnnotationInfo methodAnnotationInfo : methodInfo.annotationInfo) { - scheduleScanningIfExternalClass(methodAnnotationInfo.getName(), "method annotation"); + scheduleScanningIfExternalClass(methodAnnotationInfo.getName(), "method annotation", log); + extendScanningUpwardsFromAnnotationParameterValues(methodAnnotationInfo, log); } if (methodInfo.parameterAnnotationInfo != null && methodInfo.parameterAnnotationInfo.length > 0) { - for (final AnnotationInfo[] paramAnns : methodInfo.parameterAnnotationInfo) { - if (paramAnns != null && paramAnns.length > 0) { - for (final AnnotationInfo paramAnn : paramAnns) { - scheduleScanningIfExternalClass(paramAnn.getName(), - "method parameter annotation"); + for (final AnnotationInfo[] paramAnnInfoArr : methodInfo.parameterAnnotationInfo) { + if (paramAnnInfoArr != null && paramAnnInfoArr.length > 0) { + for (final AnnotationInfo paramAnnInfo : paramAnnInfoArr) { + scheduleScanningIfExternalClass(paramAnnInfo.getName(), + "method parameter annotation", log); + extendScanningUpwardsFromAnnotationParameterValues(paramAnnInfo, log); } } } } } + if (methodInfo.getThrownExceptionNames() != null) { + for (final String thrownExceptionName : methodInfo.getThrownExceptionNames()) { + scheduleScanningIfExternalClass(thrownExceptionName, "method throws", log); + } + } } } // Check field annotations @@ -318,11 +441,20 @@ private void extendScanningUpwards() { for (final FieldInfo fieldInfo : fieldInfoList) { if (fieldInfo.annotationInfo != null) { for (final AnnotationInfo fieldAnnotationInfo : fieldInfo.annotationInfo) { - scheduleScanningIfExternalClass(fieldAnnotationInfo.getName(), "field annotation"); + scheduleScanningIfExternalClass(fieldAnnotationInfo.getName(), "field annotation", log); + extendScanningUpwardsFromAnnotationParameterValues(fieldAnnotationInfo, log); } } } } + // Check if this class is an inner class, and if so, extend scanning to outer class + if (classContainmentEntries != null) { + for (final ClassContainment classContainmentEntry : classContainmentEntries) { + if (classContainmentEntry.innerClassName.equals(className)) { + scheduleScanningIfExternalClass(classContainmentEntry.outerClassName, "outer class", log); + } + } + } } // ------------------------------------------------------------------------------------------------------------- @@ -353,9 +485,12 @@ void link(final Map classNameToClassInfo, // Handle regular classfile classInfo = ClassInfo.addScannedClass(className, classModifiers, isExternalClass, classNameToClassInfo, classpathElement, classfileResource); + classInfo.setClassfileVersion(minorVersion, majorVersion); classInfo.setModifiers(classModifiers); classInfo.setIsInterface(isInterface); classInfo.setIsAnnotation(isAnnotation); + classInfo.setIsRecord(isRecord); + classInfo.setSourceFile(sourceFile); if (superclassName != null) { classInfo.addSuperclass(superclassName, classNameToClassInfo); } @@ -384,20 +519,23 @@ void link(final Map classNameToClassInfo, if (methodInfoList != null) { classInfo.addMethodInfo(methodInfoList, classNameToClassInfo); } - if (typeSignature != null) { - classInfo.setTypeSignature(typeSignature); + if (typeSignatureStr != null) { + classInfo.setTypeSignature(typeSignatureStr); } if (refdClassNames != null) { classInfo.addReferencedClassNames(refdClassNames); } + if (classTypeAnnotationDecorators != null) { + classInfo.addTypeDecorators(classTypeAnnotationDecorators); + } } // Get or create PackageInfo, if this is not a module descriptor (the module descriptor's package is "") PackageInfo packageInfo = null; if (!isModuleDescriptor) { // Get package for this class or package descriptor - packageInfo = PackageInfo.getOrCreatePackage(PackageInfo.getParentPackageName(className), - packageNameToPackageInfo); + final String packageName = PackageInfo.getParentPackageName(className); + packageInfo = PackageInfo.getOrCreatePackage(packageName, packageNameToPackageInfo, scanSpec); if (isPackageDescriptor) { // Add any class annotations on the package-info.class file to the ModuleInfo packageInfo.addAnnotations(classAnnotations); @@ -431,11 +569,28 @@ void link(final Map classNameToClassInfo, moduleInfo.addPackageInfo(packageInfo); } } - } // ------------------------------------------------------------------------------------------------------------- + /** + * Intern a string. + * + * @param str + * the str + * @return the string + */ + private String intern(final String str) { + if (str == null) { + return null; + } + final String interned = stringInternMap.putIfAbsent(str, str); + if (interned != null) { + return interned; + } + return str; + } + /** * Get the byte offset within the buffer of a string from the constant pool, or 0 for a null string. * @@ -529,9 +684,15 @@ private int getConstantPoolStringOffset(final int cpIdx, final int subFieldIdx) private String getConstantPoolString(final int cpIdx, final boolean replaceSlashWithDot, final boolean stripLSemicolon) throws ClassfileFormatException, IOException { final int constantPoolStringOffset = getConstantPoolStringOffset(cpIdx, /* subFieldIdx = */ 0); - return constantPoolStringOffset == 0 ? null - : inputStreamOrByteBuffer.readString(constantPoolStringOffset, replaceSlashWithDot, - stripLSemicolon); + if (constantPoolStringOffset == 0) { + return null; + } + final int utfLen = reader.readUnsignedShort(constantPoolStringOffset); + if (utfLen == 0) { + return ""; + } + return intern( + reader.readString(constantPoolStringOffset + 2L, utfLen, replaceSlashWithDot, stripLSemicolon)); } /** @@ -551,9 +712,15 @@ private String getConstantPoolString(final int cpIdx, final boolean replaceSlash private String getConstantPoolString(final int cpIdx, final int subFieldIdx) throws ClassfileFormatException, IOException { final int constantPoolStringOffset = getConstantPoolStringOffset(cpIdx, subFieldIdx); - return constantPoolStringOffset == 0 ? null - : inputStreamOrByteBuffer.readString(constantPoolStringOffset, /* replaceSlashWithDot = */ false, - /* stripLSemicolon = */ false); + if (constantPoolStringOffset == 0) { + return null; + } + final int utfLen = reader.readUnsignedShort(constantPoolStringOffset); + if (utfLen == 0) { + return ""; + } + return intern(reader.readString(constantPoolStringOffset + 2L, utfLen, /* replaceSlashWithDot = */ false, + /* stripLSemicolon = */ false)); } /** @@ -587,11 +754,11 @@ private byte getConstantPoolStringFirstByte(final int cpIdx) throws ClassfileFor if (constantPoolStringOffset == 0) { return '\0'; } - final int utfLen = inputStreamOrByteBuffer.readUnsignedShort(constantPoolStringOffset); + final int utfLen = reader.readUnsignedShort(constantPoolStringOffset); if (utfLen == 0) { return '\0'; } - return inputStreamOrByteBuffer.buf[constantPoolStringOffset + 2]; + return reader.readByte(constantPoolStringOffset + 2L); } /** @@ -627,34 +794,37 @@ private String getConstantPoolClassDescriptor(final int cpIdx) throws ClassfileF } /** - * Compare a string in the constant pool with a given constant, without constructing the String object. + * Compare a string in the constant pool with a given ASCII string, without constructing the constant pool + * String object. * * @param cpIdx * the constant pool index - * @param otherString - * the other string + * @param asciiStr + * the ASCII string to compare to * @return true, if successful * @throws ClassfileFormatException * If a problem occurs. * @throws IOException * If an IO exception occurs. */ - private boolean constantPoolStringEquals(final int cpIdx, final String otherString) + private boolean constantPoolStringEquals(final int cpIdx, final String asciiStr) throws ClassfileFormatException, IOException { - final int strOffset = getConstantPoolStringOffset(cpIdx, /* subFieldIdx = */ 0); - if (strOffset == 0) { - return otherString == null; - } else if (otherString == null) { + final int cpStrOffset = getConstantPoolStringOffset(cpIdx, /* subFieldIdx = */ 0); + if (cpStrOffset == 0) { + return asciiStr == null; + } else if (asciiStr == null) { return false; } - final int strLen = inputStreamOrByteBuffer.readUnsignedShort(strOffset); - final int otherLen = otherString.length(); - if (strLen != otherLen) { + final int cpStrLen = reader.readUnsignedShort(cpStrOffset); + final int asciiStrLen = asciiStr.length(); + if (cpStrLen != asciiStrLen) { return false; } - final int strStart = strOffset + 2; - for (int i = 0; i < strLen; i++) { - if ((char) (inputStreamOrByteBuffer.buf[strStart + i] & 0xff) != otherString.charAt(i)) { + final int cpStrStart = cpStrOffset + 2; + reader.bufferTo(cpStrStart + cpStrLen); + final byte[] buf = reader.buf(); + for (int i = 0; i < cpStrLen; i++) { + if ((char) (buf[cpStrStart + i] & 0xff) != asciiStr.charAt(i)) { return false; } } @@ -663,24 +833,6 @@ private boolean constantPoolStringEquals(final int cpIdx, final String otherStri // ------------------------------------------------------------------------------------------------------------- - /** - * Read an unsigned short from the constant pool. - * - * @param cpIdx - * the constant pool index. - * @return the unsigned short - * @throws IOException - * If an I/O exception occurred. - */ - private int cpReadUnsignedShort(final int cpIdx) throws IOException { - if (cpIdx < 1 || cpIdx >= cpCount) { - throw new ClassfileFormatException("Constant pool index " + cpIdx + ", should be in range [1, " - + (cpCount - 1) + "] -- cannot continue reading class. " - + "Please report this at https://github.com/classgraph/classgraph/issues"); - } - return inputStreamOrByteBuffer.readUnsignedShort(entryOffset[cpIdx]); - } - /** * Read an int from the constant pool. * @@ -696,7 +848,7 @@ private int cpReadInt(final int cpIdx) throws IOException { + (cpCount - 1) + "] -- cannot continue reading class. " + "Please report this at https://github.com/classgraph/classgraph/issues"); } - return inputStreamOrByteBuffer.readInt(entryOffset[cpIdx]); + return reader.readInt(entryOffset[cpIdx]); } /** @@ -714,7 +866,7 @@ private long cpReadLong(final int cpIdx) throws IOException { + (cpCount - 1) + "] -- cannot continue reading class. " + "Please report this at https://github.com/classgraph/classgraph/issues"); } - return inputStreamOrByteBuffer.readLong(entryOffset[cpIdx]); + return reader.readLong(entryOffset[cpIdx]); } // ------------------------------------------------------------------------------------------------------------- @@ -770,7 +922,7 @@ private Object getFieldConstantPoolValue(final int tag, final char fieldTypeDesc default: // ClassGraph doesn't expect other types // (N.B. in particular, enum values are not stored in the constant pool, so don't need to be handled) - throw new ClassfileFormatException("Unknown constant pool tag " + tag + ", " + throw new ClassfileFormatException("Unknown field constant pool tag " + tag + ", " + "cannot continue reading class. Please report this at " + "https://github.com/classgraph/classgraph/issues"); } @@ -787,14 +939,13 @@ private Object getFieldConstantPoolValue(final int tag, final char fieldTypeDesc */ private AnnotationInfo readAnnotation() throws IOException { // Lcom/xyz/Annotation; -> Lcom.xyz.Annotation; - final String annotationClassName = getConstantPoolClassDescriptor( - inputStreamOrByteBuffer.readUnsignedShort()); - final int numElementValuePairs = inputStreamOrByteBuffer.readUnsignedShort(); + final String annotationClassName = getConstantPoolClassDescriptor(reader.readUnsignedShort()); + final int numElementValuePairs = reader.readUnsignedShort(); AnnotationParameterValueList paramVals = null; if (numElementValuePairs > 0) { paramVals = new AnnotationParameterValueList(numElementValuePairs); for (int i = 0; i < numElementValuePairs; i++) { - final String paramName = getConstantPoolString(inputStreamOrByteBuffer.readUnsignedShort()); + final String paramName = getConstantPoolString(reader.readUnsignedShort()); final Object paramValue = readAnnotationElementValue(); paramVals.add(new AnnotationParameterValue(paramName, paramValue)); } @@ -810,44 +961,42 @@ private AnnotationInfo readAnnotation() throws IOException { * If an IO exception occurs. */ private Object readAnnotationElementValue() throws IOException { - final int tag = (char) inputStreamOrByteBuffer.readUnsignedByte(); + final int tag = (char) reader.readUnsignedByte(); switch (tag) { case 'B': - return (byte) cpReadInt(inputStreamOrByteBuffer.readUnsignedShort()); + return (byte) cpReadInt(reader.readUnsignedShort()); case 'C': - return (char) cpReadInt(inputStreamOrByteBuffer.readUnsignedShort()); + return (char) cpReadInt(reader.readUnsignedShort()); case 'D': - return Double.longBitsToDouble(cpReadLong(inputStreamOrByteBuffer.readUnsignedShort())); + return Double.longBitsToDouble(cpReadLong(reader.readUnsignedShort())); case 'F': - return Float.intBitsToFloat(cpReadInt(inputStreamOrByteBuffer.readUnsignedShort())); + return Float.intBitsToFloat(cpReadInt(reader.readUnsignedShort())); case 'I': - return cpReadInt(inputStreamOrByteBuffer.readUnsignedShort()); + return cpReadInt(reader.readUnsignedShort()); case 'J': - return cpReadLong(inputStreamOrByteBuffer.readUnsignedShort()); + return cpReadLong(reader.readUnsignedShort()); case 'S': - return (short) cpReadUnsignedShort(inputStreamOrByteBuffer.readUnsignedShort()); + return (short) cpReadInt(reader.readUnsignedShort()); case 'Z': - return cpReadInt(inputStreamOrByteBuffer.readUnsignedShort()) != 0; + return cpReadInt(reader.readUnsignedShort()) != 0; case 's': - return getConstantPoolString(inputStreamOrByteBuffer.readUnsignedShort()); + return getConstantPoolString(reader.readUnsignedShort()); case 'e': { // Return type is AnnotationEnumVal. - final String annotationClassName = getConstantPoolClassDescriptor( - inputStreamOrByteBuffer.readUnsignedShort()); - final String annotationConstName = getConstantPoolString(inputStreamOrByteBuffer.readUnsignedShort()); + final String annotationClassName = getConstantPoolClassDescriptor(reader.readUnsignedShort()); + final String annotationConstName = getConstantPoolString(reader.readUnsignedShort()); return new AnnotationEnumValue(annotationClassName, annotationConstName); } case 'c': // Return type is AnnotationClassRef (for class references in annotations) - final String classRefTypeDescriptor = getConstantPoolString( - inputStreamOrByteBuffer.readUnsignedShort()); + final String classRefTypeDescriptor = getConstantPoolString(reader.readUnsignedShort()); return new AnnotationClassRef(classRefTypeDescriptor); case '@': // Complex (nested) annotation. Return type is AnnotationInfo. return readAnnotation(); case '[': // Return type is Object[] (of nested annotation element values) - final int count = inputStreamOrByteBuffer.readUnsignedShort(); + final int count = reader.readUnsignedShort(); final Object[] arr = new Object[count]; for (int i = 0; i < count; ++i) { // Nested annotation element value @@ -863,23 +1012,69 @@ private Object readAnnotationElementValue() throws IOException { // ------------------------------------------------------------------------------------------------------------- + interface ClassTypeAnnotationDecorator { + void decorate(ClassTypeSignature classTypeSignature); + } + + interface MethodTypeAnnotationDecorator { + void decorate(MethodTypeSignature methodTypeSignature); + } + + interface TypeAnnotationDecorator { + void decorate(TypeSignature typeSignature); + } + + static class TypePathNode { + short typePathKind; + short typeArgumentIdx; + + public TypePathNode(final int typePathKind, final int typeArgumentIdx) { + this.typePathKind = (short) typePathKind; + this.typeArgumentIdx = (short) typeArgumentIdx; + } + + @Override + public String toString() { + return "(" + typePathKind + "," + typeArgumentIdx + ")"; + } + } + + private List readTypePath() throws IOException { + final int typePathLength = reader.readUnsignedByte(); + if (typePathLength == 0) { + return Collections.emptyList(); + } else { + final List list = new ArrayList<>(typePathLength); + for (int i = 0; i < typePathLength; i++) { + final int typePathKind = reader.readUnsignedByte(); + final int typeArgumentIdx = reader.readUnsignedByte(); + list.add(new TypePathNode(typePathKind, typeArgumentIdx)); + } + return list; + } + } + + // ------------------------------------------------------------------------------------------------------------- + /** * Read constant pool entries. * + * @param log + * The log * @throws IOException * Signals that an I/O exception has occurred. */ - private void readConstantPoolEntries() throws IOException { + private void readConstantPoolEntries(final LogNode log) throws IOException { // Only record class dependency info if inter-class dependencies are enabled List classNameCpIdxs = null; List typeSignatureIdxs = null; if (scanSpec.enableInterClassDependencies) { - classNameCpIdxs = new ArrayList(); - typeSignatureIdxs = new ArrayList(); + classNameCpIdxs = new ArrayList<>(); + typeSignatureIdxs = new ArrayList<>(); } // Read size of constant pool - cpCount = inputStreamOrByteBuffer.readUnsignedShort(); + cpCount = reader.readUnsignedShort(); // Allocate storage for constant pool entryOffset = new int[cpCount]; @@ -894,74 +1089,79 @@ private void readConstantPoolEntries() throws IOException { skipSlot = 0; continue; } - entryTag[i] = inputStreamOrByteBuffer.readUnsignedByte(); - entryOffset[i] = inputStreamOrByteBuffer.curr; + entryTag[i] = reader.readUnsignedByte(); + entryOffset[i] = reader.currPos(); switch (entryTag[i]) { case 0: // Impossible, probably buffer underflow - throw new ClassfileFormatException("Unknown constant pool tag 0 in classfile " + relativePath + throw new ClassfileFormatException("Invalid constant pool tag 0 in classfile " + relativePath + " (possible buffer underflow issue). Please report this at " + "https://github.com/classgraph/classgraph/issues"); case 1: // Modified UTF8 - final int strLen = inputStreamOrByteBuffer.readUnsignedShort(); - inputStreamOrByteBuffer.skip(strLen); + final int strLen = reader.readUnsignedShort(); + reader.skip(strLen); break; + // There is no constant pool tag type 2 case 3: // int, short, char, byte, boolean are all represented by Constant_INTEGER case 4: // float - inputStreamOrByteBuffer.skip(4); + reader.skip(4); break; case 5: // long case 6: // double - inputStreamOrByteBuffer.skip(8); + reader.skip(8); skipSlot = 1; // double slot break; case 7: // Class reference (format is e.g. "java/lang/String") // Forward or backward indirect reference to a modified UTF8 entry - indirectStringRefs[i] = inputStreamOrByteBuffer.readUnsignedShort(); - if (scanSpec.enableInterClassDependencies && entryTag[i] == 7) { + indirectStringRefs[i] = reader.readUnsignedShort(); + if (classNameCpIdxs != null) { // If this is a class ref, and inter-class dependencies are enabled, record the dependency classNameCpIdxs.add(indirectStringRefs[i]); } break; case 8: // String // Forward or backward indirect reference to a modified UTF8 entry - indirectStringRefs[i] = inputStreamOrByteBuffer.readUnsignedShort(); + indirectStringRefs[i] = reader.readUnsignedShort(); break; case 9: // field ref // Refers to a class ref (case 7) and then a name and type (case 12) - inputStreamOrByteBuffer.skip(4); + reader.skip(4); break; case 10: // method ref // Refers to a class ref (case 7) and then a name and type (case 12) - inputStreamOrByteBuffer.skip(4); + reader.skip(4); break; case 11: // interface method ref // Refers to a class ref (case 7) and then a name and type (case 12) - inputStreamOrByteBuffer.skip(4); + reader.skip(4); break; case 12: // name and type - final int nameRef = inputStreamOrByteBuffer.readUnsignedShort(); - final int typeRef = inputStreamOrByteBuffer.readUnsignedShort(); - if (scanSpec.enableInterClassDependencies) { + final int nameRef = reader.readUnsignedShort(); + final int typeRef = reader.readUnsignedShort(); + if (typeSignatureIdxs != null) { typeSignatureIdxs.add(typeRef); } indirectStringRefs[i] = (nameRef << 16) | typeRef; break; + // There is no constant pool tag type 13 or 14 case 15: // method handle - inputStreamOrByteBuffer.skip(3); + reader.skip(3); break; case 16: // method type - inputStreamOrByteBuffer.skip(2); + reader.skip(2); + break; + case 17: // dynamic + reader.skip(4); break; case 18: // invoke dynamic - inputStreamOrByteBuffer.skip(4); + reader.skip(4); break; case 19: // module (for module-info.class in JDK9+) // see https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.4 - indirectStringRefs[i] = inputStreamOrByteBuffer.readUnsignedShort(); + indirectStringRefs[i] = reader.readUnsignedShort(); break; case 20: // package (for module-info.class in JDK9+) // see https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.4 - inputStreamOrByteBuffer.skip(2); + reader.skip(2); break; default: throw new ClassfileFormatException("Unknown constant pool tag " + entryTag[i] @@ -970,10 +1170,12 @@ private void readConstantPoolEntries() throws IOException { } } - // Find classes referenced in the constant pool (note that there are some class refs that will not be + // Find classes referenced in the constant pool. Note that there are some class refs that will not be // found this way, e.g. enum classes and class refs in annotation parameter values, since they are - // referenced as strings (tag 1) rather than classes (tag 7) or type signatures (part of tag 12)). - if (scanSpec.enableInterClassDependencies) { + // referenced as strings (tag 1) rather than classes (tag 7) or type signatures (part of tag 12). + // Therefore, a hybrid approach needs to be applied of extracting these other class refs from + // the ClassInfo graph, and combining them with class names extracted from the constant pool here. + if (classNameCpIdxs != null) { refdClassNames = new HashSet<>(); // Get class names from direct class references in constant pool for (final int cpIdx : classNameCpIdxs) { @@ -995,26 +1197,36 @@ private void readConstantPoolEntries() throws IOException { } } } + } + if (typeSignatureIdxs != null) { // Get class names from type signatures in "name and type" entries in constant pool for (final int cpIdx : typeSignatureIdxs) { final String typeSigStr = getConstantPoolString(cpIdx); if (typeSigStr != null) { try { - if (typeSigStr.indexOf('(') >= 0 || "".equals(typeSigStr)) { - // Parse the type signature - final MethodTypeSignature typeSig = MethodTypeSignature.parse(typeSigStr, + if (typeSigStr.startsWith("L") && typeSigStr.endsWith(";")) { + // Parse the class name + final TypeSignature typeSig = TypeSignature.parse(typeSigStr, /* definingClassName = */ null); // Extract class names from type signature typeSig.findReferencedClassNames(refdClassNames); - } else { + } else if (typeSigStr.indexOf('(') >= 0 || "".equals(typeSigStr)) { // Parse the type signature - final TypeSignature typeSig = TypeSignature.parse(typeSigStr, + final MethodTypeSignature typeSig = MethodTypeSignature.parse(typeSigStr, /* definingClassName = */ null); // Extract class names from type signature typeSig.findReferencedClassNames(refdClassNames); + } else { + if (log != null) { + log.log("Could not extract referenced class names from constant pool string: " + + typeSigStr); + } } } catch (final ParseException e) { - throw new ClassfileFormatException("Could not parse type signature: " + typeSigStr, e); + if (log != null) { + log.log("Could not extract referenced class names from constant pool string: " + + typeSigStr + " : " + e); + } } } } @@ -1036,12 +1248,13 @@ private void readConstantPoolEntries() throws IOException { */ private void readBasicClassInfo() throws IOException, ClassfileFormatException, SkipClassException { // Modifier flags - classModifiers = inputStreamOrByteBuffer.readUnsignedShort(); + classModifiers = reader.readUnsignedShort(); + isInterface = (classModifiers & 0x0200) != 0; isAnnotation = (classModifiers & 0x2000) != 0; // The fully-qualified class name of this class, with slashes replaced with dots - final String classNamePath = getConstantPoolString(inputStreamOrByteBuffer.readUnsignedShort()); + final String classNamePath = getConstantPoolString(reader.readUnsignedShort()); if (classNamePath == null) { throw new ClassfileFormatException("Class name is null"); } @@ -1072,7 +1285,7 @@ private void readBasicClassInfo() throws IOException, ClassfileFormatException, } // Superclass name, with slashes replaced with dots - final int superclassNameCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final int superclassNameCpIdx = reader.readUnsignedShort(); if (superclassNameCpIdx > 0) { superclassName = getConstantPoolClassName(superclassNameCpIdx); } @@ -1088,9 +1301,9 @@ private void readBasicClassInfo() throws IOException, ClassfileFormatException, */ private void readInterfaces() throws IOException { // Interfaces - final int interfaceCount = inputStreamOrByteBuffer.readUnsignedShort(); + final int interfaceCount = reader.readUnsignedShort(); for (int i = 0; i < interfaceCount; i++) { - final String interfaceName = getConstantPoolClassName(inputStreamOrByteBuffer.readUnsignedShort()); + final String interfaceName = getConstantPoolClassName(reader.readUnsignedShort()); if (implementedInterfaces == null) { implementedInterfaces = new ArrayList<>(); } @@ -1110,47 +1323,47 @@ private void readInterfaces() throws IOException { */ private void readFields() throws IOException, ClassfileFormatException { // Fields - final int fieldCount = inputStreamOrByteBuffer.readUnsignedShort(); + final int fieldCount = reader.readUnsignedShort(); for (int i = 0; i < fieldCount; i++) { // Info on modifier flags: http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.5 - final int fieldModifierFlags = inputStreamOrByteBuffer.readUnsignedShort(); + final int fieldModifierFlags = reader.readUnsignedShort(); final boolean isPublicField = ((fieldModifierFlags & 0x0001) == 0x0001); - final boolean isStaticFinalField = ((fieldModifierFlags & 0x0018) == 0x0018); final boolean fieldIsVisible = isPublicField || scanSpec.ignoreFieldVisibility; final boolean getStaticFinalFieldConstValue = scanSpec.enableStaticFinalFieldConstantInitializerValues - && isStaticFinalField && fieldIsVisible; + && fieldIsVisible; + List fieldTypeAnnotationDecorators = null; if (!fieldIsVisible || (!scanSpec.enableFieldInfo && !getStaticFinalFieldConstValue)) { // Skip field - inputStreamOrByteBuffer.readUnsignedShort(); // fieldNameCpIdx - inputStreamOrByteBuffer.readUnsignedShort(); // fieldTypeDescriptorCpIdx - final int attributesCount = inputStreamOrByteBuffer.readUnsignedShort(); + reader.readUnsignedShort(); // fieldNameCpIdx + reader.readUnsignedShort(); // fieldTypeDescriptorCpIdx + final int attributesCount = reader.readUnsignedShort(); for (int j = 0; j < attributesCount; j++) { - inputStreamOrByteBuffer.readUnsignedShort(); // attributeNameCpIdx - final int attributeLength = inputStreamOrByteBuffer.readInt(); // == 2 - inputStreamOrByteBuffer.skip(attributeLength); + reader.readUnsignedShort(); // attributeNameCpIdx + final int attributeLength = reader.readInt(); // == 2 + reader.skip(attributeLength); } } else { - final int fieldNameCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final int fieldNameCpIdx = reader.readUnsignedShort(); final String fieldName = getConstantPoolString(fieldNameCpIdx); - final int fieldTypeDescriptorCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final int fieldTypeDescriptorCpIdx = reader.readUnsignedShort(); final char fieldTypeDescriptorFirstChar = (char) getConstantPoolStringFirstByte( fieldTypeDescriptorCpIdx); String fieldTypeDescriptor; - String fieldTypeSignature = null; + String fieldTypeSignatureStr = null; fieldTypeDescriptor = getConstantPoolString(fieldTypeDescriptorCpIdx); Object fieldConstValue = null; AnnotationInfoList fieldAnnotationInfo = null; - final int attributesCount = inputStreamOrByteBuffer.readUnsignedShort(); + final int attributesCount = reader.readUnsignedShort(); for (int j = 0; j < attributesCount; j++) { - final int attributeNameCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); - final int attributeLength = inputStreamOrByteBuffer.readInt(); // == 2 + final int attributeNameCpIdx = reader.readUnsignedShort(); + final int attributeLength = reader.readInt(); // == 2 // See if field name matches one of the requested names for this class, and if it does, // check if it is initialized with a constant value if ((getStaticFinalFieldConstValue) && constantPoolStringEquals(attributeNameCpIdx, "ConstantValue")) { // http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.2 - final int cpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final int cpIdx = reader.readUnsignedShort(); if (cpIdx < 1 || cpIdx >= cpCount) { throw new ClassfileFormatException("Constant pool index " + cpIdx + ", should be in range [1, " + (cpCount - 1) @@ -1160,25 +1373,52 @@ && constantPoolStringEquals(attributeNameCpIdx, "ConstantValue")) { fieldConstValue = getFieldConstantPoolValue(entryTag[cpIdx], fieldTypeDescriptorFirstChar, cpIdx); } else if (fieldIsVisible && constantPoolStringEquals(attributeNameCpIdx, "Signature")) { - fieldTypeSignature = getConstantPoolString(inputStreamOrByteBuffer.readUnsignedShort()); + fieldTypeSignatureStr = getConstantPoolString(reader.readUnsignedShort()); } else if (scanSpec.enableAnnotationInfo // && (constantPoolStringEquals(attributeNameCpIdx, "RuntimeVisibleAnnotations") || (!scanSpec.disableRuntimeInvisibleAnnotations && constantPoolStringEquals( attributeNameCpIdx, "RuntimeInvisibleAnnotations")))) { // Read annotation names - final int fieldAnnotationCount = inputStreamOrByteBuffer.readUnsignedShort(); - if (fieldAnnotationInfo == null && fieldAnnotationCount > 0) { - fieldAnnotationInfo = new AnnotationInfoList(1); - } - if (fieldAnnotationInfo != null) { + final int fieldAnnotationCount = reader.readUnsignedShort(); + if (fieldAnnotationCount > 0) { + if (fieldAnnotationInfo == null) { + fieldAnnotationInfo = new AnnotationInfoList(1); + } for (int k = 0; k < fieldAnnotationCount; k++) { final AnnotationInfo fieldAnnotation = readAnnotation(); fieldAnnotationInfo.add(fieldAnnotation); } } + } else if (scanSpec.enableAnnotationInfo // + && (constantPoolStringEquals(attributeNameCpIdx, "RuntimeVisibleTypeAnnotations") + || (!scanSpec.disableRuntimeInvisibleAnnotations && constantPoolStringEquals( + attributeNameCpIdx, "RuntimeInvisibleTypeAnnotations")))) { + final int annotationCount = reader.readUnsignedShort(); + if (annotationCount > 0) { + fieldTypeAnnotationDecorators = new ArrayList<>(); + for (int m = 0; m < annotationCount; m++) { + final int targetType = reader.readUnsignedByte(); + if (targetType != 0x13) { + throw new ClassfileFormatException( + "Class " + className + " has unknown field type annotation target 0x" + + Integer.toHexString(targetType) + + ": element size unknown, cannot continue reading class. " + + "Please report this at " + + "https://github.com/classgraph/classgraph/issues"); + } + final List typePath = readTypePath(); + final AnnotationInfo annotationInfo = readAnnotation(); + fieldTypeAnnotationDecorators.add(new TypeAnnotationDecorator() { + @Override + public void decorate(final TypeSignature typeSignature) { + typeSignature.addTypeAnnotation(typePath, annotationInfo); + } + }); + } + } } else { // No match, just skip attribute - inputStreamOrByteBuffer.skip(attributeLength); + reader.skip(attributeLength); } } if (scanSpec.enableFieldInfo && fieldIsVisible) { @@ -1186,7 +1426,8 @@ && constantPoolStringEquals(attributeNameCpIdx, "ConstantValue")) { fieldInfoList = new FieldInfoList(); } fieldInfoList.add(new FieldInfo(className, fieldName, fieldModifierFlags, fieldTypeDescriptor, - fieldTypeSignature, fieldConstValue, fieldAnnotationInfo)); + fieldTypeSignatureStr, fieldConstValue, fieldAnnotationInfo, + fieldTypeAnnotationDecorators)); } } } @@ -1204,53 +1445,56 @@ && constantPoolStringEquals(attributeNameCpIdx, "ConstantValue")) { */ private void readMethods() throws IOException, ClassfileFormatException { // Methods - final int methodCount = inputStreamOrByteBuffer.readUnsignedShort(); + final int methodCount = reader.readUnsignedShort(); for (int i = 0; i < methodCount; i++) { // Info on modifier flags: http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6 - final int methodModifierFlags = inputStreamOrByteBuffer.readUnsignedShort(); + final int methodModifierFlags = reader.readUnsignedShort(); final boolean isPublicMethod = ((methodModifierFlags & 0x0001) == 0x0001); final boolean methodIsVisible = isPublicMethod || scanSpec.ignoreMethodVisibility; - + List methodTypeAnnotationDecorators = null; String methodName = null; String methodTypeDescriptor = null; - String methodTypeSignature = null; + String methodTypeSignatureStr = null; // Always enable MethodInfo for annotations (this is how annotation constants are defined) final boolean enableMethodInfo = scanSpec.enableMethodInfo || isAnnotation; if (enableMethodInfo || isAnnotation) { // Annotations store defaults in method_info - final int methodNameCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final int methodNameCpIdx = reader.readUnsignedShort(); methodName = getConstantPoolString(methodNameCpIdx); - final int methodTypeDescriptorCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final int methodTypeDescriptorCpIdx = reader.readUnsignedShort(); methodTypeDescriptor = getConstantPoolString(methodTypeDescriptorCpIdx); } else { - inputStreamOrByteBuffer.skip(4); // name_index, descriptor_index + reader.skip(4); // name_index, descriptor_index } - final int attributesCount = inputStreamOrByteBuffer.readUnsignedShort(); + final int attributesCount = reader.readUnsignedShort(); String[] methodParameterNames = null; + String[] thrownExceptionNames = null; int[] methodParameterModifiers = null; AnnotationInfo[][] methodParameterAnnotations = null; AnnotationInfoList methodAnnotationInfo = null; boolean methodHasBody = false; + int minLineNum = 0; + int maxLineNum = 0; if (!methodIsVisible || (!enableMethodInfo && !isAnnotation)) { // Skip method attributes for (int j = 0; j < attributesCount; j++) { - inputStreamOrByteBuffer.skip(2); // attribute_name_index - final int attributeLength = inputStreamOrByteBuffer.readInt(); - inputStreamOrByteBuffer.skip(attributeLength); + reader.skip(2); // attribute_name_index + final int attributeLength = reader.readInt(); + reader.skip(attributeLength); } } else { // Look for method annotations for (int j = 0; j < attributesCount; j++) { - final int attributeNameCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); - final int attributeLength = inputStreamOrByteBuffer.readInt(); + final int attributeNameCpIdx = reader.readUnsignedShort(); + final int attributeLength = reader.readInt(); if (scanSpec.enableAnnotationInfo && (constantPoolStringEquals(attributeNameCpIdx, "RuntimeVisibleAnnotations") || (!scanSpec.disableRuntimeInvisibleAnnotations && constantPoolStringEquals( attributeNameCpIdx, "RuntimeInvisibleAnnotations")))) { - final int methodAnnotationCount = inputStreamOrByteBuffer.readUnsignedShort(); - if (methodAnnotationInfo == null && methodAnnotationCount > 0) { - methodAnnotationInfo = new AnnotationInfoList(1); - } - if (methodAnnotationInfo != null) { + final int methodAnnotationCount = reader.readUnsignedShort(); + if (methodAnnotationCount > 0) { + if (methodAnnotationInfo == null) { + methodAnnotationInfo = new AnnotationInfoList(1); + } for (int k = 0; k < methodAnnotationCount; k++) { final AnnotationInfo annotationInfo = readAnnotation(); methodAnnotationInfo.add(annotationInfo); @@ -1260,31 +1504,219 @@ private void readMethods() throws IOException, ClassfileFormatException { && (constantPoolStringEquals(attributeNameCpIdx, "RuntimeVisibleParameterAnnotations") || (!scanSpec.disableRuntimeInvisibleAnnotations && constantPoolStringEquals( attributeNameCpIdx, "RuntimeInvisibleParameterAnnotations")))) { - final int paramCount = inputStreamOrByteBuffer.readUnsignedByte(); - methodParameterAnnotations = new AnnotationInfo[paramCount][]; - for (int k = 0; k < paramCount; k++) { - final int numAnnotations = inputStreamOrByteBuffer.readUnsignedShort(); - methodParameterAnnotations[k] = numAnnotations == 0 ? NO_ANNOTATIONS - : new AnnotationInfo[numAnnotations]; - for (int l = 0; l < numAnnotations; l++) { - methodParameterAnnotations[k][l] = readAnnotation(); + // Merge together runtime visible and runtime invisible annotations into a single array + // of annotations for each method parameter (runtime visible and runtime invisible + // annotations are given in separate attributes, so if both attributes are present, + // have to make the parameter annotation arrays larger when the second attribute is + // encountered). + final int numParams = reader.readUnsignedByte(); + if (methodParameterAnnotations == null) { + methodParameterAnnotations = new AnnotationInfo[numParams][]; + } else if (methodParameterAnnotations.length != numParams) { + throw new ClassfileFormatException( + "Mismatch in number of parameters between RuntimeVisibleParameterAnnotations " + + "and RuntimeInvisibleParameterAnnotations"); + } + for (int paramIdx = 0; paramIdx < numParams; paramIdx++) { + final int numAnnotations = reader.readUnsignedShort(); + if (numAnnotations > 0) { + int annStartIdx = 0; + if (methodParameterAnnotations[paramIdx] != null) { + annStartIdx = methodParameterAnnotations[paramIdx].length; + methodParameterAnnotations[paramIdx] = Arrays.copyOf( + methodParameterAnnotations[paramIdx], annStartIdx + numAnnotations); + } else { + methodParameterAnnotations[paramIdx] = new AnnotationInfo[numAnnotations]; + } + for (int annIdx = 0; annIdx < numAnnotations; annIdx++) { + methodParameterAnnotations[paramIdx][annStartIdx + annIdx] = readAnnotation(); + } + } else if (methodParameterAnnotations[paramIdx] == null) { + methodParameterAnnotations[paramIdx] = NO_ANNOTATIONS; + } + } + } else if (scanSpec.enableAnnotationInfo // + && (constantPoolStringEquals(attributeNameCpIdx, "RuntimeVisibleTypeAnnotations") + || (!scanSpec.disableRuntimeInvisibleAnnotations && constantPoolStringEquals( + attributeNameCpIdx, "RuntimeInvisibleTypeAnnotations")))) { + final int annotationCount = reader.readUnsignedShort(); + if (annotationCount > 0) { + methodTypeAnnotationDecorators = new ArrayList<>(annotationCount); + for (int m = 0; m < annotationCount; m++) { + final int targetType = reader.readUnsignedByte(); + final int typeParameterIndex; + final int boundIndex; + final int formalParameterIndex; + final int throwsTypeIndex; + if (targetType == 0x01) { + // Type parameter declaration of generic method or constructor + typeParameterIndex = reader.readUnsignedByte(); + boundIndex = -1; + formalParameterIndex = -1; + throwsTypeIndex = -1; + } else if (targetType == 0x10) { + // This target_type is not supposed to be added to methods, it is intended + // for ClassFile annotations, but Google's Java compiler adds annotations + // of this type to methods in guava for some reason. Just ignore these + // annotations. (#861) + reader.readUnsignedShort(); + typeParameterIndex = -1; + boundIndex = -1; + formalParameterIndex = -1; + throwsTypeIndex = -1; + } else if (targetType == 0x12) { + // Type in bound of type parameter declaration of generic method + // or constructor + typeParameterIndex = reader.readUnsignedByte(); + boundIndex = reader.readUnsignedByte(); + formalParameterIndex = -1; + throwsTypeIndex = -1; + } else if (targetType == 0x13) { + // Type in field or record component declaration + // (empty target) + // This target_type is not supposed to be added to methods, but it seems + // that the JDK 17 compiler is buggy, and adds this target_type to the + // methods of records anyway (#797). Therefore, accept this, but ignore + // it (the same target_type should also be added to the fields of records). + typeParameterIndex = -1; + boundIndex = -1; + formalParameterIndex = -1; + throwsTypeIndex = -1; + } else if (targetType == 0x14) { + // Return type of method, or type of newly constructed object + // (empty target) + typeParameterIndex = -1; + boundIndex = -1; + formalParameterIndex = -1; + throwsTypeIndex = -1; + } else if (targetType == 0x15) { + // Receiver type of method or constructor + // (empty target) + typeParameterIndex = -1; + boundIndex = -1; + formalParameterIndex = -1; + throwsTypeIndex = -1; + } else if (targetType == 0x16) { + // Type in formal parameter declaration of method, constructor, + // or lambda expression + typeParameterIndex = -1; + boundIndex = -1; + formalParameterIndex = reader.readUnsignedByte(); + throwsTypeIndex = -1; + } else if (targetType == 0x17) { + // Type in throws clause of method or constructor + typeParameterIndex = -1; + boundIndex = -1; + formalParameterIndex = -1; + throwsTypeIndex = reader.readUnsignedShort(); + } else { + throw new ClassfileFormatException( + "Class " + className + " has unknown method type annotation target 0x" + + Integer.toHexString(targetType) + + ": element size unknown, cannot continue reading class. " + + "Please report this at " + + "https://github.com/classgraph/classgraph/issues"); + } + final List typePath = readTypePath(); + final AnnotationInfo annotationInfo = readAnnotation(); + methodTypeAnnotationDecorators.add(new MethodTypeAnnotationDecorator() { + @Override + public void decorate(final MethodTypeSignature methodTypeSignature) { + if (targetType == 0x01) { + // Type parameter declaration of generic method or constructor + final List typeParameters = methodTypeSignature + .getTypeParameters(); + if (typeParameters != null + && typeParameterIndex < typeParameters.size()) { + typeParameters.get(typeParameterIndex).addTypeAnnotation(typePath, + annotationInfo); + } + // else this is a method type descriptor, not a method type signature, + // so there are no type parameters + } else if (targetType == 0x12) { + // Type in bound of type parameter declaration of generic method or + // constructor + final List typeParameters = methodTypeSignature + .getTypeParameters(); + if (typeParameters != null + && typeParameterIndex < typeParameters.size()) { + final TypeParameter typeParameter = typeParameters + .get(typeParameterIndex); + // boundIndex == 0 => class bound; boundIndex > 0 => interface bound + if (boundIndex == 0) { + final ReferenceTypeSignature classBound = typeParameter + .getClassBound(); + if (classBound != null) { + classBound.addTypeAnnotation(typePath, annotationInfo); + } + } else { + final List interfaceBounds = // + typeParameter.getInterfaceBounds(); + if (interfaceBounds != null + && boundIndex - 1 < interfaceBounds.size()) { + interfaceBounds.get(boundIndex - 1) + .addTypeAnnotation(typePath, annotationInfo); + } + } + } + // else this is a method type descriptor, not a method type signature, + // so there are no type parameters + } else if (targetType == 0x14) { + // Return type of method, or type of newly constructed object + methodTypeSignature.getResultType().addTypeAnnotation(typePath, + annotationInfo); + } else if (targetType == 0x15) { + // Receiver type of method or constructor (explicit receiver parameter) + methodTypeSignature.addRecieverTypeAnnotation(annotationInfo); + } else if (targetType == 0x16) { + // Type in formal parameter declaration of method, constructor, + // or lambda expression. + // N.B. formal parameter indices are dodgy, because not all compilers + // index parameters the same way -- so be robust here. + // The classfile spec says "A formal_parameter_index value of i may, + // but is not required to, correspond to the i'th parameter descriptor + // in the method descriptor". Also "The formal_parameter_target item + // records that a formal parameter's type is annotated, but does not + // record the type itself. The type may be found by inspecting the + // method descriptor, although a formal_parameter_index value of 0 + // does not always indicate the first parameter descriptor in the + // method descriptor." + // What the heck, guys. + final List parameterTypeSignatures = methodTypeSignature + .getParameterTypeSignatures(); + if (formalParameterIndex < parameterTypeSignatures.size()) { + parameterTypeSignatures.get(formalParameterIndex) + .addTypeAnnotation(typePath, annotationInfo); + } + } else if (targetType == 0x17) { + // Type in throws clause of method or constructor + final List throwsSignatures = // + methodTypeSignature.getThrowsSignatures(); + if (throwsSignatures != null + && throwsTypeIndex < throwsSignatures.size()) { + throwsSignatures.get(throwsTypeIndex).addTypeAnnotation(typePath, + annotationInfo); + } + } + } + }); } } } else if (constantPoolStringEquals(attributeNameCpIdx, "MethodParameters")) { // Read method parameters. For Java, these are only produced in JDK8+, and only if the // commandline switch `-parameters` is provided at compiletime. - final int paramCount = inputStreamOrByteBuffer.readUnsignedByte(); + final int paramCount = reader.readUnsignedByte(); methodParameterNames = new String[paramCount]; methodParameterModifiers = new int[paramCount]; for (int k = 0; k < paramCount; k++) { - final int cpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final int cpIdx = reader.readUnsignedShort(); // If the constant pool index is zero, then the parameter is unnamed => use null methodParameterNames[k] = cpIdx == 0 ? null : getConstantPoolString(cpIdx); - methodParameterModifiers[k] = inputStreamOrByteBuffer.readUnsignedShort(); + methodParameterModifiers[k] = reader.readUnsignedShort(); } } else if (constantPoolStringEquals(attributeNameCpIdx, "Signature")) { // Add type params to method type signature - methodTypeSignature = getConstantPoolString(inputStreamOrByteBuffer.readUnsignedShort()); + methodTypeSignatureStr = getConstantPoolString(reader.readUnsignedShort()); } else if (constantPoolStringEquals(attributeNameCpIdx, "AnnotationDefault")) { if (annotationParamDefaultValues == null) { annotationParamDefaultValues = new AnnotationParameterValueList(); @@ -1292,11 +1724,38 @@ private void readMethods() throws IOException, ClassfileFormatException { this.annotationParamDefaultValues.add(new AnnotationParameterValue(methodName, // Get annotation parameter default value readAnnotationElementValue())); + } else if (constantPoolStringEquals(attributeNameCpIdx, "Exceptions")) { + final int exceptionCount = reader.readUnsignedShort(); + thrownExceptionNames = new String[exceptionCount]; + for (int k = 0; k < exceptionCount; k++) { + final int cpIdx = reader.readUnsignedShort(); + thrownExceptionNames[k] = getConstantPoolClassName(cpIdx); + } } else if (constantPoolStringEquals(attributeNameCpIdx, "Code")) { methodHasBody = true; - inputStreamOrByteBuffer.skip(attributeLength); + reader.skip(4); // max_stack, max_locals + final int codeLength = reader.readInt(); + reader.skip(codeLength); + final int exceptionTableLength = reader.readUnsignedShort(); + reader.skip(8 * exceptionTableLength); + final int codeAttrCount = reader.readUnsignedShort(); + for (int k = 0; k < codeAttrCount; k++) { + final int codeAttrCpIdx = reader.readUnsignedShort(); + final int codeAttrLen = reader.readInt(); + if (constantPoolStringEquals(codeAttrCpIdx, "LineNumberTable")) { + final int lineNumTableLen = reader.readUnsignedShort(); + for (int l = 0; l < lineNumTableLen; l++) { + reader.skip(2); // start_pc + final int lineNum = reader.readUnsignedShort(); + minLineNum = minLineNum == 0 ? lineNum : Math.min(minLineNum, lineNum); + maxLineNum = maxLineNum == 0 ? lineNum : Math.max(maxLineNum, lineNum); + } + } else { + reader.skip(codeAttrLen); + } + } } else { - inputStreamOrByteBuffer.skip(attributeLength); + reader.skip(attributeLength); } } // Create MethodInfo @@ -1305,8 +1764,9 @@ private void readMethods() throws IOException, ClassfileFormatException { methodInfoList = new MethodInfoList(); } methodInfoList.add(new MethodInfo(className, methodName, methodAnnotationInfo, - methodModifierFlags, methodTypeDescriptor, methodTypeSignature, methodParameterNames, - methodParameterModifiers, methodParameterAnnotations, methodHasBody)); + methodModifierFlags, methodTypeDescriptor, methodTypeSignatureStr, methodParameterNames, + methodParameterModifiers, methodParameterAnnotations, methodHasBody, minLineNum, + maxLineNum, methodTypeAnnotationDecorators, thrownExceptionNames)); } } } @@ -1324,43 +1784,156 @@ private void readMethods() throws IOException, ClassfileFormatException { */ private void readClassAttributes() throws IOException, ClassfileFormatException { // Class attributes (including class annotations, class type variables, module info, etc.) - final int attributesCount = inputStreamOrByteBuffer.readUnsignedShort(); + final int attributesCount = reader.readUnsignedShort(); for (int i = 0; i < attributesCount; i++) { - final int attributeNameCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); - final int attributeLength = inputStreamOrByteBuffer.readInt(); + final int attributeNameCpIdx = reader.readUnsignedShort(); + final int attributeLength = reader.readInt(); if (scanSpec.enableAnnotationInfo // && (constantPoolStringEquals(attributeNameCpIdx, "RuntimeVisibleAnnotations") || (!scanSpec.disableRuntimeInvisibleAnnotations && constantPoolStringEquals( attributeNameCpIdx, "RuntimeInvisibleAnnotations")))) { - final int annotationCount = inputStreamOrByteBuffer.readUnsignedShort(); - for (int m = 0; m < annotationCount; m++) { + final int annotationCount = reader.readUnsignedShort(); + if (annotationCount > 0) { if (classAnnotations == null) { classAnnotations = new AnnotationInfoList(); } - classAnnotations.add(readAnnotation()); + for (int m = 0; m < annotationCount; m++) { + classAnnotations.add(readAnnotation()); + } } + } else if (scanSpec.enableAnnotationInfo // + && (constantPoolStringEquals(attributeNameCpIdx, "RuntimeVisibleTypeAnnotations") + || (!scanSpec.disableRuntimeInvisibleAnnotations && constantPoolStringEquals( + attributeNameCpIdx, "RuntimeInvisibleTypeAnnotations")))) { + final int annotationCount = reader.readUnsignedShort(); + if (annotationCount > 0) { + classTypeAnnotationDecorators = new ArrayList<>(annotationCount); + for (int m = 0; m < annotationCount; m++) { + final int targetType = reader.readUnsignedByte(); + final int typeParameterIndex; + final int supertypeIndex; + final int boundIndex; + if (targetType == 0x00) { + // Type parameter declaration of generic class or interface + typeParameterIndex = reader.readUnsignedByte(); + supertypeIndex = -1; + boundIndex = -1; + } else if (targetType == 0x10) { + // Type in extends or implements clause of class declaration (including + // the direct superclass or direct superinterface of an anonymous class + // declaration), or in extends clause of interface declaration + supertypeIndex = reader.readUnsignedShort(); + typeParameterIndex = -1; + boundIndex = -1; + } else if (targetType == 0x11) { + // Type in bound of type parameter declaration of generic class or interface + typeParameterIndex = reader.readUnsignedByte(); + boundIndex = reader.readUnsignedByte(); + supertypeIndex = -1; + } else { + throw new ClassfileFormatException("Class " + className + + " has unknown class type annotation target 0x" + + Integer.toHexString(targetType) + + ": element size unknown, cannot continue reading class. " + + "Please report this at https://github.com/classgraph/classgraph/issues"); + } + final List typePath = readTypePath(); + final AnnotationInfo annotationInfo = readAnnotation(); + classTypeAnnotationDecorators.add(new ClassTypeAnnotationDecorator() { + @Override + public void decorate(final ClassTypeSignature classTypeSignature) { + if (targetType == 0x00) { + // Type parameter declaration of generic class or interface + final List typeParameters = classTypeSignature + .getTypeParameters(); + if (typeParameters != null && typeParameterIndex < typeParameters.size()) { + typeParameters.get(typeParameterIndex).addTypeAnnotation(typePath, + annotationInfo); + } + } else if (targetType == 0x10) { + // Type in extends or implements clause of class declaration (including + // the direct superclass or direct superinterface of an anonymous class + // declaration), or in extends clause of interface declaration + if (supertypeIndex == 65535) { + // Type in extends clause of class declaration + classTypeSignature.getSuperclassSignature().addTypeAnnotation(typePath, + annotationInfo); + } else { + // Type in implements clause of interface declaration + classTypeSignature.getSuperinterfaceSignatures().get(supertypeIndex) + .addTypeAnnotation(typePath, annotationInfo); + } + } else if (targetType == 0x11) { + // Type in bound of type parameter declaration of generic class or interface + final List typeParameters = classTypeSignature + .getTypeParameters(); + if (typeParameters != null && typeParameterIndex < typeParameters.size()) { + final TypeParameter typeParameter = typeParameters.get(typeParameterIndex); + // boundIndex == 0 => class bound; boundIndex > 0 => interface bound + if (boundIndex == 0) { + final ReferenceTypeSignature classBound = typeParameter.getClassBound(); + if (classBound != null) { + classBound.addTypeAnnotation(typePath, annotationInfo); + } + } else { + final List interfaceBounds = typeParameter + .getInterfaceBounds(); + if (interfaceBounds != null + && boundIndex - 1 < interfaceBounds.size()) { + typeParameter.getInterfaceBounds().get(boundIndex - 1) + .addTypeAnnotation(typePath, annotationInfo); + } + } + } + } + } + }); + } + } + } else if (constantPoolStringEquals(attributeNameCpIdx, "Record")) { + isRecord = true; + // No need to read record_components_info entries -- there is a 1:1 correspondence between + // record components and fields/methods of the same name and type as the record component, + // so we can just rely on the field and method reading code to work correctly with records. + reader.skip(attributeLength); } else if (constantPoolStringEquals(attributeNameCpIdx, "InnerClasses")) { - final int numInnerClasses = inputStreamOrByteBuffer.readUnsignedShort(); + final int numInnerClasses = reader.readUnsignedShort(); for (int j = 0; j < numInnerClasses; j++) { - final int innerClassInfoCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); - final int outerClassInfoCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final int innerClassInfoCpIdx = reader.readUnsignedShort(); + final int outerClassInfoCpIdx = reader.readUnsignedShort(); + reader.skip(2); // inner_name_idx + final int innerClassAccessFlags = reader.readUnsignedShort(); if (innerClassInfoCpIdx != 0 && outerClassInfoCpIdx != 0) { - if (classContainmentEntries == null) { - classContainmentEntries = new ArrayList<>(); + final String innerClassName = getConstantPoolClassName(innerClassInfoCpIdx); + final String outerClassName = getConstantPoolClassName(outerClassInfoCpIdx); + if (innerClassName == null || outerClassName == null) { + // Should not happen (fix static analyzer warning) + throw new ClassfileFormatException("Inner and/or outer class name is null"); + } + if (innerClassName.equals(outerClassName)) { + // Invalid according to spec + throw new ClassfileFormatException("Inner and outer class name cannot be the same"); + } + // Record types have a Lookup inner class for boostrap methods in JDK 14 -- drop this + if (!("java.lang.invoke.MethodHandles$Lookup".equals(innerClassName) + && "java.lang.invoke.MethodHandles".equals(outerClassName))) { + // Store relationship between inner class and outer class + if (classContainmentEntries == null) { + classContainmentEntries = new ArrayList<>(); + } + classContainmentEntries.add( + new ClassContainment(innerClassName, innerClassAccessFlags, outerClassName)); } - classContainmentEntries.add(new SimpleEntry<>(getConstantPoolClassName(innerClassInfoCpIdx), - getConstantPoolClassName(outerClassInfoCpIdx))); } - inputStreamOrByteBuffer.skip(2); // inner_name_idx - inputStreamOrByteBuffer.skip(2); // inner_class_access_flags } } else if (constantPoolStringEquals(attributeNameCpIdx, "Signature")) { // Get class type signature, including type variables - typeSignature = getConstantPoolString(inputStreamOrByteBuffer.readUnsignedShort()); + typeSignatureStr = getConstantPoolString(reader.readUnsignedShort()); + } else if (constantPoolStringEquals(attributeNameCpIdx, "SourceFile")) { + sourceFile = getConstantPoolString(reader.readUnsignedShort()); } else if (constantPoolStringEquals(attributeNameCpIdx, "EnclosingMethod")) { - final String innermostEnclosingClassName = getConstantPoolClassName( - inputStreamOrByteBuffer.readUnsignedShort()); - final int enclosingMethodCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final String innermostEnclosingClassName = getConstantPoolClassName(reader.readUnsignedShort()); + final int enclosingMethodCpIdx = reader.readUnsignedShort(); String definingMethodName; if (enclosingMethodCpIdx == 0) { // A cpIdx of 0 (which is an invalid value) is used for anonymous inner classes declared in @@ -1374,18 +1947,19 @@ private void readClassAttributes() throws IOException, ClassfileFormatException if (classContainmentEntries == null) { classContainmentEntries = new ArrayList<>(); } - classContainmentEntries.add(new SimpleEntry<>(className, innermostEnclosingClassName)); + classContainmentEntries + .add(new ClassContainment(className, classModifiers, innermostEnclosingClassName)); // Also store the fully-qualified name of the enclosing method, to mark this as an anonymous inner // class this.fullyQualifiedDefiningMethodName = innermostEnclosingClassName + "." + definingMethodName; } else if (constantPoolStringEquals(attributeNameCpIdx, "Module")) { - final int moduleNameCpIdx = inputStreamOrByteBuffer.readUnsignedShort(); + final int moduleNameCpIdx = reader.readUnsignedShort(); classpathElement.moduleNameFromModuleDescriptor = getConstantPoolString(moduleNameCpIdx); // (Future work): parse the rest of the module descriptor fields, and add to ModuleInfo: // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.25 - inputStreamOrByteBuffer.skip(attributeLength - 2); + reader.skip(attributeLength - 2); } else { - inputStreamOrByteBuffer.skip(attributeLength); + reader.skip(attributeLength); } } } @@ -1401,14 +1975,20 @@ private void readClassAttributes() throws IOException, ClassfileFormatException * the classpath element * @param classpathOrder * the classpath order - * @param classNamesScheduledForScanning - * the class names scheduled for scanning + * @param acceptedClassNamesFound + * the names of accepted classes found in the classpath while scanning paths within classpath + * elements. + * @param classNamesScheduledForExtendedScanning + * the names of external (non-accepted) classes scheduled for extended scanning (where scanning is + * extended upwards to superclasses, interfaces and annotations). * @param relativePath * the relative path * @param classfileResource * the classfile resource * @param isExternalClass * if this is an external class + * @param stringInternMap + * the string intern map * @param workQueue * the work queue * @param scanSpec @@ -1424,36 +2004,36 @@ private void readClassAttributes() throws IOException, ClassfileFormatException * false) */ Classfile(final ClasspathElement classpathElement, final List classpathOrder, - final Set classNamesScheduledForScanning, final String relativePath, - final Resource classfileResource, final boolean isExternalClass, + final Set acceptedClassNamesFound, final Set classNamesScheduledForExtendedScanning, + final String relativePath, final Resource classfileResource, final boolean isExternalClass, + final ConcurrentHashMap stringInternMap, final WorkQueue workQueue, final ScanSpec scanSpec, final LogNode log) throws IOException, ClassfileFormatException, SkipClassException { this.classpathElement = classpathElement; this.classpathOrder = classpathOrder; this.relativePath = relativePath; - this.classNamesScheduledForScanning = classNamesScheduledForScanning; + this.acceptedClassNamesFound = acceptedClassNamesFound; + this.classNamesScheduledForExtendedScanning = classNamesScheduledForExtendedScanning; this.classfileResource = classfileResource; this.isExternalClass = isExternalClass; + this.stringInternMap = stringInternMap; this.scanSpec = scanSpec; - this.log = log; - try { - // Open classfile as a ByteBuffer or InputStream - inputStreamOrByteBuffer = classfileResource.openOrRead(); + // Open a BufferedSequentialReader for the classfile + try (ClassfileReader classfileReader = classfileResource.openClassfile()) { + reader = classfileReader; // Check magic number - if (inputStreamOrByteBuffer.readInt() != 0xCAFEBABE) { + if (reader.readInt() != 0xCAFEBABE) { throw new ClassfileFormatException("Classfile does not have correct magic number"); } - // Read classfile minor version - inputStreamOrByteBuffer.readUnsignedShort(); - - // Read classfile major version - inputStreamOrByteBuffer.readUnsignedShort(); + // Read classfile minor and major version + minorVersion = reader.readUnsignedShort(); + majorVersion = reader.readUnsignedShort(); // Read the constant pool - readConstantPoolEntries(); + readConstantPoolEntries(log); // Read basic class info ( readBasicClassInfo(); @@ -1470,38 +2050,24 @@ private void readClassAttributes() throws IOException, ClassfileFormatException // Read class attributes readClassAttributes(); - } finally { - // Close ByteBuffer or InputStream - classfileResource.close(); - inputStreamOrByteBuffer = null; - } - - // Check if any superclasses, interfaces or annotations are external (non-whitelisted) classes - // that need to be scheduled for scanning, so that all of the "upwards" direction of the class - // graph is scanned for any whitelisted class, even if the superclasses / interfaces / annotations - // are not themselves whitelisted. - if (scanSpec.extendScanningUpwardsToExternalClasses) { - extendScanningUpwards(); - // If any external classes were found, schedule them for scanning - if (additionalWorkUnits != null) { - workQueue.addWorkUnits(additionalWorkUnits); - } + reader = null; } // Write class info to log - if (log != null) { - final LogNode subLog = log.log("Found " // - + (isAnnotation ? "annotation class" : isInterface ? "interface class" : "class") // - + " " + className); + final LogNode subLog = log == null ? null + : log.log("Found " // + + (isAnnotation ? "annotation class" : isInterface ? "interface class" : "class") // + + " " + className); + if (subLog != null) { if (superclassName != null) { subLog.log( "Super" + (isInterface && !isAnnotation ? "interface" : "class") + ": " + superclassName); } if (implementedInterfaces != null) { - subLog.log("Interfaces: " + Join.join(", ", implementedInterfaces)); + subLog.log("Interfaces: " + StringUtils.join(", ", implementedInterfaces)); } if (classAnnotations != null) { - subLog.log("Class annotations: " + Join.join(", ", classAnnotations)); + subLog.log("Class annotations: " + StringUtils.join(", ", classAnnotations)); } if (annotationParamDefaultValues != null) { for (final AnnotationParameterValue apv : annotationParamDefaultValues) { @@ -1510,29 +2076,36 @@ private void readClassAttributes() throws IOException, ClassfileFormatException } if (fieldInfoList != null) { for (final FieldInfo fieldInfo : fieldInfoList) { - subLog.log("Field: " + fieldInfo); + final String modifierStr = fieldInfo.getModifiersStr(); + subLog.log("Field: " + modifierStr + (modifierStr.isEmpty() ? "" : " ") + fieldInfo.getName()); } } if (methodInfoList != null) { for (final MethodInfo methodInfo : methodInfoList) { - subLog.log("Method: " + methodInfo); + final String modifierStr = methodInfo.getModifiersStr(); + subLog.log( + "Method: " + modifierStr + (modifierStr.isEmpty() ? "" : " ") + methodInfo.getName()); } } - if (typeSignature != null) { - ClassTypeSignature typeSig = null; - try { - typeSig = ClassTypeSignature.parse(typeSignature, /* classInfo = */ null); - } catch (final ParseException e) { - // Ignore - } - subLog.log("Class type signature: " + (typeSig == null ? typeSignature - : typeSig.toString(className, /* typeNameOnly = */ false, classModifiers, isAnnotation, - isInterface))); + if (typeSignatureStr != null) { + subLog.log("Class type signature: " + typeSignatureStr); } if (refdClassNames != null) { final List refdClassNamesSorted = new ArrayList<>(refdClassNames); - Collections.sort(refdClassNamesSorted); - subLog.log("Referenced class names: " + Join.join(", ", refdClassNamesSorted)); + CollectionUtils.sortIfNotEmpty(refdClassNamesSorted); + subLog.log("Additional referenced class names: " + StringUtils.join(", ", refdClassNamesSorted)); + } + } + + // Check if any superclasses, interfaces or annotations are external (non-accepted) classes + // that need to be scheduled for scanning, so that all of the "upwards" direction of the class + // graph is scanned for any accepted class, even if the superclasses / interfaces / annotations + // are not themselves accepted. + if (scanSpec.extendScanningUpwardsToExternalClasses) { + extendScanningUpwards(subLog); + // If any external classes were found, schedule them for scanning + if (additionalWorkUnits != null) { + workQueue.addWorkUnits(additionalWorkUnits); } } } diff --git a/src/main/java/io/github/classgraph/ClasspathElement.java b/src/main/java/io/github/classgraph/ClasspathElement.java index 5f93f51ae..84fe51126 100644 --- a/src/main/java/io/github/classgraph/ClasspathElement.java +++ b/src/main/java/io/github/classgraph/ClasspathElement.java @@ -34,23 +34,24 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import io.github.classgraph.Scanner.ClasspathEntryWorkUnit; -import nonapi.io.github.classgraph.ScanSpec; -import nonapi.io.github.classgraph.ScanSpec.ScanSpecPathMatch; import nonapi.io.github.classgraph.concurrency.WorkQueue; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.scanspec.ScanSpec.ScanSpecPathMatch; import nonapi.io.github.classgraph.utils.FileUtils; import nonapi.io.github.classgraph.utils.JarUtils; import nonapi.io.github.classgraph.utils.LogNode; /** A classpath element (a directory or jarfile on the classpath). */ -abstract class ClasspathElement { +abstract class ClasspathElement implements Comparable { + /** The index of the classpath element within the classpath or module path. */ + int classpathElementIdx; + /** * If non-null, contains a list of resolved paths for any classpath element roots nested inside this classpath * element. (Scanning should stop at a nested classpath element root, otherwise that subtree will be scanned @@ -66,26 +67,34 @@ abstract class ClasspathElement { */ boolean skipClasspathElement; - /** True if classpath element contains a specifically-whitelisted resource path. */ - boolean containsSpecificallyWhitelistedClasspathElementResourcePath; + /** True if classpath element contains a specifically-accepted resource path. */ + boolean containsSpecificallyAcceptedClasspathElementResourcePath; + + /** + * The index of the classpath element within the parent classpath element (e.g. for classpath elements added via + * a Class-Path entry in the manifest). Set to -1 initially in case the same ClasspathElement is present twice + * in the classpath, as a child of different parent ClasspathElements. + */ + final int classpathElementIdxWithinParent; /** * The child classpath elements, keyed by the order of the child classpath element within the Class-Path entry * of the manifest file the child classpath element was listed in (or the position of the file within the sorted * entries of a lib directory). */ - final Queue> childClasspathElementsIndexed = new ConcurrentLinkedQueue<>(); + Collection childClasspathElements = new ConcurrentLinkedQueue<>(); /** - * The child classpath elements, ordered by order within the parent classpath element. + * Resources found within this classpath element that were accepted and not rejected. (Only written by one + * thread, so doesn't need to be a concurrent list.) */ - List childClasspathElementsOrdered; - - /** The list of all resources found within this classpath element that were whitelisted and not blacklisted. */ - protected final Collection whitelistedResources = new ConcurrentLinkedQueue<>(); + protected final List acceptedResources = new ArrayList<>(); - /** The list of all classfiles found within this classpath element that were whitelisted and not blacklisted. */ - protected Collection whitelistedClassfileResources = new ConcurrentLinkedQueue<>(); + /** + * The list of all classfiles found within this classpath element that were accepted and not rejected. (Only + * written by one thread, so doesn't need to be a concurrent list.) + */ + protected List acceptedClassfileResources = new ArrayList<>(); /** The map from File to last modified timestamp, if scanFiles is true. */ protected final Map fileToLastModified = new ConcurrentHashMap<>(); @@ -96,6 +105,9 @@ abstract class ClasspathElement { /** The classloader that this classpath element was obtained from. */ protected ClassLoader classLoader; + /** The package root within the jarfile or Path. */ + protected String packageRootPrefix; + /** * The name of the module from the {@code module-info.class} module descriptor, if one is present in the root of * the classpath element. @@ -105,23 +117,43 @@ abstract class ClasspathElement { /** The scan spec. */ final ScanSpec scanSpec; + /** The ScanResult that the classpath element came from. */ + protected ScanResult scanResult; + // ------------------------------------------------------------------------------------------------------------- /** * A classpath element. * - * @param classLoader - * the classloader + * @param workUnit + * the work unit * @param scanSpec * the scan spec */ - ClasspathElement(final ClassLoader classLoader, final ScanSpec scanSpec) { - this.classLoader = classLoader; + ClasspathElement(final ClasspathEntryWorkUnit workUnit, final ScanSpec scanSpec) { + this.packageRootPrefix = workUnit.packageRootPrefix; + this.classpathElementIdxWithinParent = workUnit.classpathElementIdxWithinParent; + this.classLoader = workUnit.classLoader; this.scanSpec = scanSpec; } // ------------------------------------------------------------------------------------------------------------- + /** Used to set the ScanResult after the scan is complete. */ + void setScanResult(final ScanResult scanResult) { + this.scanResult = scanResult; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** Sort in increasing order of classpathElementIdxWithinParent. */ + @Override + public int compareTo(final ClasspathElement other) { + return this.classpathElementIdxWithinParent - other.classpathElementIdxWithinParent; + } + + // ------------------------------------------------------------------------------------------------------------- + /** * Get the ClassLoader the classpath element was obtained from. * @@ -137,37 +169,37 @@ ClassLoader getClassLoader() { * @return the num classfile matches */ int getNumClassfileMatches() { - return whitelistedClassfileResources == null ? 0 : whitelistedClassfileResources.size(); + return acceptedClassfileResources == null ? 0 : acceptedClassfileResources.size(); } // ------------------------------------------------------------------------------------------------------------- /** - * Check relativePath against classpathElementResourcePathWhiteBlackList. + * Check relativePath against classpathElementResourcePathAcceptReject. * * @param relativePath * the relative path * @param log * the log + * @return true if path should be scanned */ - protected void checkResourcePathWhiteBlackList(final String relativePath, final LogNode log) { - // Whitelist/blacklist classpath elements based on file resource paths - if (!scanSpec.classpathElementResourcePathWhiteBlackList.whitelistAndBlacklistAreEmpty()) { - if (scanSpec.classpathElementResourcePathWhiteBlackList.isBlacklisted(relativePath)) { + protected boolean checkResourcePathAcceptReject(final String relativePath, final LogNode log) { + // Accept/reject classpath elements based on file resource paths + if (!scanSpec.classpathElementResourcePathAcceptReject.acceptAndRejectAreEmpty()) { + if (scanSpec.classpathElementResourcePathAcceptReject.isRejected(relativePath)) { if (log != null) { - log.log("Reached blacklisted classpath element resource path, stopping scanning: " - + relativePath); + log.log("Reached rejected classpath element resource path, stopping scanning: " + relativePath); } - skipClasspathElement = true; - return; + return false; } - if (scanSpec.classpathElementResourcePathWhiteBlackList.isSpecificallyWhitelisted(relativePath)) { + if (scanSpec.classpathElementResourcePathAcceptReject.isSpecificallyAccepted(relativePath)) { if (log != null) { - log.log("Reached specifically whitelisted classpath element resource path: " + relativePath); + log.log("Reached specifically accepted classpath element resource path: " + relativePath); } - containsSpecificallyWhitelistedClasspathElementResourcePath = true; + containsSpecificallyAcceptedClasspathElementResourcePath = true; } } + return true; } // ------------------------------------------------------------------------------------------------------------- @@ -184,23 +216,21 @@ protected void checkResourcePathWhiteBlackList(final String relativePath, final * the log */ void maskClassfiles(final int classpathIdx, final Set classpathRelativePathsFound, final LogNode log) { - if (!scanSpec.performScan) { - // Should not happen - throw new IllegalArgumentException("performScan is false"); - } // Find relative paths that occur more than once in the classpath / module path. // Usually duplicate relative paths occur only between classpath / module path elements, not within, // but actually there is no restriction for paths within a zipfile to be unique, and in fact // zipfiles in the wild do contain the same classfiles multiple times with the same exact path, // e.g.: xmlbeans-2.6.0.jar!org/apache/xmlbeans/xml/stream/Location.class - final List whitelistedClassfileResourcesFiltered = new ArrayList<>( - whitelistedClassfileResources.size()); + final List acceptedClassfileResourcesFiltered = new ArrayList<>( + acceptedClassfileResources.size()); boolean foundMasked = false; - for (final Resource res : whitelistedClassfileResources) { + for (final Resource res : acceptedClassfileResources) { final String pathRelativeToPackageRoot = res.getPath(); - // Don't mask module-info.class or package-info.class, these are read for every module/package + // Don't mask module-info.class or package-info.class, these are read for every module/package, + // and they don't result in a ClassInfo object, so there will be no duplicate ClassInfo objects + // created, even if they are encountered multiple times. Instead, any annotations on modules or + // packages are merged into the appropriate ModuleInfo / PackageInfo object. if (!pathRelativeToPackageRoot.equals("module-info.class") - && !pathRelativeToPackageRoot.endsWith("/module-info.class") && !pathRelativeToPackageRoot.equals("package-info.class") && !pathRelativeToPackageRoot.endsWith("/package-info.class") // Check if pathRelativeToPackageRoot has been seen before @@ -213,14 +243,14 @@ void maskClassfiles(final int classpathIdx, final Set classpathRelativeP + JarUtils.classfilePathToClassName(pathRelativeToPackageRoot) + " found at " + res); } } else { - whitelistedClassfileResourcesFiltered.add(res); + acceptedClassfileResourcesFiltered.add(res); } } if (foundMasked) { // Remove masked (duplicated) paths. N.B. this replaces the concurrent collection with a non-concurrent // collection, but this is the last time the collection is changed during a scan, and this method is // run from a single thread. - whitelistedClassfileResources = whitelistedClassfileResourcesFiltered; + acceptedClassfileResources = acceptedClassfileResourcesFiltered; } } @@ -233,46 +263,51 @@ void maskClassfiles(final int classpathIdx, final Set classpathRelativeP * the resource * @param parentMatchStatus * the parent match status + * @param isClassfileOnly + * if true, only add the resource to the list of classfile resources, not to the list of + * non-classfile resources * @param log * the log */ - protected void addWhitelistedResource(final Resource resource, final ScanSpecPathMatch parentMatchStatus, - final LogNode log) { + protected void addAcceptedResource(final Resource resource, final ScanSpecPathMatch parentMatchStatus, + final boolean isClassfileOnly, final LogNode log) { final String path = resource.getPath(); final boolean isClassFile = FileUtils.isClassfile(path); - boolean isWhitelisted = false; + boolean isAccepted = false; if (isClassFile) { - // Check classfile scanning is enabled, and classfile is not specifically blacklisted - if (scanSpec.enableClassInfo && !scanSpec.classfilePathWhiteBlackList.isBlacklisted(path)) { - // ClassInfo is enabled, and found a whitelisted classfile - whitelistedClassfileResources.add(resource); - isWhitelisted = true; + // Check classfile scanning is enabled, and classfile is not specifically rejected + if (scanSpec.enableClassInfo && !scanSpec.classfilePathAcceptReject.isRejected(path)) { + // ClassInfo is enabled, and found an accepted classfile + acceptedClassfileResources.add(resource); + isAccepted = true; } } else { - // Resources are always whitelisted if found in whitelisted directories - isWhitelisted = true; + // Resources are always accepted if found in accepted directories + isAccepted = true; } - // Add resource to whitelistedResources, whether for a classfile or non-classfile resource - whitelistedResources.add(resource); + if (!isClassfileOnly) { + // Add resource to list of accepted resources, whether for a classfile or non-classfile resource + acceptedResources.add(resource); + } // Write to log if enabled, and as long as classfile scanning is not disabled, and this is not - // a blacklisted classfile - if (log != null && isWhitelisted) { + // a rejected classfile + if (log != null && isAccepted) { final String type = isClassFile ? "classfile" : "resource"; String logStr; switch (parentMatchStatus) { - case HAS_WHITELISTED_PATH_PREFIX: - logStr = "Found " + type + " within subpackage of whitelisted package: "; + case HAS_ACCEPTED_PATH_PREFIX: + logStr = "Found " + type + " within subpackage of accepted package: "; break; - case AT_WHITELISTED_PATH: - logStr = "Found " + type + " within whitelisted package: "; + case AT_ACCEPTED_PATH: + logStr = "Found " + type + " within accepted package: "; break; - case AT_WHITELISTED_CLASS_PACKAGE: - logStr = "Found specifically-whitelisted " + type + ": "; + case AT_ACCEPTED_CLASS_PACKAGE: + logStr = "Found specifically-accepted " + type + ": "; break; default: - logStr = "Found whitelisted " + type + ": "; + logStr = "Found accepted " + type + ": "; break; } // Precede log entry sort key with "0:file:" so that file entries come before dir entries for @@ -293,13 +328,13 @@ protected void addWhitelistedResource(final Resource resource, final ScanSpecPat */ protected void finishScanPaths(final LogNode log) { if (log != null) { - if (whitelistedResources.isEmpty() && whitelistedClassfileResources.isEmpty()) { - log.log(scanSpec.enableClassInfo ? "No whitelisted classfiles or resources found" - : "Classfile scanning is disabled, and no whitelisted resources found"); - } else if (whitelistedResources.isEmpty()) { - log.log("No whitelisted resources found"); - } else if (whitelistedClassfileResources.isEmpty()) { - log.log(scanSpec.enableClassInfo ? "No whitelisted classfiles found" + if (acceptedResources.isEmpty() && acceptedClassfileResources.isEmpty()) { + log.log(scanSpec.enableClassInfo ? "No accepted classfiles or resources found" + : "Classfile scanning is disabled, and no accepted resources found"); + } else if (acceptedResources.isEmpty()) { + log.log("No accepted resources found"); + } else if (acceptedClassfileResources.isEmpty()) { + log.log(scanSpec.enableClassInfo ? "No accepted classfiles found" : "Classfile scanning is disabled"); } } @@ -310,6 +345,40 @@ protected void finishScanPaths(final LogNode log) { // ------------------------------------------------------------------------------------------------------------- + /** + * Write entries to log in classpath / module path order. + * + * @param classpathElementIdx + * the classpath element idx + * @param msg + * the log message + * @param log + * the log + * @return the new {@link LogNode} + */ + protected LogNode log(final int classpathElementIdx, final String msg, final LogNode log) { + return log.log(String.format("%07d", classpathElementIdx), msg); + } + + /** + * Write entries to log in classpath / module path order. + * + * @param classpathElementIdx + * the classpath element idx + * @param msg + * the log message + * @param t + * The exception that was thrown + * @param log + * the log + * @return the new {@link LogNode} + */ + protected LogNode log(final int classpathElementIdx, final String msg, final Throwable t, final LogNode log) { + return log.log(String.format("%07d", classpathElementIdx), msg, t); + } + + // ------------------------------------------------------------------------------------------------------------- + /** * Determine if this classpath element is valid. If it is not valid, sets skipClasspathElement. For * {@link ClasspathElementZip}, may also open or extract inner jars, and also causes jarfile manifests to be @@ -327,8 +396,8 @@ abstract void open(final WorkQueue workQueue, final LogN throws InterruptedException; /** - * Scan paths in the classpath element for whitelist/blacklist criteria, creating Resource objects for - * whitelisted and non-blacklisted resources and classfiles. + * Scan paths in the classpath element for accept/reject criteria, creating Resource objects for accepted and + * non-rejected resources and classfiles. * * @param log * the log @@ -354,6 +423,14 @@ abstract void open(final WorkQueue workQueue, final LogN */ abstract URI getURI(); + /** + * Get the URI for this classpath element, and the URIs for any automatic nested package prefixes (e.g. + * "spring-boot.jar/BOOT-INF/classes") within this jarfile. + * + * @return the URI for the classpath element. + */ + abstract List getAllURIs(); + /** * Get the file for this classpath element, or null if this is a module with a "jrt:" URI. * diff --git a/src/main/java/io/github/classgraph/ClasspathElementDir.java b/src/main/java/io/github/classgraph/ClasspathElementDir.java index 210ee030c..1468542e6 100644 --- a/src/main/java/io/github/classgraph/ClasspathElementDir.java +++ b/src/main/java/io/github/classgraph/ClasspathElementDir.java @@ -29,54 +29,66 @@ package io.github.classgraph; import java.io.File; -import java.io.FileNotFoundException; +import java.io.IOError; import java.io.IOException; import java.io.InputStream; -import java.io.RandomAccessFile; import java.net.URI; import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; +import java.nio.file.DirectoryStream; import java.nio.file.Files; -import java.util.AbstractMap.SimpleEntry; -import java.util.Arrays; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import io.github.classgraph.Scanner.ClasspathEntryWorkUnit; -import nonapi.io.github.classgraph.ScanSpec; -import nonapi.io.github.classgraph.ScanSpec.ScanSpecPathMatch; import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandlerRegistry; import nonapi.io.github.classgraph.concurrency.WorkQueue; +import nonapi.io.github.classgraph.fastzipfilereader.LogicalZipFile; +import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; +import nonapi.io.github.classgraph.fileslice.PathSlice; +import nonapi.io.github.classgraph.fileslice.reader.ClassfileReader; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.scanspec.ScanSpec.ScanSpecPathMatch; +import nonapi.io.github.classgraph.utils.FastPathResolver; import nonapi.io.github.classgraph.utils.FileUtils; -import nonapi.io.github.classgraph.utils.InputStreamOrByteBufferAdapter; import nonapi.io.github.classgraph.utils.LogNode; +import nonapi.io.github.classgraph.utils.VersionFinder; -/** A directory classpath element. */ +/** A directory classpath element, using the {@link Path} API. */ class ClasspathElementDir extends ClasspathElement { /** The directory at the root of the classpath element. */ - private final File classpathEltDir; - - /** The number of characters to ignore to strip the classpath element path and relativize the path. */ - private final int ignorePrefixLen; + private final Path classpathEltPath; /** Used to ensure that recursive scanning doesn't get into an infinite loop due to a link cycle. */ - private final Set scannedCanonicalPaths = new HashSet<>(); + private final Set scannedCanonicalPaths = new HashSet<>(); + + /** The nested jar handler. */ + private final NestedJarHandler nestedJarHandler; /** * A directory classpath element. * - * @param classpathEltDir - * the classpath element directory - * @param classLoader - * the classloader + * @param workUnit + * the work unit -- workUnit.classpathEntryObj must be a {@link Path} object + * @param nestedJarHandler + * the nested jar handler * @param scanSpec * the scan spec */ - ClasspathElementDir(final File classpathEltDir, final ClassLoader classLoader, final ScanSpec scanSpec) { - super(classLoader, scanSpec); - this.classpathEltDir = classpathEltDir; - this.ignorePrefixLen = classpathEltDir.getPath().length() + 1; + ClasspathElementDir(final ClasspathEntryWorkUnit workUnit, final NestedJarHandler nestedJarHandler, + final ScanSpec scanSpec) { + super(workUnit, scanSpec); + this.classpathEltPath = (Path) workUnit.classpathEntryObj; + this.nestedJarHandler = nestedJarHandler; } /* (non-Javadoc) @@ -87,7 +99,8 @@ class ClasspathElementDir extends ClasspathElement { void open(final WorkQueue workQueue, final LogNode log) { if (!scanSpec.scanDirs) { if (log != null) { - log.log("Skipping classpath element, since dir scanning is disabled: " + classpathEltDir); + log(classpathElementIdx, + "Skipping classpath element, since dir scanning is disabled: " + classpathEltPath, log); } skipClasspathElement = true; return; @@ -96,182 +109,191 @@ void open(final WorkQueue workQueue, final LogNode log) // Auto-add nested lib dirs int childClasspathEntryIdx = 0; for (final String libDirPrefix : ClassLoaderHandlerRegistry.AUTOMATIC_LIB_DIR_PREFIXES) { - final File libDir = new File(classpathEltDir, libDirPrefix); - if (libDir.exists() && libDir.isDirectory()) { - // Sort directory entries for consistency - final File[] listFiles = libDir.listFiles(); - if (listFiles != null) { - Arrays.sort(listFiles); - // Add all jarfiles within lib dir as child classpath entries - for (final File file : listFiles) { - if (file.isFile() && file.getName().endsWith(".jar")) { - if (log != null) { - log.log("Found lib jar: " + file); + final Path libDirPath = classpathEltPath.resolve(libDirPrefix); + if (FileUtils.canReadAndIsDir(libDirPath)) { + // Add all jarfiles within the lib dir as child classpath entries + try (DirectoryStream stream = Files.newDirectoryStream(libDirPath, + new DirectoryStream.Filter() { + @Override + public boolean accept(Path filePath) { + return filePath.toString().toLowerCase().endsWith(".jar") + && Files.isRegularFile(filePath); } - workQueue.addWorkUnit(new ClasspathEntryWorkUnit( - /* rawClasspathEntry = */ // - new SimpleEntry<>(file.getPath(), classLoader), - /* parentClasspathElement = */ this, - /* orderWithinParentClasspathElement = */ childClasspathEntryIdx++)); + })) { + for (final Path filePath : stream) { + if (log != null) { + log(classpathElementIdx, "Found lib jar: " + filePath, log); } + workQueue.addWorkUnit(new ClasspathEntryWorkUnit(filePath, getClassLoader(), + /* parentClasspathElement = */ this, + /* orderWithinParentClasspathElement = */ childClasspathEntryIdx++, + /* packageRootPrefix = */ "")); } + } catch (final IOException e) { + // Ignore -- thrown by Files.newDirectoryStream } } } - for (final String packageRootPrefix : ClassLoaderHandlerRegistry.AUTOMATIC_PACKAGE_ROOT_PREFIXES) { - final File packageRootDir = new File(classpathEltDir, packageRootPrefix); - if (packageRootDir.exists() && packageRootDir.isDirectory()) { - if (log != null) { - log.log("Found package root: " + packageRootDir); + // Only look for package roots if the package root is empty + if (packageRootPrefix.isEmpty()) { + for (final String packageRootPrefix : ClassLoaderHandlerRegistry.AUTOMATIC_PACKAGE_ROOT_PREFIXES) { + final Path packageRoot = classpathEltPath.resolve(packageRootPrefix); + if (FileUtils.canReadAndIsDir(packageRoot)) { + if (log != null) { + log(classpathElementIdx, "Found package root: " + packageRootPrefix, log); + } + workQueue.addWorkUnit(new ClasspathEntryWorkUnit(packageRoot, getClassLoader(), + /* parentClasspathElement = */ this, + /* orderWithinParentClasspathElement = */ childClasspathEntryIdx++, + packageRootPrefix)); } - workQueue - .addWorkUnit(new ClasspathEntryWorkUnit( - /* rawClasspathEntry = */ new SimpleEntry<>(packageRootDir.getPath(), - classLoader), - /* parentClasspathElement = */ this, - /* orderWithinParentClasspathElement = */ childClasspathEntryIdx++)); } } } catch (final SecurityException e) { if (log != null) { - log.log("Skipping classpath element, since dir cannot be accessed: " + classpathEltDir); + log(classpathElementIdx, + "Skipping classpath element, since dir cannot be accessed: " + classpathEltPath, log); } skipClasspathElement = true; - return; } } /** * Create a new {@link Resource} object for a resource or classfile discovered while scanning paths. * - * @param classpathEltDir - * the classpath element directory - * @param relativePath - * the relative path - * @param classpathResourceFile - * the classpath resource file + * @param resourcePath + * the {@link Path} for the resource * @return the resource */ - private Resource newResource(final File classpathEltDir, final String relativePath, - final File classpathResourceFile) { - return new Resource(this, classpathResourceFile.length()) { - /** The random access file. */ - private RandomAccessFile randomAccessFile; + private Resource newResource(final Path resourcePath, final BasicFileAttributes attributes) { + final int notYetLoadedLength = -2; + return new Resource(this, attributes == null ? notYetLoadedLength : attributes.size()) { + /** The {@link PathSlice} opened on the file. */ + private PathSlice pathSlice; - /** The file channel. */ - private FileChannel fileChannel; + /** True if the resource is open. */ + private final AtomicBoolean isOpen = new AtomicBoolean(); + + @Override + public long getLength() { + if (length == notYetLoadedLength) { + try { + length = Files.size(resourcePath); + } catch (IOException | SecurityException e) { + length = -1; + } + } + return length; + } @Override public String getPath() { - return relativePath; + String path = FastPathResolver.resolve(classpathEltPath.relativize(resourcePath).toString()); + while (path.startsWith("/")) { + path = path.substring(1); + } + return path; } @Override public String getPathRelativeToClasspathElement() { - return relativePath; + return packageRootPrefix.isEmpty() ? getPath() : packageRootPrefix + getPath(); } @Override - public synchronized ByteBuffer read() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Parent directory could not be opened"); + public long getLastModified() { + try { + return attributes == null ? resourcePath.toFile().lastModified() + : attributes.lastModifiedTime().toMillis(); + } catch (final UnsupportedOperationException e) { + return 0L; } - markAsOpen(); + } + + @SuppressWarnings("null") + @Override + public Set getPosixFilePermissions() { + Set posixFilePermissions = null; try { - randomAccessFile = new RandomAccessFile(classpathResourceFile, "r"); - fileChannel = randomAccessFile.getChannel(); - MappedByteBuffer buffer = null; - try { - buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); - } catch (final FileNotFoundException e) { - throw e; - } catch (IOException | OutOfMemoryError e) { - // If map failed, try calling System.gc() to free some allocated MappedByteBuffers - // (there is a limit to the number of mapped files -- 64k on Linux) - // See: http://www.mapdb.org/blog/mmap_files_alloc_and_jvm_crash/ - System.gc(); - // Then try calling map again - buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); + if (attributes instanceof PosixFileAttributes) { + posixFilePermissions = ((PosixFileAttributes) attributes).permissions(); + } else { + posixFilePermissions = Files.readAttributes(resourcePath, PosixFileAttributes.class) + .permissions(); } - byteBuffer = buffer; - length = byteBuffer.remaining(); - return byteBuffer; - } catch (final IOException | SecurityException | OutOfMemoryError e) { - close(); - throw new IOException("Could not open " + this, e); + } catch (UnsupportedOperationException | IOException | SecurityException e) { + // POSIX attributes not supported } + return posixFilePermissions; } - @Override - synchronized InputStreamOrByteBufferAdapter openOrRead() throws IOException { - if (length >= FileUtils.FILECHANNEL_FILE_SIZE_THRESHOLD) { - return new InputStreamOrByteBufferAdapter(read()); - } else { - return new InputStreamOrByteBufferAdapter(inputStream = new InputStreamResourceCloser(this, - Files.newInputStream(classpathResourceFile.toPath()))); + protected void checkCanOpen() { + if (skipClasspathElement) { + // Shouldn't happen + throw new IllegalStateException("Classpath element could not be opened"); + } + if (isOpen.getAndSet(true)) { + throw new IllegalStateException( + "Resource is already open -- cannot open it again without first calling close()"); + } + if (scanResult != null && scanResult.isClosed()) { + throw new IllegalStateException("Cannot open a resource after the ScanResult is closed"); } } @Override - public synchronized InputStream open() throws IOException { - if (length >= FileUtils.FILECHANNEL_FILE_SIZE_THRESHOLD) { - read(); - return inputStream = new InputStreamResourceCloser(this, byteBufferToInputStream()); - } else { - markAsOpen(); - try { - return inputStream = new InputStreamResourceCloser(this, - Files.newInputStream(classpathResourceFile.toPath())); - } catch (final IOException | SecurityException e) { - close(); - throw new IOException("Could not open " + this, e); - } - } + public ByteBuffer read() throws IOException { + openAndCreateSlice(); + byteBuffer = pathSlice.read(); + return byteBuffer; + } + + @Override + ClassfileReader openClassfile() throws IOException { + // Classfile won't be compressed, so wrap it in a new PathSlice and then open it + openAndCreateSlice(); + return new ClassfileReader(pathSlice, this); + } + + @Override + public InputStream open() throws IOException { + openAndCreateSlice(); + inputStream = pathSlice.open(this); + return inputStream; } @Override - public synchronized byte[] load() throws IOException { + public byte[] load() throws IOException { try { - final byte[] byteArray; - if (length >= FileUtils.FILECHANNEL_FILE_SIZE_THRESHOLD) { - read(); - byteArray = byteBufferToByteArray(); - } else { - open(); - byteArray = FileUtils.readAllBytesAsArray(inputStream, length); - } - length = byteArray.length; - return byteArray; + openAndCreateSlice(); + return pathSlice.load(); } finally { close(); } } @Override - public synchronized void close() { - super.close(); // Close inputStream - if (byteBuffer != null) { - FileUtils.closeDirectByteBuffer(byteBuffer, /* log = */ null); - byteBuffer = null; - } - if (fileChannel != null) { - try { - fileChannel.close(); - } catch (final IOException e) { - // Ignore + public void close() { + if (isOpen.getAndSet(false)) { + if (byteBuffer != null) { + // Any ByteBuffer ref should be a duplicate, so it doesn't need to be cleaned + byteBuffer = null; } - fileChannel = null; - } - if (randomAccessFile != null) { - try { - randomAccessFile.close(); - } catch (final IOException e) { - // Ignore + if (pathSlice != null) { + pathSlice.close(); + nestedJarHandler.markSliceAsClosed(pathSlice); + pathSlice = null; } - randomAccessFile = null; + + // Close inputStream + super.close(); } - markAsClosed(); + } + + private void openAndCreateSlice() throws IOException { + checkCanOpen(); + pathSlice = new PathSlice(resourcePath, false, 0L, nestedJarHandler, false); + length = pathSlice.sliceLength; } }; } @@ -286,140 +308,184 @@ public synchronized void close() { */ @Override Resource getResource(final String relativePath) { - final File resourceFile = new File(classpathEltDir, relativePath); - return resourceFile.canRead() && resourceFile.isFile() - ? newResource(classpathEltDir, relativePath, resourceFile) - : null; + final Path resourcePath = classpathEltPath.resolve(relativePath); + return FileUtils.canReadAndIsFile(resourcePath) ? newResource(resourcePath, null) : null; } /** - * Recursively scan a directory for file path patterns matching the scan spec. + * Recursively scan a {@link Path} for sub-path patterns matching the scan spec. * - * @param dir - * the directory + * @param path + * the {@link Path} * @param log * the log */ - private void scanDirRecursively(final File dir, final LogNode log) { - if (skipClasspathElement) { - return; - } + private void scanPathRecursively(final Path path, final LogNode log) { // See if this canonical path has been scanned before, so that recursive scanning doesn't get stuck in an // infinite loop due to symlinks - String canonicalPath; + Path canonicalPath; try { - canonicalPath = dir.getCanonicalPath(); + canonicalPath = path.toRealPath(); if (!scannedCanonicalPaths.add(canonicalPath)) { if (log != null) { - log.log("Reached symlink cycle, stopping recursion: " + dir); + log.log("Reached symlink cycle, stopping recursion: " + path); } return; } } catch (final IOException | SecurityException e) { if (log != null) { - log.log("Could not canonicalize path: " + dir, e); + log.log("Could not canonicalize path: " + path, e); } return; } - final String dirPath = dir.getPath(); - final String dirRelativePath = ignorePrefixLen > dirPath.length() ? "/" // - : dirPath.substring(ignorePrefixLen).replace(File.separatorChar, '/') + "/"; + String dirRelativePathStr = FastPathResolver.resolve(classpathEltPath.relativize(path).toString()); + while (dirRelativePathStr.startsWith("/")) { + dirRelativePathStr = dirRelativePathStr.substring(1); + } + if (!dirRelativePathStr.endsWith("/")) { + dirRelativePathStr += "/"; + } + final boolean isDefaultPackage = dirRelativePathStr.equals("/"); - if (nestedClasspathRootPrefixes != null && nestedClasspathRootPrefixes.contains(dirRelativePath)) { + if (nestedClasspathRootPrefixes != null && nestedClasspathRootPrefixes.contains(dirRelativePathStr)) { if (log != null) { log.log("Reached nested classpath root, stopping recursion to avoid duplicate scanning: " - + dirRelativePath); + + dirRelativePathStr); } return; } - // Whitelist/blacklist classpath elements based on dir resource paths - checkResourcePathWhiteBlackList(dirRelativePath, log); - if (skipClasspathElement) { + // Ignore versioned sections in exploded jars -- they are only supposed to be used in jars. + // TODO: is it necessary to support multi-versioned exploded jars anyway? If so, all the paths in a + // directory classpath entry will have to be pre-scanned and masked, as happens in ClasspathElementZip. + if (!scanSpec.enableMultiReleaseVersions + && dirRelativePathStr.startsWith(LogicalZipFile.MULTI_RELEASE_PATH_PREFIX)) { + if (log != null) { + log.log("Found unexpected nested versioned entry in directory classpath element -- skipping: " + + dirRelativePathStr); + } + return; + } + + // Accept/reject classpath elements based on dir resource paths + if (!checkResourcePathAcceptReject(dirRelativePathStr, log)) { return; } - final ScanSpecPathMatch parentMatchStatus = scanSpec.dirWhitelistMatchStatus(dirRelativePath); - if (parentMatchStatus == ScanSpecPathMatch.HAS_BLACKLISTED_PATH_PREFIX) { - // Reached a non-whitelisted or blacklisted path -- stop the recursive scan + final ScanSpecPathMatch parentMatchStatus = scanSpec.dirAcceptMatchStatus(dirRelativePathStr); + if (parentMatchStatus == ScanSpecPathMatch.HAS_REJECTED_PATH_PREFIX) { + // Reached a non-accepted or rejected path -- stop the recursive scan if (log != null) { - log.log("Reached blacklisted directory, stopping recursive scan: " + dirRelativePath); + log.log("Reached rejected directory, stopping recursive scan: " + dirRelativePathStr); } return; } - if (parentMatchStatus == ScanSpecPathMatch.NOT_WITHIN_WHITELISTED_PATH) { - // Reached a non-whitelisted and non-blacklisted path -- stop the recursive scan + if (parentMatchStatus == ScanSpecPathMatch.NOT_WITHIN_ACCEPTED_PATH) { + // Reached a non-accepted and non-rejected path -- stop the recursive scan return; } - final File[] filesInDir = dir.listFiles(); - if (filesInDir == null) { + final LogNode subLog = log == null ? null + // Log dirs after files (addAcceptedResources() precedes log entry with "0:") + : log.log("1:" + canonicalPath, + "Scanning Path: " + FastPathResolver.resolve(path.toString()) + (path.equals(canonicalPath) + ? "" + : " ; canonical path: " + FastPathResolver.resolve(canonicalPath.toString()))); + + final List pathsInDir = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(path)) { + for (final Path subPath : stream) { + pathsInDir.add(subPath); + } + } catch (IOException | SecurityException e) { if (log != null) { - log.log("Invalid directory " + dir); + log.log("Could not read directory " + path + " : " + e.getMessage()); } return; } - Arrays.sort(filesInDir); - final LogNode subLog = log == null ? null - // Log dirs after files (addWhitelistedResources() precedes log entry with "0:") - : log.log("1:" + canonicalPath, "Scanning directory: " + dir - + (dir.getPath().equals(canonicalPath) ? "" : " ; canonical path: " + canonicalPath)); + Collections.sort(pathsInDir); + final FileUtils.FileAttributesGetter getFileAttributes = FileUtils.createCachedAttributesGetter(); + + // Determine whether this is a modular jar running under JRE 9+ + final boolean isModularJar = VersionFinder.JAVA_MAJOR_VERSION >= 9 && getModuleName() != null; - // Only scan files in directory if directory is not only an ancestor of a whitelisted path - if (parentMatchStatus != ScanSpecPathMatch.ANCESTOR_OF_WHITELISTED_PATH) { + // Only scan files in directory if directory is not only an ancestor of an accepted path + if (parentMatchStatus != ScanSpecPathMatch.ANCESTOR_OF_ACCEPTED_PATH) { // Do preorder traversal (files in dir, then subdirs), to reduce filesystem cache misses - for (final File fileInDir : filesInDir) { + final Iterator pathsIterator = pathsInDir.iterator(); + while (pathsIterator.hasNext()) { + final Path subPath = pathsIterator.next(); // Process files in dir before recursing - if (fileInDir.isFile()) { - final String fileInDirRelativePath = dirRelativePath.isEmpty() || "/".equals(dirRelativePath) - ? fileInDir.getName() - : dirRelativePath + fileInDir.getName(); - - // Whitelist/blacklist classpath elements based on file resource paths - checkResourcePathWhiteBlackList(fileInDirRelativePath, subLog); - if (skipClasspathElement) { + final BasicFileAttributes fileAttributes = getFileAttributes.get(subPath); + if (fileAttributes.isRegularFile()) { + pathsIterator.remove(); + final Path subPathRelative = classpathEltPath.relativize(subPath); + final String subPathRelativeStr = FastPathResolver.resolve(subPathRelative.toString()); + // If this is a modular jar, ignore all classfiles other than "module-info.class" in the + // default package, since these are disallowed. + if (isModularJar && isDefaultPackage && subPathRelativeStr.endsWith(".class") + && !subPathRelativeStr.equals("module-info.class")) { + continue; + } + + // Accept/reject classpath elements based on file resource paths + if (!checkResourcePathAcceptReject(subPathRelativeStr, subLog)) { return; } - // If relative path is whitelisted - if (parentMatchStatus == ScanSpecPathMatch.HAS_WHITELISTED_PATH_PREFIX - || parentMatchStatus == ScanSpecPathMatch.AT_WHITELISTED_PATH - || (parentMatchStatus == ScanSpecPathMatch.AT_WHITELISTED_CLASS_PACKAGE - && scanSpec.classfileIsSpecificallyWhitelisted(fileInDirRelativePath))) { - // Resource is whitelisted - final Resource resource = newResource(classpathEltDir, fileInDirRelativePath, fileInDir); - addWhitelistedResource(resource, parentMatchStatus, subLog); - - // Save last modified time - fileToLastModified.put(fileInDir, fileInDir.lastModified()); + // If relative path is accepted + if (parentMatchStatus == ScanSpecPathMatch.HAS_ACCEPTED_PATH_PREFIX + || parentMatchStatus == ScanSpecPathMatch.AT_ACCEPTED_PATH + || (parentMatchStatus == ScanSpecPathMatch.AT_ACCEPTED_CLASS_PACKAGE + && scanSpec.classfileIsSpecificallyAccepted(subPathRelativeStr))) { + // Resource is accepted + final Resource resource = newResource(subPath, fileAttributes); + addAcceptedResource(resource, parentMatchStatus, /* isClassfileOnly = */ false, subLog); + + // Save last modified time + try { + fileToLastModified.put(subPath.toFile(), fileAttributes.lastModifiedTime().toMillis()); + } catch (final UnsupportedOperationException e) { + // Ignore + } } else { if (subLog != null) { - subLog.log("Skipping non-whitelisted file: " + fileInDirRelativePath); + subLog.log("Skipping non-accepted file: " + subPathRelative); } } } } - } else if (scanSpec.enableClassInfo && dirRelativePath.equals("/")) { - // Always check for module descriptor in package root, even if package root isn't in whitelist - for (final File fileInDir : filesInDir) { - if (fileInDir.getName().equals("module-info.class") && fileInDir.isFile()) { - final Resource resource = newResource(classpathEltDir, "module-info.class", fileInDir); - addWhitelistedResource(resource, parentMatchStatus, subLog); - fileToLastModified.put(fileInDir, fileInDir.lastModified()); + } else if (scanSpec.enableClassInfo && dirRelativePathStr.equals("/")) { + // Always check for module descriptor in package root, even if package root isn't in accept + final Iterator pathsIterator = pathsInDir.iterator(); + while (pathsIterator.hasNext()) { + final Path subPath = pathsIterator.next(); + if (subPath.getFileName().toString().equals("module-info.class")) { + final BasicFileAttributes fileAttributes = getFileAttributes.get(subPath); + if (fileAttributes.isRegularFile()) { + pathsIterator.remove(); + final Resource resource = newResource(subPath, fileAttributes); + addAcceptedResource(resource, parentMatchStatus, /* isClassfileOnly = */ true, subLog); + try { + fileToLastModified.put(subPath.toFile(), fileAttributes.lastModifiedTime().toMillis()); + } catch (final UnsupportedOperationException e) { + // Ignore + } + break; + } } } } // Recurse into subdirectories - for (final File fileInDir : filesInDir) { - if (fileInDir.isDirectory()) { - scanDirRecursively(fileInDir, subLog); - // If a blacklisted classpath element resource path was found, it will set skipClasspathElement - if (skipClasspathElement) { - if (subLog != null) { - subLog.addElapsedTime(); - } - return; + for (final Path subPath : pathsInDir) { + try { + if (getFileAttributes.get(subPath).isDirectory()) { + scanPathRecursively(subPath, subLog); + } + } catch (final SecurityException e) { + if (subLog != null) { + subLog.log("Could not read sub-directory " + subPath + " : " + e.getMessage()); } } } @@ -429,7 +495,12 @@ private void scanDirRecursively(final File dir, final LogNode log) { } // Save the last modified time of the directory - fileToLastModified.put(dir, dir.lastModified()); + try { + final File file = path.toFile(); + fileToLastModified.put(file, file.lastModified()); + } catch (final UnsupportedOperationException e) { + // Ignore + } } /** @@ -440,18 +511,21 @@ private void scanDirRecursively(final File dir, final LogNode log) { */ @Override void scanPaths(final LogNode log) { + if (!checkResourcePathAcceptReject(classpathEltPath.toString(), log)) { + skipClasspathElement = true; + } if (skipClasspathElement) { return; } if (scanned.getAndSet(true)) { // Should not happen - throw new IllegalArgumentException("Already scanned classpath element " + toString()); + throw new IllegalArgumentException("Already scanned classpath element " + this); } final LogNode subLog = log == null ? null - : log.log(classpathEltDir.getPath(), "Scanning directory classpath element " + classpathEltDir); + : log(classpathElementIdx, "Scanning Path classpath element " + getURI(), log); - scanDirRecursively(classpathEltDir, subLog); + scanPathRecursively(classpathEltPath, subLog); finishScanPaths(subLog); } @@ -470,11 +544,16 @@ public String getModuleName() { /** * Get the directory {@link File}. * - * @return The classpath element directory as a {@link File}. + * @return The classpath element directory as a {@link File}, or null if this classpath element is not backed by + * a directory (should not happen). */ @Override public File getFile() { - return classpathEltDir; + try { + return classpathEltPath.toFile(); + } catch (final UnsupportedOperationException e) { + return null; + } } /* (non-Javadoc) @@ -482,7 +561,16 @@ public File getFile() { */ @Override URI getURI() { - return classpathEltDir.toURI(); + try { + return classpathEltPath.toUri(); + } catch (IOError | SecurityException e) { + throw new IllegalArgumentException("Could not convert to URI: " + classpathEltPath); + } + } + + @Override + List getAllURIs() { + return Collections.singletonList(getURI()); } /** @@ -492,28 +580,33 @@ URI getURI() { */ @Override public String toString() { - return classpathEltDir.toString(); + try { + // Path.toString() does not include the URI scheme for some reason + return classpathEltPath.toUri().toString(); + } catch (IOError | SecurityException e) { + return classpathEltPath.toString(); + } } /* (non-Javadoc) - * @see java.lang.Object#equals(java.lang.Object) + * @see java.lang.Object#hashCode() */ @Override - public boolean equals(final Object o) { - if (o == this) { - return true; - } else if (!(o instanceof ClasspathElementDir)) { - return false; - } - final ClasspathElementDir other = (ClasspathElementDir) o; - return this.classpathEltDir.equals(other.classpathEltDir); + public int hashCode() { + return Objects.hash(classpathEltPath); } /* (non-Javadoc) - * @see java.lang.Object#hashCode() + * @see java.lang.Object#equals(java.lang.Object) */ @Override - public int hashCode() { - return classpathEltDir.hashCode(); + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof ClasspathElementDir)) { + return false; + } + final ClasspathElementDir other = (ClasspathElementDir) obj; + return Objects.equals(this.classpathEltPath, other.classpathEltPath); } } diff --git a/src/main/java/io/github/classgraph/ClasspathElementModule.java b/src/main/java/io/github/classgraph/ClasspathElementModule.java index 65830de8f..734638057 100644 --- a/src/main/java/io/github/classgraph/ClasspathElementModule.java +++ b/src/main/java/io/github/classgraph/ClasspathElementModule.java @@ -33,30 +33,42 @@ import java.io.InputStream; import java.net.URI; import java.nio.ByteBuffer; +import java.nio.file.attribute.PosixFilePermission; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import io.github.classgraph.Scanner.ClasspathEntryWorkUnit; -import nonapi.io.github.classgraph.ScanSpec; -import nonapi.io.github.classgraph.ScanSpec.ScanSpecPathMatch; +import nonapi.io.github.classgraph.concurrency.SingletonMap; +import nonapi.io.github.classgraph.concurrency.SingletonMap.NewInstanceException; import nonapi.io.github.classgraph.concurrency.SingletonMap.NullSingletonException; import nonapi.io.github.classgraph.concurrency.WorkQueue; -import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; +import nonapi.io.github.classgraph.fastzipfilereader.LogicalZipFile; +import nonapi.io.github.classgraph.fileslice.reader.ClassfileReader; import nonapi.io.github.classgraph.recycler.RecycleOnClose; import nonapi.io.github.classgraph.recycler.Recycler; -import nonapi.io.github.classgraph.utils.InputStreamOrByteBufferAdapter; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.scanspec.ScanSpec.ScanSpecPathMatch; +import nonapi.io.github.classgraph.utils.CollectionUtils; import nonapi.io.github.classgraph.utils.LogNode; +import nonapi.io.github.classgraph.utils.ProxyingInputStream; +import nonapi.io.github.classgraph.utils.VersionFinder; -/** A module classpath element. */ +/** + * A module classpath element. + * + * @author luke + */ class ClasspathElementModule extends ClasspathElement { /** The module ref. */ final ModuleRef moduleRef; - /** The nested jar handler. */ - private final NestedJarHandler nestedJarHandler; + /** A singleton map from a {@link ModuleRef} to a {@link ModuleReaderProxy} recycler for the module. */ + SingletonMap, IOException> // + moduleRefToModuleReaderProxyRecyclerMap; /** The module reader proxy recycler. */ private Recycler moduleReaderProxyRecycler; @@ -69,18 +81,20 @@ class ClasspathElementModule extends ClasspathElement { * * @param moduleRef * the module ref - * @param classLoader - * the classloader - * @param nestedJarHandler - * the nested jar handler + * @param workUnit + * the work unit + * @param moduleRefToModuleReaderProxyRecyclerMap + * the module ref to module reader proxy recycler map * @param scanSpec * the scan spec */ - ClasspathElementModule(final ModuleRef moduleRef, final ClassLoader classLoader, - final NestedJarHandler nestedJarHandler, final ScanSpec scanSpec) { - super(classLoader, scanSpec); + ClasspathElementModule(final ModuleRef moduleRef, + final SingletonMap, IOException> // + moduleRefToModuleReaderProxyRecyclerMap, final ClasspathEntryWorkUnit workUnit, + final ScanSpec scanSpec) { + super(workUnit, scanSpec); + this.moduleRefToModuleReaderProxyRecyclerMap = moduleRefToModuleReaderProxyRecyclerMap; this.moduleRef = moduleRef; - this.nestedJarHandler = nestedJarHandler; } /* (non-Javadoc) @@ -92,17 +106,18 @@ void open(final WorkQueue workQueueIgnored, final LogNod throws InterruptedException { if (!scanSpec.scanModules) { if (log != null) { - log.log("Skipping module, since module scanning is disabled: " + getModuleName()); + log(classpathElementIdx, "Skipping module, since module scanning is disabled: " + getModuleName(), + log); } skipClasspathElement = true; return; } try { - moduleReaderProxyRecycler = nestedJarHandler.moduleRefToModuleReaderProxyRecyclerMap.get(moduleRef, - /* ignored */ null); - } catch (final IOException | NullSingletonException e) { + moduleReaderProxyRecycler = moduleRefToModuleReaderProxyRecyclerMap.get(moduleRef, log); + } catch (final IOException | NullSingletonException | NewInstanceException e) { if (log != null) { - log.log("Skipping invalid module " + getModuleName() + " : " + e); + log(classpathElementIdx, "Skipping invalid module " + getModuleName() + " : " + + (e.getCause() == null ? e : e.getCause()), log); } skipClasspathElement = true; return; @@ -121,23 +136,41 @@ private Resource newResource(final String resourcePath) { /** The module reader proxy. */ private ModuleReaderProxy moduleReaderProxy; + /** True if the resource is open. */ + private final AtomicBoolean isOpen = new AtomicBoolean(); + @Override public String getPath() { return resourcePath; } @Override - public String getPathRelativeToClasspathElement() { - return resourcePath; + public long getLastModified() { + return 0L; // Unknown } @Override - public synchronized ByteBuffer read() throws IOException { + public Set getPosixFilePermissions() { + return null; // N/A + } + + protected void checkCanOpen() { if (skipClasspathElement) { // Shouldn't happen - throw new IOException("Module could not be opened"); + throw new IllegalStateException("Classpath element could not be opened"); } - markAsOpen(); + if (isOpen.getAndSet(true)) { + throw new IllegalStateException( + "Resource is already open -- cannot open it again without first calling close()"); + } + if (scanResult != null && scanResult.isClosed()) { + throw new IllegalStateException("Cannot open a resource after the ScanResult is closed"); + } + } + + @Override + public ByteBuffer read() throws IOException { + checkCanOpen(); try { moduleReaderProxy = moduleReaderProxyRecycler.acquire(); // ModuleReader#read(String name) internally calls: @@ -153,20 +186,44 @@ public synchronized ByteBuffer read() throws IOException { } @Override - synchronized InputStreamOrByteBufferAdapter openOrRead() throws IOException { - return new InputStreamOrByteBufferAdapter(open()); + ClassfileReader openClassfile() throws IOException { + return new ClassfileReader(open(), this); } @Override - public synchronized InputStream open() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Module could not be opened"); + public URI getURI() { + try { + final ModuleReaderProxy localModuleReaderProxy = moduleReaderProxyRecycler.acquire(); + try { + return localModuleReaderProxy.find(resourcePath); + } finally { + moduleReaderProxyRecycler.recycle(localModuleReaderProxy); + } + } catch (final IOException e) { + throw new RuntimeException(e); } - markAsOpen(); + } + + @Override + public InputStream open() throws IOException { + checkCanOpen(); try { + final Resource thisResource = this; moduleReaderProxy = moduleReaderProxyRecycler.acquire(); - inputStream = new InputStreamResourceCloser(this, moduleReaderProxy.open(resourcePath)); + inputStream = new ProxyingInputStream(moduleReaderProxy.open(resourcePath)) { + @Override + public void close() throws IOException { + // Close the wrapped InputStream obtained from moduleReaderProxy + super.close(); + try { + // Close the Resource, releasing any underlying ByteBuffer and recycling + // the moduleReaderProxy + thisResource.close(); + } catch (final Exception e) { + // Ignore + } + } + }; // Length cannot be obtained from ModuleReader length = -1L; return inputStream; @@ -178,36 +235,42 @@ public synchronized InputStream open() throws IOException { } @Override - public synchronized byte[] load() throws IOException { - try { - read(); - final byte[] byteArray = byteBufferToByteArray(); - length = byteArray.length; + public byte[] load() throws IOException { + try (Resource res = this) { // Close this after use + read(); // Fill byteBuffer + final byte[] byteArray; + if (res.byteBuffer.hasArray() && res.byteBuffer.position() == 0 + && res.byteBuffer.limit() == res.byteBuffer.capacity()) { + byteArray = res.byteBuffer.array(); + } else { + byteArray = new byte[res.byteBuffer.remaining()]; + res.byteBuffer.get(byteArray); + } + res.length = byteArray.length; return byteArray; - } finally { - close(); } } @Override - public synchronized void close() { - super.close(); // Close inputStream - if (byteBuffer != null) { + public void close() { + if (isOpen.getAndSet(false)) { if (moduleReaderProxy != null) { - // Release any open ByteBuffer - moduleReaderProxy.release(byteBuffer); + if (byteBuffer != null) { + // Release any open ByteBuffer + moduleReaderProxy.release(byteBuffer); + byteBuffer = null; + } + // Recycle the (open) ModuleReaderProxy instance. + moduleReaderProxyRecycler.recycle(moduleReaderProxy); + // Don't call ModuleReaderProxy#close(), leave the ModuleReaderProxy open in the recycler. + // Just set the ref to null here. The ModuleReaderProxy will be closed by + // ClasspathElementModule#close(). + moduleReaderProxy = null; } - byteBuffer = null; - } - if (moduleReaderProxy != null) { - // Recycle the (open) ModuleReaderProxy instance. - moduleReaderProxyRecycler.recycle(moduleReaderProxy); - // Don't call ModuleReaderProxy#close(), leave the ModuleReaderProxy open in the recycler. - // Just set the ref to null here. The ModuleReaderProxy will be closed by - // ClasspathElementModule#close(). - moduleReaderProxy = null; + + // Close inputStream + super.close(); } - markAsClosed(); } }; } @@ -238,16 +301,18 @@ void scanPaths(final LogNode log) { } if (scanned.getAndSet(true)) { // Should not happen - throw new IllegalArgumentException("Already scanned classpath element " + toString()); + throw new IllegalArgumentException("Already scanned classpath element " + this); } - final String moduleLocationStr = moduleRef.getLocationStr(); final LogNode subLog = log == null ? null - : log.log(moduleLocationStr, "Scanning module " + moduleRef.getName()); + : log(classpathElementIdx, "Scanning module " + moduleRef.getName(), log); + + // Determine whether this is a modular jar running under JRE 9+ + final boolean isModularJar = VersionFinder.JAVA_MAJOR_VERSION >= 9 && getModuleName() != null; try (RecycleOnClose moduleReaderProxyRecycleOnClose // = moduleReaderProxyRecycler.acquireRecycleOnClose()) { - // Look for whitelisted files in the module. + // Look for accepted files in the module. List resourceRelativePaths; try { resourceRelativePaths = moduleReaderProxyRecycleOnClose.get().list(); @@ -257,7 +322,7 @@ void scanPaths(final LogNode log) { } return; } - Collections.sort(resourceRelativePaths); + CollectionUtils.sortIfNotEmpty(resourceRelativePaths); String prevParentRelativePath = null; ScanSpecPathMatch prevParentMatchStatus = null; @@ -274,10 +339,30 @@ void scanPaths(final LogNode log) { continue; } - // Whitelist/blacklist classpath elements based on file resource paths - checkResourcePathWhiteBlackList(relativePath, log); - if (skipClasspathElement) { - return; + // Paths in modules should never start with "META-INF/versions/{version}/", because the module + // system should already strip these prefixes away. If they are found, then the jarfile must + // contain a path like "META-INF/versions/{version}/META-INF/versions/{version}/", which cannot + // be valid (META-INF should only ever exist in the module root), and the nested versioned section + // should be ignored. + if (!scanSpec.enableMultiReleaseVersions + && relativePath.startsWith(LogicalZipFile.MULTI_RELEASE_PATH_PREFIX)) { + if (subLog != null) { + subLog.log( + "Found unexpected nested versioned entry in module -- skipping: " + relativePath); + } + continue; + } + + // If this is a modular jar, ignore all classfiles other than "module-info.class" in the + // default package, since these are disallowed. + if (isModularJar && relativePath.indexOf('/') < 0 && relativePath.endsWith(".class") + && !relativePath.equals("module-info.class")) { + continue; + } + + // Accept/reject classpath elements based on file resource paths + if (!checkResourcePathAcceptReject(relativePath, log)) { + continue; } // Get match status of the parent directory of this resource's relative path (or reuse the last @@ -288,30 +373,36 @@ void scanPaths(final LogNode log) { final boolean parentRelativePathChanged = !parentRelativePath.equals(prevParentRelativePath); final ScanSpecPathMatch parentMatchStatus = // prevParentRelativePath == null || parentRelativePathChanged - ? scanSpec.dirWhitelistMatchStatus(parentRelativePath) + ? scanSpec.dirAcceptMatchStatus(parentRelativePath) : prevParentMatchStatus; prevParentRelativePath = parentRelativePath; prevParentMatchStatus = parentMatchStatus; - if (parentMatchStatus == ScanSpecPathMatch.HAS_BLACKLISTED_PATH_PREFIX) { - // The parent dir or one of its ancestral dirs is blacklisted + if (parentMatchStatus == ScanSpecPathMatch.HAS_REJECTED_PATH_PREFIX) { + // The parent dir or one of its ancestral dirs is rejected if (subLog != null) { - subLog.log("Skipping blacklisted path: " + relativePath); + subLog.log("Skipping rejected path: " + relativePath); } continue; } - // Found non-blacklisted relative path - if (allResourcePaths.add(relativePath) - // If resource is whitelisted - && (parentMatchStatus == ScanSpecPathMatch.HAS_WHITELISTED_PATH_PREFIX - || parentMatchStatus == ScanSpecPathMatch.AT_WHITELISTED_PATH - || (parentMatchStatus == ScanSpecPathMatch.AT_WHITELISTED_CLASS_PACKAGE - && scanSpec.classfileIsSpecificallyWhitelisted(relativePath)) - || (scanSpec.enableClassInfo && relativePath.equals("module-info.class")))) { - // Add whitelisted resource - final Resource resource = newResource(relativePath); - addWhitelistedResource(resource, parentMatchStatus, subLog); + // Found non-rejected relative path + if (allResourcePaths.add(relativePath)) { + // If resource is accepted + if (parentMatchStatus == ScanSpecPathMatch.HAS_ACCEPTED_PATH_PREFIX + || parentMatchStatus == ScanSpecPathMatch.AT_ACCEPTED_PATH + || (parentMatchStatus == ScanSpecPathMatch.AT_ACCEPTED_CLASS_PACKAGE + && scanSpec.classfileIsSpecificallyAccepted(relativePath))) { + // Add accepted resource + addAcceptedResource(newResource(relativePath), parentMatchStatus, + /* isClassfileOnly = */ false, subLog); + } else if (scanSpec.enableClassInfo && relativePath.equals("module-info.class")) { + // Add module descriptor as an accepted classfile resource, so that it is scanned, + // but don't add it to the list of resources in the ScanResult, since it is not + // in an accepted package (#352) + addAcceptedResource(newResource(relativePath), parentMatchStatus, + /* isClassfileOnly = */ true, subLog); + } } } @@ -377,6 +468,11 @@ URI getURI() { return uri; } + @Override + List getAllURIs() { + return Collections.singletonList(getURI()); + } + /* (non-Javadoc) * @see io.github.classgraph.ClasspathElement#getFile() */ @@ -406,20 +502,32 @@ public String toString() { return moduleRef.toString(); } + /** + * Equals. + * + * @param obj + * the obj + * @return true, if successful + */ /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(final Object o) { - if (o == this) { + public boolean equals(final Object obj) { + if (obj == this) { return true; - } else if (!(o instanceof ClasspathElementModule)) { + } else if (!(obj instanceof ClasspathElementModule)) { return false; } - final ClasspathElementModule other = (ClasspathElementModule) o; + final ClasspathElementModule other = (ClasspathElementModule) obj; return this.getModuleNameOrEmpty().equals(other.getModuleNameOrEmpty()); } + /** + * Hash code. + * + * @return the int + */ /* (non-Javadoc) * @see java.lang.Object#hashCode() */ diff --git a/src/main/java/io/github/classgraph/ClasspathElementZip.java b/src/main/java/io/github/classgraph/ClasspathElementZip.java index 17d7a5933..83649ad54 100644 --- a/src/main/java/io/github/classgraph/ClasspathElementZip.java +++ b/src/main/java/io/github/classgraph/ClasspathElementZip.java @@ -29,45 +29,58 @@ package io.github.classgraph; import java.io.File; +import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.nio.ByteBuffer; -import java.util.AbstractMap.SimpleEntry; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import io.github.classgraph.Scanner.ClasspathEntryWorkUnit; -import nonapi.io.github.classgraph.ScanSpec; -import nonapi.io.github.classgraph.ScanSpec.ScanSpecPathMatch; import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandlerRegistry; +import nonapi.io.github.classgraph.concurrency.SingletonMap.NewInstanceException; import nonapi.io.github.classgraph.concurrency.SingletonMap.NullSingletonException; import nonapi.io.github.classgraph.concurrency.WorkQueue; import nonapi.io.github.classgraph.fastzipfilereader.FastZipEntry; import nonapi.io.github.classgraph.fastzipfilereader.LogicalZipFile; import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; +import nonapi.io.github.classgraph.fastzipfilereader.ZipFileSlice; +import nonapi.io.github.classgraph.fileslice.reader.ClassfileReader; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.scanspec.ScanSpec.ScanSpecPathMatch; import nonapi.io.github.classgraph.utils.FastPathResolver; import nonapi.io.github.classgraph.utils.FileUtils; -import nonapi.io.github.classgraph.utils.InputStreamOrByteBufferAdapter; import nonapi.io.github.classgraph.utils.JarUtils; import nonapi.io.github.classgraph.utils.LogNode; import nonapi.io.github.classgraph.utils.URLPathEncoder; +import nonapi.io.github.classgraph.utils.VersionFinder; /** A zip/jarfile classpath element. */ class ClasspathElementZip extends ClasspathElement { - /** The raw path for this zipfile. */ + /** + * The {@link String} representation of the path string, {@link URL}, {@link URI}, or {@link Path} for this + * zipfile. + */ private final String rawPath; /** The logical zipfile for this classpath element. */ LogicalZipFile logicalZipFile; - /** The package root within the jarfile. */ - private String packageRootPrefix = ""; /** The normalized path of the jarfile, "!/"-separated if nested, excluding any package root. */ private String zipFilePath; - /** A map from relative path to {@link Resource} for non-blacklisted zip entries. */ + /** A map from relative path to {@link Resource} for non-rejected zip entries. */ private final ConcurrentHashMap relativePathToResource = new ConcurrentHashMap<>(); + /** A list of all automatic package root prefixes found as prefixes of paths within this zipfile. */ + private final Set strippedAutomaticPackageRootPrefixes = new HashSet<>(); /** The nested jar handler. */ private final NestedJarHandler nestedJarHandler; /** @@ -81,18 +94,32 @@ class ClasspathElementZip extends ClasspathElement { /** * A jarfile classpath element. * - * @param rawPath - * the raw path to the jarfile, possibly including "!"-delimited nested paths. - * @param classLoader - * the classloader + * @param workUnit + * the work unit * @param nestedJarHandler * the nested jar handler * @param scanSpec * the scan spec */ - ClasspathElementZip(final String rawPath, final ClassLoader classLoader, - final NestedJarHandler nestedJarHandler, final ScanSpec scanSpec) { - super(classLoader, scanSpec); + ClasspathElementZip(final ClasspathEntryWorkUnit workUnit, final NestedJarHandler nestedJarHandler, + final ScanSpec scanSpec) { + super(workUnit, scanSpec); + final Object rawPathObj = workUnit.classpathEntryObj; + + // Convert the raw path object (Path, URL, or URI) to a string. + // Any required URL/URI parsing are done in NestedJarHandler. + String rawPath = null; + if (rawPathObj instanceof Path) { + // Path.toString does not include URI scheme => turn into a URI so that toString works + try { + rawPath = ((Path) rawPathObj).toUri().toString(); + } catch (final IOError | SecurityException e) { + // Fall through + } + } + if (rawPath == null) { + rawPath = rawPathObj.toString(); + } this.rawPath = rawPath; this.zipFilePath = rawPath; // May change when open() is called this.nestedJarHandler = nestedJarHandler; @@ -106,18 +133,19 @@ class ClasspathElementZip extends ClasspathElement { void open(final WorkQueue workQueue, final LogNode log) throws InterruptedException { if (!scanSpec.scanJars) { if (log != null) { - log.log("Skipping classpath element, since jar scanning is disabled: " + rawPath); + log(classpathElementIdx, "Skipping classpath element, since jar scanning is disabled: " + rawPath, + log); } skipClasspathElement = true; return; } - final LogNode subLog = log == null ? null : log.log("Opening jar: " + rawPath); + final LogNode subLog = log == null ? null : log(classpathElementIdx, "Opening jar: " + rawPath, log); final int plingIdx = rawPath.indexOf('!'); - final String outermostZipFilePathResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, + final String outermostZipFilePathResolved = FastPathResolver.resolve(FileUtils.currDirPath(), plingIdx < 0 ? rawPath : rawPath.substring(0, plingIdx)); - if (!scanSpec.jarWhiteBlackList.isWhitelistedAndNotBlacklisted(outermostZipFilePathResolved)) { + if (!scanSpec.jarAcceptReject.isAcceptedAndNotRejected(outermostZipFilePathResolved)) { if (subLog != null) { - subLog.log("Skipping jarfile that is blacklisted or not whitelisted: " + rawPath); + subLog.log("Skipping jarfile that is rejected or not accepted: " + rawPath); } skipClasspathElement = true; return; @@ -129,9 +157,11 @@ void open(final WorkQueue workQueue, final LogNode log) try { logicalZipFileAndPackageRoot = nestedJarHandler.nestedPathToLogicalZipFileAndPackageRootMap .get(rawPath, subLog); - } catch (final NullSingletonException e) { - // Generally thrown on the second and subsequent attempt to call .get(), after the first failed - throw new IOException("Could not get logical zipfile " + rawPath + " : " + e); + } catch (final NullSingletonException | NewInstanceException e) { + // Generally thrown on the second and subsequent attempt to call .get(), after the first failed, + // or newInstance() threw an exception + throw new IOException("Could not get logical zipfile " + rawPath + " : " + + (e.getCause() == null ? e : e.getCause())); } logicalZipFile = logicalZipFileAndPackageRoot.getKey(); if (logicalZipFile == null) { @@ -140,9 +170,9 @@ void open(final WorkQueue workQueue, final LogNode log) } // Get the normalized path of the logical zipfile - zipFilePath = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, logicalZipFile.getPath()); + zipFilePath = FastPathResolver.resolve(FileUtils.currDirPath(), logicalZipFile.getPath()); - // Get package root of jarfile + // Get package root of jarfile final String packageRoot = logicalZipFileAndPackageRoot.getValue(); if (!packageRoot.isEmpty()) { packageRootPrefix = packageRoot + "/"; @@ -156,7 +186,7 @@ void open(final WorkQueue workQueue, final LogNode log) } if (!scanSpec.enableSystemJarsAndModules && logicalZipFile.isJREJar) { - // Found a blacklisted JRE jar that was not caught by filtering for rt.jar in ClasspathFinder + // Found a rejected JRE jar that was not caught by filtering for rt.jar in ClasspathFinder // (the isJREJar value was set by detecting JRE headers in the jar's manifest file) if (subLog != null) { subLog.log("Ignoring JRE jar: " + rawPath); @@ -165,9 +195,9 @@ void open(final WorkQueue workQueue, final LogNode log) return; } - if (!logicalZipFile.isWhitelistedAndNotBlacklisted(scanSpec.jarWhiteBlackList)) { + if (!logicalZipFile.isAcceptedAndNotRejected(scanSpec.jarAcceptReject)) { if (subLog != null) { - subLog.log("Skipping jarfile that is blacklisted or not whitelisted: " + rawPath); + subLog.log("Skipping jarfile that is rejected or not accepted: " + rawPath); } skipClasspathElement = true; return; @@ -179,44 +209,86 @@ void open(final WorkQueue workQueue, final LogNode log) if (scanSpec.scanNestedJars) { for (final FastZipEntry zipEntry : logicalZipFile.entries) { for (final String libDirPrefix : ClassLoaderHandlerRegistry.AUTOMATIC_LIB_DIR_PREFIXES) { + // Even if a package root is given, e.g. BOOT-INF/classes, still look in lib/ etc. for jars if (zipEntry.entryNameUnversioned.startsWith(libDirPrefix) && zipEntry.entryNameUnversioned.endsWith(".jar")) { final String entryPath = zipEntry.getPath(); if (subLog != null) { subLog.log("Found nested lib jar: " + entryPath); } - workQueue.addWorkUnit(new ClasspathEntryWorkUnit( - /* rawClasspathEntry = */ new SimpleEntry<>(entryPath, classLoader), + workQueue.addWorkUnit(new ClasspathEntryWorkUnit(entryPath, getClassLoader(), /* parentClasspathElement = */ this, /* orderWithinParentClasspathElement = */ - childClasspathEntryIdx++)); + childClasspathEntryIdx++, /* packageRootPrefix = */ "")); break; } } } } - // Create child classpath elements from values obtained from Class-Path entry in manifest + // Don't add child classpath elements that are identical to this classpath element, or that are duplicates + final Set scheduledChildClasspathElements = new HashSet<>(); + scheduledChildClasspathElements.add(rawPath); + + // Create child classpath elements from values obtained from Class-Path entry in manifest, resolving + // the paths relative to the dir or parent jarfile that the jarfile is contained in if (logicalZipFile.classPathManifestEntryValue != null) { - // Class-Path entries in the manifest file are resolved relative to the dir that - // the manifest's jarfile is contained in -- get parent dir of logical zipfile - final String path = logicalZipFile.getPath(); - final int lastSlashIdx = path.lastIndexOf('/'); - final String parentPathPrefix = lastSlashIdx < 0 ? "" : path.substring(0, lastSlashIdx + 1); - for (final String childClassPathEltPath : logicalZipFile.classPathManifestEntryValue.split(" ")) { - if (!childClassPathEltPath.isEmpty()) { + // Get parent dir of logical zipfile within grandparent slice, + // e.g. for a zipfile slice path of "/path/to/jar1.jar!/lib/jar2.jar", this is "lib", + // or for "/path/to/jar1.jar", this is "/path/to", or "" if the jar is in the toplevel dir. + final String jarParentDir = FileUtils + .getParentDirPath(logicalZipFile.getPathWithinParentZipFileSlice()); + // Add paths in manifest file's "Class-Path" entry to the classpath, resolving paths relative to + // the parent directory or jar + for (final String childClassPathEltPathRelative : logicalZipFile.classPathManifestEntryValue + .split(" ")) { + if (!childClassPathEltPathRelative.isEmpty()) { // Resolve Class-Path entry relative to containing dir - final String childClassPathEltPathResolved = FastPathResolver - .resolve(parentPathPrefix + "/" + childClassPathEltPath); + final String childClassPathEltPath = FastPathResolver.resolve(jarParentDir, + childClassPathEltPathRelative); + // If this is a nested jar, prepend outer jar prefix + final ZipFileSlice parentZipFileSlice = logicalZipFile.getParentZipFileSlice(); + final String childClassPathEltPathWithPrefix = parentZipFileSlice == null + ? childClassPathEltPath + : parentZipFileSlice.getPath() + (childClassPathEltPath.startsWith("/") ? "!" : "!/") + + childClassPathEltPath; + // Only add child classpath elements once + if (scheduledChildClasspathElements.add(childClassPathEltPathWithPrefix)) { + // Schedule child classpath element for scanning + workQueue.addWorkUnit( // + new ClasspathEntryWorkUnit(childClassPathEltPathWithPrefix, getClassLoader(), + /* parentClasspathElement = */ this, + /* orderWithinParentClasspathElement = */ + childClasspathEntryIdx++, /* packageRootPrefix = */ "")); + } + } + } + } + // Add paths in an OSGi bundle jar manifest's "Bundle-ClassPath" entry to the classpath, resolving + // the paths relative to the root of the jarfile + if (logicalZipFile.bundleClassPathManifestEntryValue != null) { + final String zipFilePathPrefix = zipFilePath + "!/"; + // Class-Path is split on " ", but Bundle-ClassPath is split on "," + for (String childBundlePath : logicalZipFile.bundleClassPathManifestEntryValue.split(",")) { + // Assume that Bundle-ClassPath paths have to be given relative to jarfile root + while (childBundlePath.startsWith("/")) { + childBundlePath = childBundlePath.substring(1); + } + // Currently the position of "." relative to child classpath entries is ignored (the + // Bundle-ClassPath path is treated as if "." is in the first position, since child + // classpath entries are always added to the classpath after the parent classpath + // entry that they were obtained from). + if (!childBundlePath.isEmpty() && !childBundlePath.equals(".")) { + // Resolve Bundle-ClassPath entry within jar + final String childClassPathEltPath = zipFilePathPrefix + FileUtils.sanitizeEntryPath( + childBundlePath, /* removeInitialSlash = */ true, /* removeFinalSlash = */ true); // Only add child classpath elements once - if (!childClassPathEltPathResolved.equals(rawPath)) { + if (scheduledChildClasspathElements.add(childClassPathEltPath)) { // Schedule child classpath element for scanning - workQueue.addWorkUnit(new ClasspathEntryWorkUnit( - /* rawClasspathEntry = */ new SimpleEntry<>(childClassPathEltPathResolved, - classLoader), + workQueue.addWorkUnit(new ClasspathEntryWorkUnit(childClassPathEltPath, getClassLoader(), /* parentClasspathElement = */ this, /* orderWithinParentClasspathElement = */ - childClasspathEntryIdx++)); + childClasspathEntryIdx++, /* packageRootPrefix = */ "")); } } } @@ -234,6 +306,9 @@ void open(final WorkQueue workQueue, final LogNode log) */ private Resource newResource(final FastZipEntry zipEntry, final String pathRelativeToPackageRoot) { return new Resource(this, zipEntry.uncompressedSize) { + /** True if the resource is open. */ + private final AtomicBoolean isOpen = new AtomicBoolean(); + /** * Path with package root prefix and/or any Spring Boot prefix ("BOOT-INF/classes/" or * "WEB-INF/classes/") removed. @@ -245,83 +320,125 @@ public String getPath() { @Override public String getPathRelativeToClasspathElement() { - return zipEntry.entryName; + if (zipEntry.entryName.startsWith(packageRootPrefix)) { + return zipEntry.entryName.substring(packageRootPrefix.length()); + } else { + return zipEntry.entryName; + } } @Override - public synchronized InputStream open() throws IOException { + public long getLastModified() { + return zipEntry.getLastModifiedTimeMillis(); + } + + @Override + public Set getPosixFilePermissions() { + final int fileAttributes = zipEntry.fileAttributes; + Set perms; + if (fileAttributes == 0) { + perms = null; + } else { + perms = new HashSet<>(); + if ((fileAttributes & 0400) > 0) { + perms.add(PosixFilePermission.OWNER_READ); + } + if ((fileAttributes & 0200) > 0) { + perms.add(PosixFilePermission.OWNER_WRITE); + } + if ((fileAttributes & 0100) > 0) { + perms.add(PosixFilePermission.OWNER_EXECUTE); + } + if ((fileAttributes & 0040) > 0) { + perms.add(PosixFilePermission.GROUP_READ); + } + if ((fileAttributes & 0020) > 0) { + perms.add(PosixFilePermission.GROUP_WRITE); + } + if ((fileAttributes & 0010) > 0) { + perms.add(PosixFilePermission.GROUP_EXECUTE); + } + if ((fileAttributes & 0004) > 0) { + perms.add(PosixFilePermission.OTHERS_READ); + } + if ((fileAttributes & 0002) > 0) { + perms.add(PosixFilePermission.OTHERS_WRITE); + } + if ((fileAttributes & 0001) > 0) { + perms.add(PosixFilePermission.OTHERS_EXECUTE); + } + } + return perms; + } + + protected void checkCanOpen() { if (skipClasspathElement) { // Shouldn't happen - throw new IOException("Jarfile could not be opened"); + throw new IllegalStateException("Classpath element could not be opened"); + } + if (isOpen.getAndSet(true)) { + throw new IllegalStateException( + "Resource is already open -- cannot open it again without first calling close()"); + } + if (scanResult != null && scanResult.isClosed()) { + throw new IllegalStateException("Cannot open a resource after the ScanResult is closed"); } - markAsOpen(); + } + + @Override + ClassfileReader openClassfile() throws IOException { + return new ClassfileReader(open(), this); + } + + @Override + public InputStream open() throws IOException { + checkCanOpen(); try { - inputStream = new InputStreamResourceCloser(this, zipEntry.open()); + inputStream = zipEntry.getSlice().open(this); length = zipEntry.uncompressedSize; return inputStream; } catch (final IOException e) { close(); throw e; - } catch (final InterruptedException e) { - close(); - nestedJarHandler.interruptionChecker.interrupt(); - throw new IOException(e); } } @Override - synchronized InputStreamOrByteBufferAdapter openOrRead() throws IOException { - return new InputStreamOrByteBufferAdapter(open()); - } - - @Override - public synchronized ByteBuffer read() throws IOException { + public ByteBuffer read() throws IOException { + checkCanOpen(); try { - if (zipEntry.canGetAsSlice()) { - // For STORED entries that do not span multiple 2GB chunks, can create a - // ByteBuffer slice directly from the entry - markAsOpen(); - // compressedSize should have the same value as uncompressedSize for STORED - // entries, but compressedSize is more reliable (uncompressedSize may be -1) - length = zipEntry.compressedSize; - return zipEntry.getAsSlice(); - - } else { - // Otherwise, decompress or extract the entry into a byte[] array, - // then wrap in a ByteBuffer - open(); - return inputStreamToByteBuffer(); - } + byteBuffer = zipEntry.getSlice().read(); + length = byteBuffer.remaining(); + return byteBuffer; } catch (final IOException e) { close(); throw e; - } catch (final InterruptedException e) { - close(); - nestedJarHandler.interruptionChecker.interrupt(); - throw new IOException(e); } } @Override - public synchronized byte[] load() throws IOException { - try { - open(); - final byte[] byteArray = inputStreamToByteArray(); - length = byteArray.length; + public byte[] load() throws IOException { + checkCanOpen(); + try (Resource res = this) { // Close this after use + final byte[] byteArray = zipEntry.getSlice().load(); + res.length = byteArray.length; return byteArray; - } finally { - close(); } } @Override - public synchronized void close() { - super.close(); // Close inputStream - if (byteBuffer != null) { - byteBuffer = null; + public void close() { + if (isOpen.getAndSet(false)) { + if (byteBuffer != null) { + // ByteBuffer should be a duplicate or slice, or should wrap an array, so it doesn't + // need to be unmapped + byteBuffer = null; + } + + // Close inputStream + super.close(); } - markAsClosed(); } }; } @@ -350,6 +467,9 @@ void scanPaths(final LogNode log) { if (logicalZipFile == null) { skipClasspathElement = true; } + if (!checkResourcePathAcceptReject(getZipFilePath(), log)) { + skipClasspathElement = true; + } if (skipClasspathElement) { return; } @@ -359,7 +479,22 @@ void scanPaths(final LogNode log) { } final LogNode subLog = log == null ? null - : log.log(getZipFilePath(), "Scanning jarfile classpath element " + getZipFilePath()); + : log(classpathElementIdx, "Scanning jarfile classpath element " + getZipFilePath(), log); + + boolean isModularJar = false; + if (VersionFinder.JAVA_MAJOR_VERSION >= 9) { + // Determine whether this is a modular jar running under JRE 9+ + String moduleName = moduleNameFromModuleDescriptor; + if (moduleName == null || moduleName.isEmpty()) { + moduleName = moduleNameFromManifestFile; + } + if (moduleName != null && moduleName.isEmpty()) { + moduleName = null; + } + if (moduleName != null) { + isModularJar = true; + } + } Set loggedNestedClasspathRootPrefixes = null; String prevParentRelativePath = null; @@ -367,6 +502,32 @@ void scanPaths(final LogNode log) { for (final FastZipEntry zipEntry : logicalZipFile.entries) { String relativePath = zipEntry.entryNameUnversioned; + // Paths should never start with "META-INF/versions/{version}/", because either this is a versioned + // jar, in which case zipEntry.entryNameUnversioned has the version prefix stripped, or this is an + // unversioned jar (e.g. the multi-version flag is not set in the manifest file) and there are some + // spurious files in a multi-version path (in which case, they should be ignored). + if (!scanSpec.enableMultiReleaseVersions + && relativePath.startsWith(LogicalZipFile.MULTI_RELEASE_PATH_PREFIX)) { + if (subLog != null) { + if (VersionFinder.JAVA_MAJOR_VERSION < 9) { + subLog.log("Skipping versioned entry in jar, because JRE version " + + VersionFinder.JAVA_MAJOR_VERSION + " does not support this: " + relativePath); + } else { + subLog.log( + "Found unexpected versioned entry in jar (the jar's manifest file may be missing " + + "the \"Multi-Release\" key) -- skipping: " + relativePath); + } + } + continue; + } + + // If this is a modular jar, ignore all classfiles other than "module-info.class" in the + // default package, since these are disallowed. + if (isModularJar && relativePath.indexOf('/') < 0 && relativePath.endsWith(".class") + && !relativePath.equals("module-info.class")) { + continue; + } + // Check if the relative path is within a nested classpath root if (nestedClasspathRootPrefixes != null) { // This is O(mn), which is inefficient, but the number of nested classpath roots should be small @@ -398,23 +559,28 @@ void scanPaths(final LogNode log) { } // Strip the package root prefix from the relative path - // N.B. these semantics should mirror those in getResource() if (!packageRootPrefix.isEmpty()) { relativePath = relativePath.substring(packageRootPrefix.length()); } else { // Strip any package root prefix from the relative path for (int i = 0; i < ClassLoaderHandlerRegistry.AUTOMATIC_PACKAGE_ROOT_PREFIXES.length; i++) { - if (relativePath.startsWith(ClassLoaderHandlerRegistry.AUTOMATIC_PACKAGE_ROOT_PREFIXES[i])) { - relativePath = relativePath - .substring(ClassLoaderHandlerRegistry.AUTOMATIC_PACKAGE_ROOT_PREFIXES[i].length()); + final String packageRoot = ClassLoaderHandlerRegistry.AUTOMATIC_PACKAGE_ROOT_PREFIXES[i]; + if (relativePath.startsWith(packageRoot)) { + // Strip package root + relativePath = relativePath.substring(packageRoot.length()); + // Strip final slash from package root + final String packageRootWithoutFinalSlash = packageRoot.endsWith("/") + ? packageRoot.substring(0, packageRoot.length() - 1) + : packageRoot; + // Store package root for use by getAllURIs() + strippedAutomaticPackageRootPrefixes.add(packageRootWithoutFinalSlash); } } } - // Whitelist/blacklist classpath elements based on file resource paths - checkResourcePathWhiteBlackList(relativePath, log); - if (skipClasspathElement) { - return; + // Accept/reject classpath elements based on file resource paths + if (!checkResourcePathAcceptReject(relativePath, log)) { + continue; } // Get match status of the parent directory of this ZipEntry file's relative path (or reuse the last @@ -423,36 +589,43 @@ void scanPaths(final LogNode log) { final String parentRelativePath = lastSlashIdx < 0 ? "/" : relativePath.substring(0, lastSlashIdx + 1); final boolean parentRelativePathChanged = !parentRelativePath.equals(prevParentRelativePath); final ScanSpecPathMatch parentMatchStatus = // - parentRelativePathChanged ? scanSpec.dirWhitelistMatchStatus(parentRelativePath) + parentRelativePathChanged ? scanSpec.dirAcceptMatchStatus(parentRelativePath) : prevParentMatchStatus; prevParentRelativePath = parentRelativePath; prevParentMatchStatus = parentMatchStatus; - if (parentMatchStatus == ScanSpecPathMatch.HAS_BLACKLISTED_PATH_PREFIX) { - // The parent dir or one of its ancestral dirs is blacklisted + if (parentMatchStatus == ScanSpecPathMatch.HAS_REJECTED_PATH_PREFIX) { + // The parent dir or one of its ancestral dirs is rejected if (subLog != null) { - subLog.log("Skipping blacklisted path: " + relativePath); + subLog.log("Skipping rejected path: " + relativePath); } continue; } // Add the ZipEntry path as a Resource final Resource resource = newResource(zipEntry, relativePath); - if (relativePathToResource.putIfAbsent(relativePath, resource) == null - // If resource is whitelisted - && (parentMatchStatus == ScanSpecPathMatch.HAS_WHITELISTED_PATH_PREFIX - || parentMatchStatus == ScanSpecPathMatch.AT_WHITELISTED_PATH - || (parentMatchStatus == ScanSpecPathMatch.AT_WHITELISTED_CLASS_PACKAGE - && scanSpec.classfileIsSpecificallyWhitelisted(relativePath)) - || (scanSpec.enableClassInfo && relativePath.equals("module-info.class")))) { - // Resource is whitelisted - addWhitelistedResource(resource, parentMatchStatus, subLog); + if (relativePathToResource.putIfAbsent(relativePath, resource) == null) { + // If resource is accepted + if (parentMatchStatus == ScanSpecPathMatch.HAS_ACCEPTED_PATH_PREFIX + || parentMatchStatus == ScanSpecPathMatch.AT_ACCEPTED_PATH + || (parentMatchStatus == ScanSpecPathMatch.AT_ACCEPTED_CLASS_PACKAGE + && scanSpec.classfileIsSpecificallyAccepted(relativePath))) { + // Resource is accepted + addAcceptedResource(resource, parentMatchStatus, /* isClassfileOnly = */ false, subLog); + } else if (scanSpec.enableClassInfo && relativePath.equals("module-info.class")) { + // Add module descriptor as an accepted classfile resource, so that it is scanned, + // but don't add it to the list of resources in the ScanResult, since it is not + // in an accepted package (#352) + addAcceptedResource(resource, parentMatchStatus, /* isClassfileOnly = */ true, subLog); + } } } // Save the last modified time for the zipfile final File zipfile = getFile(); - fileToLastModified.put(zipfile, zipfile.lastModified()); + if (zipfile != null) { + fileToLastModified.put(zipfile, zipfile.lastModified()); + } finishScanPaths(subLog); } @@ -500,10 +673,36 @@ URI getURI() { } } + /** + * Return URI for classpath element, plus URIs for any stripped nested automatic package root prefixes, e.g. + * "!/BOOT-INF/classes". + */ + @Override + List getAllURIs() { + if (strippedAutomaticPackageRootPrefixes.isEmpty()) { + return Collections.singletonList(getURI()); + } else { + final URI uri = getURI(); + final List uris = new ArrayList<>(); + uris.add(uri); + final String uriStr = uri.toString(); + for (final String prefix : strippedAutomaticPackageRootPrefixes) { + try { + uris.add(new URI(uriStr + "!/" + prefix)); + } catch (final URISyntaxException e) { + // Ignore + } + } + return uris; + } + } + /** * Get the {@link File} for the outermost zipfile of this classpath element. * - * @return The {@link File} for the outermost zipfile of this classpath element. + * @return The {@link File} for the outermost zipfile of this classpath element, or null if this file was + * downloaded from a URL directly to RAM, or if the classpath element was backed by a custom filesystem + * that supports the {@link Path} API put not the {@link File} API. */ @Override File getFile() { @@ -512,7 +711,7 @@ File getFile() { } else { // Not performing a full scan (only getting classpath elements), so logicalZipFile is not set final int plingIdx = rawPath.indexOf('!'); - final String outermostZipFilePathResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, + final String outermostZipFilePathResolved = FastPathResolver.resolve(FileUtils.currDirPath(), plingIdx < 0 ? rawPath : rawPath.substring(0, plingIdx)); return new File(outermostZipFilePathResolved); } @@ -532,13 +731,13 @@ public String toString() { * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(final Object o) { - if (o == this) { + public boolean equals(final Object obj) { + if (obj == this) { return true; - } else if (!(o instanceof ClasspathElementZip)) { + } else if (!(obj instanceof ClasspathElementZip)) { return false; } - final ClasspathElementZip other = (ClasspathElementZip) o; + final ClasspathElementZip other = (ClasspathElementZip) obj; return this.getZipFilePath().equals(other.getZipFilePath()); } diff --git a/src/main/java/io/github/classgraph/CloseableByteBuffer.java b/src/main/java/io/github/classgraph/CloseableByteBuffer.java new file mode 100644 index 000000000..316685576 --- /dev/null +++ b/src/main/java/io/github/classgraph/CloseableByteBuffer.java @@ -0,0 +1,79 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A wrapper for {@link ByteBuffer} that implements the {@link Closeable} interface, releasing the + * {@link ByteBuffer} when it is no longer needed. + */ +public class CloseableByteBuffer implements Closeable { + private ByteBuffer byteBuffer; + private Runnable onClose; + + /** + * A wrapper for {@link ByteBuffer} that implements the {@link Closeable} interface, releasing the + * {@link ByteBuffer} when it is no longer needed. + * + * @param byteBuffer + * The {@link ByteBuffer} to wrap + * @param onClose + * The method to run when {@link #close()} is called. + */ + CloseableByteBuffer(final ByteBuffer byteBuffer, final Runnable onClose) { + this.byteBuffer = byteBuffer; + this.onClose = onClose; + } + + /** + * Get the wrapped ByteBuffer. + * + * @return The wrapped {@link ByteBuffer}. + */ + public ByteBuffer getByteBuffer() { + return byteBuffer; + } + + /** Release the wrapped {@link ByteBuffer}. */ + @Override + public void close() throws IOException { + if (onClose != null) { + try { + onClose.run(); + } catch (final Exception e) { + // Ignore + } + onClose = null; + } + byteBuffer = null; + } +} diff --git a/src/main/java/io/github/classgraph/FieldInfo.java b/src/main/java/io/github/classgraph/FieldInfo.java index e09228ff0..6c0f60860 100644 --- a/src/main/java/io/github/classgraph/FieldInfo.java +++ b/src/main/java/io/github/classgraph/FieldInfo.java @@ -31,34 +31,22 @@ import java.lang.annotation.Repeatable; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Map; import java.util.Set; import io.github.classgraph.ClassInfo.RelType; +import io.github.classgraph.Classfile.TypeAnnotationDecorator; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.TypeUtils; import nonapi.io.github.classgraph.types.TypeUtils.ModifierType; +import nonapi.io.github.classgraph.utils.LogNode; /** * Holds metadata about fields of a class encountered during a scan. All values are taken directly out of the * classfile for the class. */ -public class FieldInfo extends ScanResultObject implements Comparable, HasName { - - /** The declaring class name. */ - private String declaringClassName; - - /** The name of the field. */ - private String name; - - /** The modifiers. */ - private int modifiers; - - /** The type signature string. */ - private String typeSignatureStr; - - /** The type descriptor string. */ - private String typeDescriptorStr; - +public class FieldInfo extends ClassMemberInfo implements Comparable { /** The parsed type signature. */ private transient TypeSignature typeSignature; @@ -69,8 +57,8 @@ public class FieldInfo extends ScanResultObject implements Comparable // This is transient because the constant initializer value is final, so the value doesn't need to be serialized private ObjectTypedValueWrapper constantInitializerValue; - /** The annotation on the field, if any. */ - AnnotationInfoList annotationInfo; + /** The type annotation decorators for the {@link TypeSignature} instance of this field. */ + private transient List typeAnnotationDecorators; // ------------------------------------------------------------------------------------------------------------- @@ -99,84 +87,41 @@ public class FieldInfo extends ScanResultObject implements Comparable */ FieldInfo(final String definingClassName, final String fieldName, final int modifiers, final String typeDescriptorStr, final String typeSignatureStr, final Object constantInitializerValue, - final AnnotationInfoList annotationInfo) { - super(); + final AnnotationInfoList annotationInfo, final List typeAnnotationDecorators) { + super(definingClassName, fieldName, modifiers, typeDescriptorStr, typeSignatureStr, annotationInfo); if (fieldName == null) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException("fieldName must not be null"); } - this.declaringClassName = definingClassName; - this.name = fieldName; - this.modifiers = modifiers; - this.typeDescriptorStr = typeDescriptorStr; - this.typeSignatureStr = typeSignatureStr; - this.constantInitializerValue = constantInitializerValue == null ? null : new ObjectTypedValueWrapper(constantInitializerValue); - this.annotationInfo = annotationInfo == null || annotationInfo.isEmpty() ? null : annotationInfo; + this.typeAnnotationDecorators = typeAnnotationDecorators; } // ------------------------------------------------------------------------------------------------------------- /** - * Get the name of the field. - * - * @return The name of the field. - */ - @Override - public String getName() { - return name; - } - - /** - * Get the {@link ClassInfo} object for the declaring class (i.e. the class that declares this field). - * - * @return The {@link ClassInfo} object for the declaring class (i.e. the class that declares this field). + * Deprecated -- use {@link #getModifiersStr()} instead. + * + * @deprecated Use {@link #getModifiersStr()} instead. + * @return The field modifiers, as a string. */ - @Override - public ClassInfo getClassInfo() { - return super.getClassInfo(); + @Deprecated + public String getModifierStr() { + return getModifiersStr(); } - // ------------------------------------------------------------------------------------------------------------- - /** * Get the field modifiers as a string, e.g. "public static final". For the modifier bits, call getModifiers(). * * @return The field modifiers, as a string. */ - public String getModifierStr() { + @Override + public String getModifiersStr() { final StringBuilder buf = new StringBuilder(); TypeUtils.modifiersToString(modifiers, ModifierType.FIELD, /* ignored */ false, buf); return buf.toString(); } - /** - * Returns true if this field is public. - * - * @return True if the field is public. - */ - public boolean isPublic() { - return Modifier.isPublic(modifiers); - } - - /** - * Returns true if this field is static. - * - * @return True if the field is static. - */ - public boolean isStatic() { - return Modifier.isStatic(modifiers); - } - - /** - * Returns true if this field is final. - * - * @return True if the field is final. - */ - public boolean isFinal() { - return Modifier.isFinal(modifiers); - } - /** * Returns true if this field is a transient field. * @@ -187,52 +132,81 @@ public boolean isTransient() { } /** - * Returns the modifier bits for the field. + * Returns true if this field is an enum constant. * - * @return The modifier bits. + * @return True if the field is an enum constant. */ - public int getModifiers() { - return modifiers; + public boolean isEnum() { + return (modifiers & 0x4000) != 0; } /** - * Returns the parsed type descriptor for the field, if available. + * Returns the parsed type descriptor for the field, which will not include type parameters. If you need generic + * type parameters, call {@link #getTypeSignature()} instead. * - * @return The parsed type descriptor for the field, if available, else returns null. + * @return The parsed type descriptor string for the field. */ + @Override public TypeSignature getTypeDescriptor() { - if (typeDescriptorStr == null) { - return null; - } - if (typeDescriptor == null) { - try { - typeDescriptor = TypeSignature.parse(typeDescriptorStr, declaringClassName); - typeDescriptor.setScanResult(scanResult); - } catch (final ParseException e) { - throw new IllegalArgumentException(e); + synchronized (this) { + if (typeDescriptorStr == null) { + return null; + } + if (typeDescriptor == null) { + try { + typeDescriptor = TypeSignature.parse(typeDescriptorStr, declaringClassName); + typeDescriptor.setScanResult(scanResult); + if (typeAnnotationDecorators != null) { + for (final TypeAnnotationDecorator decorator : typeAnnotationDecorators) { + decorator.decorate(typeDescriptor); + } + } + } catch (final ParseException e) { + throw new IllegalArgumentException(e); + } } + return typeDescriptor; } - return typeDescriptor; } /** - * Returns the parsed type signature for the field, if available. + * Returns the parsed type signature for the field, possibly including type parameters. If this returns null, + * indicating that no type signature information is available for this field, call {@link #getTypeDescriptor()} + * instead. * - * @return The parsed type signature for the field, if available, else returns null. + * @return The parsed type signature for the field, or null if not available. + * @throws IllegalArgumentException + * if the field type signature cannot be parsed (this should only be thrown in the case of classfile + * corruption, or a compiler bug that causes an invalid type signature to be written to the + * classfile). */ + @Override public TypeSignature getTypeSignature() { - if (typeSignatureStr == null) { - return null; - } - if (typeSignature == null) { - try { - typeSignature = TypeSignature.parse(typeSignatureStr, declaringClassName); - typeSignature.setScanResult(scanResult); - } catch (final ParseException e) { - throw new IllegalArgumentException(e); + synchronized (this) { + if (typeSignatureStr == null) { + return null; } + if (typeSignature == null) { + try { + typeSignature = TypeSignature.parse(typeSignatureStr, declaringClassName); + typeSignature.setScanResult(scanResult); + if (typeAnnotationDecorators != null) { + for (final TypeAnnotationDecorator decorator : typeAnnotationDecorators) { + decorator.decorate(typeSignature); + } + } + } catch (final ParseException e) { + throw new IllegalArgumentException( + "Invalid type signature for field " + getClassName() + "." + getName() + + (getClassInfo() != null + ? " in classpath element " + getClassInfo().getClasspathElementURI() + : "") + + " : " + typeSignatureStr, + e); + } + } + return typeSignature; } - return typeSignature; } /** @@ -243,21 +217,30 @@ public TypeSignature getTypeSignature() { * @return The parsed type signature for the field, or if not available, the parsed type descriptor for the * field. */ + @Override public TypeSignature getTypeSignatureOrTypeDescriptor() { - final TypeSignature typeSig = getTypeSignature(); - if (typeSig != null) { - return typeSig; - } else { - return getTypeDescriptor(); + TypeSignature typeSig = null; + try { + typeSig = getTypeSignature(); + if (typeSig != null) { + return typeSig; + } + } catch (final Exception e) { + // Ignore } + return getTypeDescriptor(); } /** - * Returns the constant initializer value of a constant final field. Requires - * {@link ClassGraph#enableStaticFinalFieldConstantInitializerValues()} to have been called. + * Returns the constant initializer value of a field. Requires + * {@link ClassGraph#enableStaticFinalFieldConstantInitializerValues()} to have been called. Will only return + * non-null for fields that have constant initializers, which is usually only fields of primitive type, or + * String constants. Also note that it is up to the compiler as to whether or not a constant-valued field is + * assigned as a constant in the field definition itself, or whether it is assigned manually in static or + * non-static class initializer blocks or the constructor -- so your mileage may vary in being able to extract + * constant initializer values. * - * @return The initializer value, if this is a static final field, and has a constant initializer value, or null - * if none. + * @return The initializer value, if this field has a constant initializer value, or null if none. */ public Object getConstantInitializerValue() { if (!scanResult.scanSpec.enableStaticFinalFieldConstantInitializerValues) { @@ -267,58 +250,6 @@ public Object getConstantInitializerValue() { return constantInitializerValue == null ? null : constantInitializerValue.get(); } - /** - * Get a list of annotations on this field, along with any annotation parameter values, wrapped in - * {@link AnnotationInfo} objects. - * - * @return A list of annotations on this field, along with any annotation parameter values, wrapped in - * {@link AnnotationInfo} objects, or the empty list if none. - */ - public AnnotationInfoList getAnnotationInfo() { - if (!scanResult.scanSpec.enableAnnotationInfo) { - throw new IllegalArgumentException("Please call ClassGraph#enableAnnotationInfo() before #scan()"); - } - return annotationInfo == null ? AnnotationInfoList.EMPTY_LIST - : AnnotationInfoList.getIndirectAnnotations(annotationInfo, /* annotatedClass = */ null); - } - - /** - * Get a the named non-{@link Repeatable} annotation on this field, or null if the field does not have the named - * annotation. (Use {@link #getAnnotationInfoRepeatable(String)} for {@link Repeatable} annotations.) - * - * @param annotationName - * The annotation name. - * @return An {@link AnnotationInfo} object representing the named annotation on this field, or null if the - * field does not have the named annotation. - */ - public AnnotationInfo getAnnotationInfo(final String annotationName) { - return getAnnotationInfo().get(annotationName); - } - - /** - * Get a the named {@link Repeatable} annotation on this field, or the empty list if the field does not have the - * named annotation. - * - * @param annotationName - * The annotation name. - * @return An {@link AnnotationInfoList} of all instances of the named annotation on this field, or the empty - * list if the field does not have the named annotation. - */ - public AnnotationInfoList getAnnotationInfoRepeatable(final String annotationName) { - return getAnnotationInfo().getRepeatable(annotationName); - } - - /** - * Check if the field has a given named annotation. - * - * @param annotationName - * The name of an annotation. - * @return true if this field has the named annotation. - */ - public boolean hasAnnotation(final String annotationName) { - return getAnnotationInfo().containsName(annotationName); - } - // ------------------------------------------------------------------------------------------------------------- /** @@ -326,7 +257,7 @@ public boolean hasAnnotation(final String annotationName) { * * @return The {@link Field} reference for this field. * @throws IllegalArgumentException - * if the field does not exist. + * if the class can't be loaded or the field does not exist. */ public Field loadClassAndGetField() throws IllegalArgumentException { try { @@ -351,23 +282,13 @@ public Field loadClassAndGetField() throws IllegalArgumentException { void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames) { if (annotationInfo != null) { annotationInfo.handleRepeatableAnnotations(allRepeatableAnnotationNames, getClassInfo(), - RelType.FIELD_ANNOTATIONS, RelType.CLASSES_WITH_FIELD_ANNOTATION); + RelType.FIELD_ANNOTATIONS, RelType.CLASSES_WITH_FIELD_ANNOTATION, + RelType.CLASSES_WITH_NONPRIVATE_FIELD_ANNOTATION); } } // ------------------------------------------------------------------------------------------------------------- - /** - * Returns the name of the declaring class, so that super.getClassInfo() returns the {@link ClassInfo} object - * for the declaring class. - * - * @return the name of the declaring class. - */ - @Override - protected String getClassName() { - return declaringClassName; - } - /* (non-Javadoc) * @see io.github.classgraph.ScanResultObject#setScanResult(io.github.classgraph.ScanResult) */ @@ -388,24 +309,41 @@ void setScanResult(final ScanResult scanResult) { } /** - * Get the names of any classes in the type descriptor or type signature. + * Get {@link ClassInfo} objects for any classes referenced in the type descriptor or type signature. * - * @param classNames - * the names of any classes in the type descriptor or type signature. + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info */ @Override - protected void findReferencedClassNames(final Set classNames) { - final TypeSignature methodSig = getTypeSignature(); - if (methodSig != null) { - methodSig.findReferencedClassNames(classNames); + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + try { + final TypeSignature fieldSig = getTypeSignature(); + if (fieldSig != null) { + fieldSig.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + } + } catch (final IllegalArgumentException e) { + if (log != null) { + log.log("Illegal type signature for field " + getClassName() + "." + getName() + ": " + + getTypeSignatureStr()); + } } - final TypeSignature methodDesc = getTypeDescriptor(); - if (methodDesc != null) { - methodDesc.findReferencedClassNames(classNames); + try { + final TypeSignature fieldDesc = getTypeDescriptor(); + if (fieldDesc != null) { + fieldDesc.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + } + } catch (final IllegalArgumentException e) { + if (log != null) { + log.log("Illegal type descriptor for field " + getClassName() + "." + getName() + ": " + + getTypeDescriptorStr()); + } } if (annotationInfo != null) { for (final AnnotationInfo ai : annotationInfo) { - ai.findReferencedClassNames(classNames); + ai.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } } } @@ -421,13 +359,9 @@ protected void findReferencedClassNames(final Set classNames) { */ @Override public boolean equals(final Object obj) { - if (this == obj) { + if (obj == this) { return true; - } - if (obj == null) { - return false; - } - if (this.getClass() != obj.getClass()) { + } else if (!(obj instanceof FieldInfo)) { return false; } final FieldInfo other = (FieldInfo) obj; @@ -460,33 +394,32 @@ public int compareTo(final FieldInfo other) { return name.compareTo(other.name); } - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - final StringBuilder buf = new StringBuilder(); + // ------------------------------------------------------------------------------------------------------------- + void toString(final boolean includeModifiers, final boolean useSimpleNames, final StringBuilder buf) { if (annotationInfo != null) { for (final AnnotationInfo annotation : annotationInfo) { - if (buf.length() > 0) { + // There can be a paren in the previous position if this field is a record parameter + if (buf.length() > 0 && buf.charAt(buf.length() - 1) != ' ' + && buf.charAt(buf.length() - 1) != '(') { buf.append(' '); } - buf.append(annotation.toString()); + annotation.toString(useSimpleNames, buf); } } - if (modifiers != 0) { - if (buf.length() > 0) { + if (modifiers != 0 && includeModifiers) { + if (buf.length() > 0 && buf.charAt(buf.length() - 1) != ' ' && buf.charAt(buf.length() - 1) != '(') { buf.append(' '); } TypeUtils.modifiersToString(modifiers, ModifierType.FIELD, /* ignored */ false, buf); } - if (buf.length() > 0) { + if (buf.length() > 0 && buf.charAt(buf.length() - 1) != ' ' && buf.charAt(buf.length() - 1) != '(') { buf.append(' '); } - buf.append(getTypeSignatureOrTypeDescriptor().toString()); + final TypeSignature typeSig = getTypeSignatureOrTypeDescriptor(); + typeSig.toStringInternal(useSimpleNames, /* annotationsToExclude = */ annotationInfo, buf); buf.append(' '); buf.append(name); @@ -500,10 +433,13 @@ public String toString() { buf.append('\'').append(((Character) val).toString().replace("\\", "\\\\").replaceAll("'", "\\'")) .append('\''); } else { - buf.append(val.toString()); + buf.append(val == null ? "null" : val.toString()); } } + } - return buf.toString(); + @Override + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + toString(true, useSimpleNames, buf); } } diff --git a/src/main/java/io/github/classgraph/FieldInfoList.java b/src/main/java/io/github/classgraph/FieldInfoList.java index fe82fca05..efd3b8468 100644 --- a/src/main/java/io/github/classgraph/FieldInfoList.java +++ b/src/main/java/io/github/classgraph/FieldInfoList.java @@ -29,102 +29,74 @@ package io.github.classgraph; import java.util.Collection; +import java.util.Map; import java.util.Set; +import nonapi.io.github.classgraph.utils.LogNode; + /** A list of {@link FieldInfo} objects. */ public class FieldInfoList extends MappableInfoList { + /** serialVersionUID */ + private static final long serialVersionUID = 1L; + + /** An unmodifiable empty {@link FieldInfoList}. */ + static final FieldInfoList EMPTY_LIST = new FieldInfoList(); + static { + EMPTY_LIST.makeUnmodifiable(); + } /** - * Constructor. + * Return an unmodifiable empty {@link FieldInfoList}. + * + * @return the unmodifiable empty {@link FieldInfoList}. */ - FieldInfoList() { + public static FieldInfoList emptyList() { + return EMPTY_LIST; + } + + /** + * Construct a new modifiable empty list of {@link FieldInfo} objects. + */ + public FieldInfoList() { super(); } /** - * Constructor. + * Construct a new modifiable empty list of {@link FieldInfo} objects, given a size hint. * * @param sizeHint * the size hint */ - FieldInfoList(final int sizeHint) { + public FieldInfoList(final int sizeHint) { super(sizeHint); } /** - * Constructor. + * Construct a new modifiable empty {@link FieldInfoList}, given an initial list of {@link FieldInfo} objects. * * @param fieldInfoCollection - * the field info collection + * the collection of {@link FieldInfo} objects. */ - FieldInfoList(final Collection fieldInfoCollection) { + public FieldInfoList(final Collection fieldInfoCollection) { super(fieldInfoCollection); } - /** An unmodifiable empty {@link FieldInfoList}. */ - static final FieldInfoList EMPTY_LIST = new FieldInfoList() { - @Override - public boolean add(final FieldInfo e) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void add(final int index, final FieldInfo element) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean remove(final Object o) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public FieldInfo remove(final int index) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final int index, final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean removeAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean retainAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void clear() { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public FieldInfo set(final int index, final FieldInfo element) { - throw new IllegalArgumentException("List is immutable"); - } - }; - // ------------------------------------------------------------------------------------------------------------- /** - * Find the names of any classes referenced in the fields in this list. + * Get {@link ClassInfo} objects for any classes referenced in the list. * - * @param referencedClassNames - * the referenced class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info + * @param log + * the log */ - void findReferencedClassNames(final Set referencedClassNames) { + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { for (final FieldInfo fi : this) { - fi.findReferencedClassNames(referencedClassNames); + fi.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } } diff --git a/src/main/java/io/github/classgraph/GraphvizDotfileGenerator.java b/src/main/java/io/github/classgraph/GraphvizDotfileGenerator.java index c582a5b64..37660145c 100644 --- a/src/main/java/io/github/classgraph/GraphvizDotfileGenerator.java +++ b/src/main/java/io/github/classgraph/GraphvizDotfileGenerator.java @@ -29,11 +29,11 @@ package io.github.classgraph; import java.util.BitSet; -import java.util.Collections; import java.util.HashSet; import java.util.Set; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.CollectionUtils; /** Builds a class graph visualization in Graphviz .dot file format. */ final class GraphvizDotfileGenerator { @@ -269,7 +269,7 @@ private static void labelClassNodeHTML(final ClassInfo ci, final String shape, f buf.append("ANNOTATIONS"); final AnnotationInfoList annotationInfoSorted = new AnnotationInfoList(annotationInfo); - Collections.sort(annotationInfoSorted); + CollectionUtils.sortIfNotEmpty(annotationInfoSorted); for (final AnnotationInfo ai : annotationInfoSorted) { final String annotationName = ai.getName(); if (!annotationName.startsWith("java.lang.annotation.")) { @@ -285,7 +285,7 @@ private static void labelClassNodeHTML(final ClassInfo ci, final String shape, f final FieldInfoList fieldInfo = ci.fieldInfo; if (showFields && fieldInfo != null && !fieldInfo.isEmpty()) { final FieldInfoList fieldInfoSorted = new FieldInfoList(fieldInfo); - Collections.sort(fieldInfoSorted); + CollectionUtils.sortIfNotEmpty(fieldInfoSorted); for (int i = fieldInfoSorted.size() - 1; i >= 0; --i) { // Remove serialVersionUID field if (fieldInfoSorted.get(i).getName().equals("serialVersionUID")) { @@ -319,7 +319,7 @@ private static void labelClassNodeHTML(final ClassInfo ci, final String shape, f if (buf.charAt(buf.length() - 1) != ' ') { buf.append(' '); } - buf.append(fi.getModifierStr()); + buf.append(fi.getModifiersStr()); } // Field type @@ -345,7 +345,7 @@ private static void labelClassNodeHTML(final ClassInfo ci, final String shape, f final MethodInfoList methodInfo = ci.methodInfo; if (showMethods && methodInfo != null) { final MethodInfoList methodInfoSorted = new MethodInfoList(methodInfo); - Collections.sort(methodInfoSorted); + CollectionUtils.sortIfNotEmpty(methodInfoSorted); for (int i = methodInfoSorted.size() - 1; i >= 0; --i) { // Don't list static initializer blocks or methods of Object final MethodInfo mi = methodInfoSorted.get(i); @@ -573,10 +573,10 @@ static String generateGraphVizDotFile(final ClassInfoList classInfoList, final f if (showFieldTypeDependencyEdges && classNode.fieldInfo != null) { for (final FieldInfo fi : classNode.fieldInfo) { - for (final String referencedFieldTypeName : fi.findReferencedClassNames()) { - if (allVisibleNodes.contains(referencedFieldTypeName)) { + for (final ClassInfo referencedFieldType : fi.findReferencedClassInfo(/* log = */ null)) { + if (allVisibleNodes.contains(referencedFieldType.getName())) { // class --[ ] field type (open box) - buf.append(" \"").append(referencedFieldTypeName).append("\" -> \"") + buf.append(" \"").append(referencedFieldType.getName()).append("\" -> \"") .append(classNode.getName()) .append("\" [arrowtail=obox, arrowsize=2.5, dir=back]\n"); } @@ -586,10 +586,10 @@ static String generateGraphVizDotFile(final ClassInfoList classInfoList, final f if (showMethodTypeDependencyEdges && classNode.methodInfo != null) { for (final MethodInfo mi : classNode.methodInfo) { - for (final String referencedMethodTypeName : mi.findReferencedClassNames()) { - if (allVisibleNodes.contains(referencedMethodTypeName)) { + for (final ClassInfo referencedMethodType : mi.findReferencedClassInfo(/* log = */ null)) { + if (allVisibleNodes.contains(referencedMethodType.getName())) { // class --[#] field type (open box) - buf.append(" \"").append(referencedMethodTypeName).append("\" -> \"") + buf.append(" \"").append(referencedMethodType.getName()).append("\" -> \"") .append(classNode.getName()) .append("\" [arrowtail=box, arrowsize=2.5, dir=back]\n"); } diff --git a/src/main/java/io/github/classgraph/HasName.java b/src/main/java/io/github/classgraph/HasName.java index 7080ca7e2..5ac3794f0 100644 --- a/src/main/java/io/github/classgraph/HasName.java +++ b/src/main/java/io/github/classgraph/HasName.java @@ -35,5 +35,5 @@ public interface HasName { * * @return The name. */ - public String getName(); + String getName(); } diff --git a/src/main/java/io/github/classgraph/HierarchicalTypeSignature.java b/src/main/java/io/github/classgraph/HierarchicalTypeSignature.java index ecadde023..d3f6bb8a2 100644 --- a/src/main/java/io/github/classgraph/HierarchicalTypeSignature.java +++ b/src/main/java/io/github/classgraph/HierarchicalTypeSignature.java @@ -28,18 +28,87 @@ */ package io.github.classgraph; -import java.util.Set; +import java.util.List; + +import io.github.classgraph.Classfile.TypePathNode; /** * A Java type signature. Subclasses are ClassTypeSignature, MethodTypeSignature, and TypeSignature. */ public abstract class HierarchicalTypeSignature extends ScanResultObject { + protected AnnotationInfoList typeAnnotationInfo; + + /** A hierarchical type signature. */ + public HierarchicalTypeSignature() { + super(); + } + + /** + * Add a type annotation. + * + * @param annotationInfo + * the annotation + */ + protected void addTypeAnnotation(final AnnotationInfo annotationInfo) { + if (typeAnnotationInfo == null) { + typeAnnotationInfo = new AnnotationInfoList(1); + } + typeAnnotationInfo.add(annotationInfo); + } + + @Override + void setScanResult(final ScanResult scanResult) { + super.setScanResult(scanResult); + if (typeAnnotationInfo != null) { + for (final AnnotationInfo annotationInfo : typeAnnotationInfo) { + annotationInfo.setScanResult(scanResult); + } + } + } + + /** + * Get a list of {@link AnnotationInfo} objects for any type annotations on this type, or null if none. + * + * @return a list of {@link AnnotationInfo} objects for any type annotations on this type, or null if none. + */ + public AnnotationInfoList getTypeAnnotationInfo() { + return typeAnnotationInfo; + } + + /** + * Add a type annotation. + * + * @param typePath + * the type path + * @param annotationInfo + * the annotation + */ + protected abstract void addTypeAnnotation(List typePath, AnnotationInfo annotationInfo); + + /** + * Render type signature to string. + * + * @param useSimpleNames + * whether to use simple names for classes. + * @param annotationsToExclude + * toplevel annotations to exclude, to eliminate duplication (toplevel annotations are both + * class/field/method annotations and type annotations). + * @param buf + * the {@link StringBuilder} to write to. + */ + protected abstract void toStringInternal(final boolean useSimpleNames, AnnotationInfoList annotationsToExclude, + StringBuilder buf); + /** - * Find the names of all classes referenced in the type signature. + * Render type signature to string. * - * @param classNames - * The set to store class names in. + * @param useSimpleNames + * whether to use simple names for classes. + * @param buf + * the {@link StringBuilder} to write to. */ @Override - abstract void findReferencedClassNames(final Set classNames); + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + toStringInternal(useSimpleNames, /* annotationsToExclude = */ null, buf); + } } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/InfoList.java b/src/main/java/io/github/classgraph/InfoList.java index 71aa3006b..444520a2f 100644 --- a/src/main/java/io/github/classgraph/InfoList.java +++ b/src/main/java/io/github/classgraph/InfoList.java @@ -39,7 +39,7 @@ * @param * the element type */ -public class InfoList extends ArrayList { +public class InfoList extends PotentiallyUnmodifiableList { /** serialVersionUID. */ static final long serialVersionUID = 1L; @@ -70,6 +70,18 @@ public class InfoList extends ArrayList { super(infoCollection); } + // Keep Scrutinizer happy + @Override + public boolean equals(final Object o) { + return super.equals(o); + } + + // Keep Scrutinizer happy + @Override + public int hashCode() { + return super.hashCode(); + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -83,7 +95,9 @@ public List getNames() { } else { final List names = new ArrayList<>(this.size()); for (final T i : this) { - names.add(i.getName()); + if (i != null) { + names.add(i.getName()); + } } return names; } @@ -108,6 +122,28 @@ public List getAsStrings() { } } - // ------------------------------------------------------------------------------------------------------------- - + /** + * Get the String representations of all items in this list, using only simple + * names of any named classes, by calling {@code ScanResultObject#toStringWithSimpleNames()} if the object + * is a subclass of {@code ScanResultObject} (e.g. {@link ClassInfo}, {@link MethodInfo} or {@link FieldInfo} + * object), otherwise calling {@code toString()}, for each item in the list. + * + * @return The String representations of all items in this list, using only the + * simple names of any named classes. + */ + public List getAsStringsWithSimpleNames() { + if (this.isEmpty()) { + return Collections.emptyList(); + } else { + final List toStringVals = new ArrayList<>(this.size()); + for (final T i : this) { + toStringVals.add(i == null ? "null" + : i instanceof ScanResultObject ? ((ScanResultObject) i).toStringWithSimpleNames() + : i.toString()); + } + return toStringVals; + } + } } diff --git a/src/main/java/io/github/classgraph/MappableInfoList.java b/src/main/java/io/github/classgraph/MappableInfoList.java index 9bead2843..50811f988 100644 --- a/src/main/java/io/github/classgraph/MappableInfoList.java +++ b/src/main/java/io/github/classgraph/MappableInfoList.java @@ -39,6 +39,9 @@ * the element type */ public class MappableInfoList extends InfoList { + /** serialVersionUID */ + private static final long serialVersionUID = 1L; + /** * Constructor. */ @@ -76,7 +79,9 @@ public class MappableInfoList extends InfoList { public Map asMap() { final Map nameToInfoObject = new HashMap<>(); for (final T i : this) { - nameToInfoObject.put(i.getName(), i); + if (i != null) { + nameToInfoObject.put(i.getName(), i); + } } return nameToInfoObject; } @@ -90,7 +95,7 @@ public Map asMap() { */ public boolean containsName(final String name) { for (final T i : this) { - if (i.getName().equals(name)) { + if (i != null && i.getName().equals(name)) { return true; } } @@ -104,9 +109,10 @@ public boolean containsName(final String name) { * The name to search for. * @return The list item with the given name, or null if not found. */ + @SuppressWarnings("null") public T get(final String name) { for (final T i : this) { - if (i.getName().equals(name)) { + if (i != null && i.getName().equals(name)) { return i; } } diff --git a/src/main/java/io/github/classgraph/MethodInfo.java b/src/main/java/io/github/classgraph/MethodInfo.java index f99ce0327..b688773c7 100644 --- a/src/main/java/io/github/classgraph/MethodInfo.java +++ b/src/main/java/io/github/classgraph/MethodInfo.java @@ -28,50 +28,33 @@ */ package io.github.classgraph; +import java.lang.annotation.Annotation; import java.lang.annotation.Repeatable; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Set; import io.github.classgraph.ClassInfo.RelType; +import io.github.classgraph.Classfile.MethodTypeAnnotationDecorator; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.TypeUtils; import nonapi.io.github.classgraph.types.TypeUtils.ModifierType; +import nonapi.io.github.classgraph.utils.Assert; +import nonapi.io.github.classgraph.utils.LogNode; /** * Holds metadata about methods of a class encountered during a scan. All values are taken directly out of the * classfile for the class. */ -public class MethodInfo extends ScanResultObject implements Comparable, HasName { - /** Defining class name. */ - private String declaringClassName; - - /** Method name. */ - private String name; - - /** Method modifiers. */ - private int modifiers; - - /** Method annotations. */ - AnnotationInfoList annotationInfo; - - /** - * The JVM-internal type descriptor (missing type parameters, but including types for synthetic and mandated - * method parameters). - */ - private String typeDescriptorStr; - +public class MethodInfo extends ClassMemberInfo implements Comparable { /** The parsed type descriptor. */ private transient MethodTypeSignature typeDescriptor; - /** - * The type signature (may have type parameter information included, if present and available). Method parameter - * types are unaligned. - */ - private String typeSignatureStr; - /** The parsed type signature (or null if none). Method parameter types are unaligned. */ private transient MethodTypeSignature typeSignature; @@ -96,6 +79,19 @@ public class MethodInfo extends ScanResultObject implements Comparable typeAnnotationDecorators; + + private String[] thrownExceptionNames; + + private transient ClassInfoList thrownExceptions; + // ------------------------------------------------------------------------------------------------------------- /** Default constructor for deserialization. */ @@ -126,23 +122,30 @@ public class MethodInfo extends ScanResultObject implements Comparable methodTypeAnnotationDecorators, + final String[] thrownExceptionNames) { + super(definingClassName, methodName, modifiers, typeDescriptorStr, typeSignatureStr, methodAnnotationInfo); this.parameterNames = parameterNames; this.parameterModifiers = parameterModifiers; this.parameterAnnotationInfo = parameterAnnotationInfo; - this.annotationInfo = methodAnnotationInfo == null || methodAnnotationInfo.isEmpty() ? null - : methodAnnotationInfo; this.hasBody = hasBody; + this.minLineNum = minLineNum; + this.maxLineNum = maxLineNum; + this.typeAnnotationDecorators = methodTypeAnnotationDecorators; + this.thrownExceptionNames = thrownExceptionNames; } // ------------------------------------------------------------------------------------------------------------- @@ -158,128 +161,185 @@ public String getName() { return name; } - /** - * Returns the modifier bits for the method. - * - * @return The modifier bits for the method. - */ - public int getModifiers() { - return modifiers; - } - /** * Get the method modifiers as a String, e.g. "public static final". For the modifier bits, call * {@link #getModifiers()}. * * @return The modifiers for the method, as a String. */ + @Override public String getModifiersStr() { final StringBuilder buf = new StringBuilder(); TypeUtils.modifiersToString(modifiers, ModifierType.METHOD, isDefault(), buf); return buf.toString(); } - /** - * Get the {@link ClassInfo} object for the declaring class (i.e. the class that declares this method). - * - * @return The {@link ClassInfo} object for the declaring class (i.e. the class that declares this method). - */ - @Override - public ClassInfo getClassInfo() { - return super.getClassInfo(); - } - /** * Returns the parsed type descriptor for the method, which will not include type parameters. If you need - * generic type parameters, call getTypeSignature() instead. + * generic type parameters, call {@link #getTypeSignature()} instead. * * @return The parsed type descriptor for the method. */ + @Override public MethodTypeSignature getTypeDescriptor() { - if (typeDescriptor == null) { - try { - typeDescriptor = MethodTypeSignature.parse(typeDescriptorStr, declaringClassName); - typeDescriptor.setScanResult(scanResult); - } catch (final ParseException e) { - throw new IllegalArgumentException(e); + synchronized (this) { + if (typeDescriptor == null) { + try { + typeDescriptor = MethodTypeSignature.parse(typeDescriptorStr, declaringClassName); + typeDescriptor.setScanResult(scanResult); + if (typeAnnotationDecorators != null) { + // It is possible that there are extra implicit params added at the beginning of the + // parameter list that type annotations don't take into account. Assume that the + // type signature has the correct number of parameters, and temporarily remove any + // implicit prefix parameters during the type decoration process. See getParameterInfo(). + int sigNumParam = 0; + final MethodTypeSignature sig = getTypeSignature(); + if (sig == null) { + // There is no type signature -- run type annotation decorators on descriptor + for (final MethodTypeAnnotationDecorator decorator : typeAnnotationDecorators) { + decorator.decorate(typeDescriptor); + } + } else { + // Determine how many extra implicit params there are + sigNumParam = sig.getParameterTypeSignatures().size(); + final int descNumParam = typeDescriptor.getParameterTypeSignatures().size(); + final int numImplicitPrefixParams = descNumParam - sigNumParam; + if (numImplicitPrefixParams < 0) { + // Sanity check -- should not happen + throw new IllegalArgumentException( + "Fewer params in method type descriptor than in method type signature"); + } else if (numImplicitPrefixParams == 0) { + // There are no implicit prefix params -- + // run type annotation decorators on descriptor + for (final MethodTypeAnnotationDecorator decorator : typeAnnotationDecorators) { + decorator.decorate(typeDescriptor); + } + } else { + // There are implicit prefix params -- strip them temporarily from type descriptor, + // then run decorators, then add them back again + final List paramSigs = typeDescriptor.getParameterTypeSignatures(); + final List strippedParamSigs = paramSigs.subList(0, + numImplicitPrefixParams); + for (int i = 0; i < numImplicitPrefixParams; i++) { + paramSigs.remove(0); + } + for (final MethodTypeAnnotationDecorator decorator : typeAnnotationDecorators) { + decorator.decorate(typeDescriptor); + } + for (int i = numImplicitPrefixParams - 1; i >= 0; --i) { + paramSigs.add(0, strippedParamSigs.get(i)); + } + } + } + } + } catch (final ParseException e) { + throw new IllegalArgumentException(e); + } } + return typeDescriptor; } - return typeDescriptor; } /** * Returns the parsed type signature for the method, possibly including type parameters. If this returns null, - * indicating that no type signature information is available for this method, call getTypeDescriptor() instead. + * indicating that no type signature information is available for this method, call {@link #getTypeDescriptor()} + * instead. * * @return The parsed type signature for the method, or null if not available. + * @throws IllegalArgumentException + * if the method type signature cannot be parsed (this should only be thrown in the case of + * classfile corruption, or a compiler bug that causes an invalid type signature to be written to + * the classfile). */ + @Override public MethodTypeSignature getTypeSignature() { - if (typeSignature == null && typeSignatureStr != null) { - try { - typeSignature = MethodTypeSignature.parse(typeSignatureStr, declaringClassName); - typeSignature.setScanResult(scanResult); - } catch (final ParseException e) { - throw new IllegalArgumentException(e); + synchronized (this) { + if (typeSignature == null && typeSignatureStr != null) { + try { + typeSignature = MethodTypeSignature.parse(typeSignatureStr, declaringClassName); + typeSignature.setScanResult(scanResult); + if (typeAnnotationDecorators != null) { + for (final MethodTypeAnnotationDecorator decorator : typeAnnotationDecorators) { + decorator.decorate(typeSignature); + } + } + } catch (final ParseException e) { + throw new IllegalArgumentException( + "Invalid type signature for method " + getClassName() + "." + getName() + + (getClassInfo() != null + ? " in classpath element " + getClassInfo().getClasspathElementURI() + : "") + + " : " + typeSignatureStr, + e); + } } + return typeSignature; } - return typeSignature; } /** - * Returns the parsed type signature for the method, possibly including type parameters. If the parsed type - * signature is null, indicating that no type signature information is available for this method, returns the + * Returns the parsed type signature for the method, possibly including type parameters. If the type signature + * string is null, indicating that no type signature information is available for this method, returns the * parsed type descriptor instead. * * @return The parsed type signature for the method, or if not available, the parsed type descriptor for the * method. */ + @Override public MethodTypeSignature getTypeSignatureOrTypeDescriptor() { - final MethodTypeSignature typeSig = getTypeSignature(); - if (typeSig != null) { - return typeSig; - } else { - return getTypeDescriptor(); + MethodTypeSignature typeSig = null; + try { + typeSig = getTypeSignature(); + if (typeSig != null) { + return typeSig; + } + } catch (final Exception e) { + // Ignore } + return getTypeDescriptor(); } - // ------------------------------------------------------------------------------------------------------------- - /** - * Returns true if this method is a constructor. Constructors have the method name {@code - * ""}. This returns false for private static class initializer blocks, which are named - * {@code ""}. + * Returns the list of exceptions thrown by the method, as a {@link ClassInfoList}. * - * @return True if this method is a constructor. + * @return The list of exceptions thrown by the method, as a {@link ClassInfoList} (the list may be empty). */ - public boolean isConstructor() { - return "".equals(name); + public ClassInfoList getThrownExceptions() { + synchronized (this) { + if (thrownExceptions == null && thrownExceptionNames != null) { + thrownExceptions = new ClassInfoList(thrownExceptionNames.length); + for (final String thrownExceptionName : thrownExceptionNames) { + final ClassInfo classInfo = scanResult.getClassInfo(thrownExceptionName); + if (classInfo != null) { + thrownExceptions.add(classInfo); + classInfo.setScanResult(scanResult); + } + } + } + return thrownExceptions == null ? ClassInfoList.EMPTY_LIST : thrownExceptions; + } } /** - * Returns true if this method is public. + * Returns the exceptions thrown by the method, as an array. * - * @return True if this method is public. + * @return The exceptions thrown by the method, as an array (the array may be empty). */ - public boolean isPublic() { - return Modifier.isPublic(modifiers); + public String[] getThrownExceptionNames() { + return thrownExceptionNames == null ? new String[0] : thrownExceptionNames; } - /** - * Returns true if this method is static. - * - * @return True if this method is static. - */ - public boolean isStatic() { - return Modifier.isStatic(modifiers); - } + // ------------------------------------------------------------------------------------------------------------- /** - * Returns true if this method is final. + * Returns true if this method is a constructor. Constructors have the method name {@code + * ""}. This returns false for private static class initializer blocks, which are named + * {@code ""}. * - * @return True if this method is final. + * @return True if this method is a constructor. */ - public boolean isFinal() { - return Modifier.isFinal(modifiers); + public boolean isConstructor() { + return "".equals(name); } /** @@ -318,6 +378,24 @@ public boolean isNative() { return Modifier.isNative(modifiers); } + /** + * Returns true if this method is abstract. + * + * @return True if this method is abstract. + */ + public boolean isAbstract() { + return Modifier.isAbstract(modifiers); + } + + /** + * Returns true if this method is strict. + * + * @return True if this method is strict. + */ + public boolean isStrict() { + return Modifier.isStrict(modifiers); + } + /** * Returns true if this method has a body (i.e. has an implementation in the containing class). * @@ -327,6 +405,24 @@ public boolean hasBody() { return hasBody; } + /** + * The line number of the first non-empty line in the body of this method, or 0 if unknown. + * + * @return The line number of the first non-empty line in the body of this method, or 0 if unknown. + */ + public int getMinLineNum() { + return minLineNum; + } + + /** + * The line number of the last non-empty line in the body of this method, or 0 if unknown. + * + * @return The line number of the last non-empty line in the body of this method, or 0 if unknown. + */ + public int getMaxLineNum() { + return maxLineNum; + } + /** * Returns true if this is a default method (i.e. if this is a method in an interface and the method has a * body). @@ -346,164 +442,168 @@ public boolean isDefault() { * @return The {@link MethodParameterInfo} objects for the method parameters, one per parameter. */ public MethodParameterInfo[] getParameterInfo() { - if (parameterInfo == null) { - // Get params from the type descriptor, and from the type signature if available - final List paramTypeDescriptors = getTypeDescriptor().getParameterTypeSignatures(); - final List paramTypeSignatures = getTypeSignature() != null - ? getTypeSignature().getParameterTypeSignatures() - : null; - - // Figure out the number of params in the alignment (should be num params in type descriptor) - final int numParams = paramTypeDescriptors.size(); - if (paramTypeSignatures != null && paramTypeSignatures.size() > numParams) { - // Should not happen - throw ClassGraphException.newClassGraphException( - "typeSignatureParamTypes.size() > typeDescriptorParamTypes.size() for method " - + declaringClassName + "." + name); - } - - // Figure out number of other fields that need alignment, and check length for consistency - final int otherParamMax = Math.max(parameterNames == null ? 0 : parameterNames.length, - Math.max(parameterModifiers == null ? 0 : parameterModifiers.length, - parameterAnnotationInfo == null ? 0 : parameterAnnotationInfo.length)); - if (otherParamMax > numParams) { - // Should not happen - throw ClassGraphException.newClassGraphException("Type descriptor for method " + declaringClassName - + "." + name + " has insufficient parameters"); - } - - // Kotlin is very inconsistent about the arity of each of the parameter metadata types, see: - // https://github.com/classgraph/classgraph/issues/175#issuecomment-363031510 - // As a workaround, we assume that any synthetic / mandated parameters must come first in the - // parameter list, when the arities don't match, and we right-align the metadata fields. - // This is probably the safest assumption across JVM languages, even though this convention - // is by no means the only possibility. (Unfortunately we can't just rely on the modifier - // bits to find synthetic / mandated parameters, because these bits are not always available, - // and even when they are, they don't always give the right alignment, at least for Kotlin- - // generated code). - - String[] paramNamesAligned = null; - if (parameterNames != null && numParams > 0) { - if (parameterNames.length == numParams) { - // No alignment necessary - paramNamesAligned = parameterNames; - } else { - // Right-align when not the right length - paramNamesAligned = new String[numParams]; - for (int i = 0, lenDiff = numParams - parameterNames.length; i < parameterNames.length; i++) { - paramNamesAligned[lenDiff + i] = parameterNames[i]; + // Kotlin is very inconsistent about the arity of each of the parameter metadata types, see: + // https://github.com/classgraph/classgraph/issues/175#issuecomment-363031510 + // As a workaround, we assume that any synthetic / mandated parameters must come first in the + // parameter list, when the arities don't match, and we right-align the metadata fields. + // This is probably the safest assumption across JVM languages, even though this convention + // is by no means the only possibility. (Unfortunately we can't just rely on the modifier + // bits to find synthetic / mandated parameters, because these bits are not always available, + // and even when they are, they don't always give the right alignment, at least for Kotlin- + // generated code). + + // Actually the Java spec says specifically: "The signature and descriptor of a given method + // or constructor may not correspond exactly, due to compiler-generated artifacts. In particular, + // the number of TypeSignatures that encode formal arguments in MethodTypeSignature may be less + // than the number of ParameterDescriptors in MethodDescriptor." + + // This was also triggered by an implicit param in Guava 28.2 (#660). + + synchronized (this) { + if (parameterInfo == null) { + // Get param type signatures from the type signature of the method + List paramTypeSignatures = null; + final MethodTypeSignature typeSig = getTypeSignature(); + if (typeSig != null) { + paramTypeSignatures = typeSig.getParameterTypeSignatures(); + } + + // If there is no type signature (i.e. if this is not a generic method), fall back to the type + // descriptor (N.B. the type descriptor is basically junk, because the compiler may prepend + // `synthetic` and/or `bridge` parameters automatically, without providing any modifiers for + // the method, so that it is impossible to know how many parameters have been prepended -- + // see #660.) + List paramTypeDescriptors = null; + try { + final MethodTypeSignature typeDesc = getTypeDescriptor(); + if (typeDesc != null) { + paramTypeDescriptors = typeDesc.getParameterTypeSignatures(); } + } catch (final Exception e) { + // Ignore any IllegalArgumentExceptions triggered when type annotations are not able to be + /// aligned with parameters, when there is a `synthetic`, `bridge` or `mandated` parameter + // added to the first parameter position. } - } - int[] paramModifiersAligned = null; - if (parameterModifiers != null && numParams > 0) { - if (parameterModifiers.length == numParams) { - // No alignment necessary - paramModifiersAligned = parameterModifiers; - } else { - // Right-align when not the right length - paramModifiersAligned = new int[numParams]; - for (int i = 0, lenDiff = numParams - - parameterModifiers.length; i < parameterModifiers.length; i++) { - paramModifiersAligned[lenDiff + i] = parameterModifiers[i]; + + // Find the max length of all the parameter information sources + int numParams = paramTypeSignatures == null ? 0 : paramTypeSignatures.size(); + if (paramTypeDescriptors != null && paramTypeDescriptors.size() > numParams) { + numParams = paramTypeDescriptors.size(); + } + if (parameterNames != null && parameterNames.length > numParams) { + numParams = parameterNames.length; + } + if (parameterModifiers != null && parameterModifiers.length > numParams) { + numParams = parameterModifiers.length; + } + if (parameterAnnotationInfo != null && parameterAnnotationInfo.length > numParams) { + numParams = parameterAnnotationInfo.length; + } + + // "Right-align" all parameter info, i.e. assume that any automatically-added implicit parameters + // were added at the beginning of the parameter list, not the end. + + String[] paramNamesAligned = null; + if (parameterNames != null && parameterNames.length > 0) { + if (parameterNames.length == numParams) { + // No alignment necessary + paramNamesAligned = parameterNames; + } else { + // Right-align when not the right length + paramNamesAligned = new String[numParams]; + for (int i = 0, + lenDiff = numParams - parameterNames.length; i < parameterNames.length; i++) { + paramNamesAligned[lenDiff + i] = parameterNames[i]; + } } } - } - AnnotationInfo[][] paramAnnotationInfoAligned = null; - if (parameterAnnotationInfo != null && numParams > 0) { - if (parameterAnnotationInfo.length == numParams) { - // No alignment necessary - paramAnnotationInfoAligned = parameterAnnotationInfo; - } else { - // Right-align when not the right length - paramAnnotationInfoAligned = new AnnotationInfo[numParams][]; - for (int i = 0, lenDiff = numParams - - parameterAnnotationInfo.length; i < parameterAnnotationInfo.length; i++) { - paramAnnotationInfoAligned[lenDiff + i] = parameterAnnotationInfo[i]; + int[] paramModifiersAligned = null; + if (parameterModifiers != null && parameterModifiers.length > 0) { + if (parameterModifiers.length == numParams) { + // No alignment necessary + paramModifiersAligned = parameterModifiers; + } else { + // Right-align when not the right length + paramModifiersAligned = new int[numParams]; + for (int i = 0, lenDiff = numParams + - parameterModifiers.length; i < parameterModifiers.length; i++) { + paramModifiersAligned[lenDiff + i] = parameterModifiers[i]; + } } } - } - List paramTypeSignaturesAligned = null; - if (paramTypeSignatures != null && numParams > 0) { - if (paramTypeSignatures.size() == paramTypeDescriptors.size()) { - // No alignment necessary - paramTypeSignaturesAligned = paramTypeSignatures; - } else { - // Right-align when not the right length - paramTypeSignaturesAligned = new ArrayList<>(numParams); - for (int i = 0, n = numParams - paramTypeSignatures.size(); i < n; i++) { - // Left-pad with nulls - paramTypeSignaturesAligned.add(null); + AnnotationInfo[][] paramAnnotationInfoAligned = null; + if (parameterAnnotationInfo != null && parameterAnnotationInfo.length > 0) { + if (parameterAnnotationInfo.length == numParams) { + // No alignment necessary + paramAnnotationInfoAligned = parameterAnnotationInfo; + } else { + // Right-align when not the right length + paramAnnotationInfoAligned = new AnnotationInfo[numParams][]; + for (int i = 0, lenDiff = numParams + - parameterAnnotationInfo.length; i < parameterAnnotationInfo.length; i++) { + paramAnnotationInfoAligned[lenDiff + i] = parameterAnnotationInfo[i]; + } + } + } + List paramTypeSignaturesAligned = null; + if (paramTypeSignatures != null && paramTypeSignatures.size() > 0) { + if (paramTypeSignatures.size() == numParams) { + // No alignment necessary + paramTypeSignaturesAligned = paramTypeSignatures; + } else { + // Right-align when not the right length + paramTypeSignaturesAligned = new ArrayList<>(numParams); + for (int i = 0, lenDiff = numParams - paramTypeSignatures.size(); i < lenDiff; i++) { + // Left-pad with nulls + paramTypeSignaturesAligned.add(null); + } + paramTypeSignaturesAligned.addAll(paramTypeSignatures); + } + } + List paramTypeDescriptorsAligned = null; + if (paramTypeDescriptors != null && paramTypeDescriptors.size() > 0) { + if (paramTypeDescriptors.size() == numParams) { + // No alignment necessary + paramTypeDescriptorsAligned = paramTypeDescriptors; + } else { + // Right-align when not the right length + paramTypeDescriptorsAligned = new ArrayList<>(numParams); + for (int i = 0, lenDiff = numParams - paramTypeDescriptors.size(); i < lenDiff; i++) { + // Left-pad with nulls + paramTypeDescriptorsAligned.add(null); + } + paramTypeDescriptorsAligned.addAll(paramTypeDescriptors); } - paramTypeSignaturesAligned.addAll(paramTypeSignatures); } - } - // Generate MethodParameterInfo entries - parameterInfo = new MethodParameterInfo[numParams]; - for (int i = 0; i < numParams; i++) { - parameterInfo[i] = new MethodParameterInfo(this, - paramAnnotationInfoAligned == null ? null : paramAnnotationInfoAligned[i], - paramModifiersAligned == null ? 0 : paramModifiersAligned[i], paramTypeDescriptors.get(i), - paramTypeSignaturesAligned == null ? null : paramTypeSignaturesAligned.get(i), - paramNamesAligned == null ? null : paramNamesAligned[i]); - parameterInfo[i].setScanResult(scanResult); + // Generate MethodParameterInfo entries + parameterInfo = new MethodParameterInfo[numParams]; + for (int i = 0; i < numParams; i++) { + parameterInfo[i] = new MethodParameterInfo(this, + paramAnnotationInfoAligned == null ? null : paramAnnotationInfoAligned[i], + paramModifiersAligned == null ? 0 : paramModifiersAligned[i], + paramTypeDescriptorsAligned == null ? null : paramTypeDescriptorsAligned.get(i), + paramTypeSignaturesAligned == null ? null : paramTypeSignaturesAligned.get(i), + paramNamesAligned == null ? null : paramNamesAligned[i]); + parameterInfo[i].setScanResult(scanResult); + } } + return parameterInfo; } - return parameterInfo; } // ------------------------------------------------------------------------------------------------------------- /** - * Get a list of annotations on this method, along with any annotation parameter values. - * - * @return a list of annotations on this method, along with any annotation parameter values, wrapped in - * {@link AnnotationInfo} objects, or the empty list if none. - */ - public AnnotationInfoList getAnnotationInfo() { - if (!scanResult.scanSpec.enableAnnotationInfo) { - throw new IllegalArgumentException("Please call ClassGraph#enableAnnotationInfo() before #scan()"); - } - return annotationInfo == null ? AnnotationInfoList.EMPTY_LIST - : AnnotationInfoList.getIndirectAnnotations(annotationInfo, /* annotatedClass = */ null); - } - - /** - * Get a the named non-{@link Repeatable} annotation on this method, or null if the method does not have the - * named annotation. (Use {@link #getAnnotationInfoRepeatable(String)} for {@link Repeatable} annotations.) - * - * @param annotationName - * The annotation name. - * @return An {@link AnnotationInfo} object representing the named annotation on this method, or null if the - * method does not have the named annotation. - */ - public AnnotationInfo getAnnotationInfo(final String annotationName) { - return getAnnotationInfo().get(annotationName); - } - - /** - * Get a the named {@link Repeatable} annotation on this method, or the empty list if the method does not have - * the named annotation. - * - * @param annotationName - * The annotation name. - * @return An {@link AnnotationInfoList} containing all instances of the named annotation on this method, or the - * empty list if the method does not have the named annotation. - */ - public AnnotationInfoList getAnnotationInfoRepeatable(final String annotationName) { - return getAnnotationInfo().getRepeatable(annotationName); - } - - /** - * Check if this method has the named annotation. + * Check if this method has a parameter with the annotation. * - * @param annotationName - * The name of an annotation. - * @return true if this method has the named annotation. + * @param annotation + * The method parameter annotation. + * @return true if this method has a parameter with the annotation. */ - public boolean hasAnnotation(final String annotationName) { - return getAnnotationInfo().containsName(annotationName); + public boolean hasParameterAnnotation(final Class annotation) { + Assert.isAnnotation(annotation); + return hasParameterAnnotation(annotation.getName()); } /** @@ -525,27 +625,104 @@ public boolean hasParameterAnnotation(final String annotationName) { // ------------------------------------------------------------------------------------------------------------- /** - * Load the class this method is associated with, and get the {@link Method} reference for this method. + * Load and return the classes of each of the method parameters. * - * @return The {@link Method} reference for this field. - * @throws IllegalArgumentException - * if the method does not exist. + * @return An array of the {@link Class} references for each method parameter. */ - public Method loadClassAndGetMethod() throws IllegalArgumentException { + private Class[] loadParameterClasses() { final MethodParameterInfo[] allParameterInfo = getParameterInfo(); final List> parameterClasses = new ArrayList<>(allParameterInfo.length); for (final MethodParameterInfo mpi : allParameterInfo) { final TypeSignature parameterType = mpi.getTypeSignatureOrTypeDescriptor(); - parameterClasses.add(parameterType.loadClass()); + TypeSignature actualParameterType; + if (parameterType instanceof TypeVariableSignature) { + final TypeVariableSignature tvs = (TypeVariableSignature) parameterType; + final TypeParameter t = tvs.resolve(); + if (t.classBound != null) { + // Use class bound of type variable as concrete type, if available, + // in preference to using first interface bound (ignores interface + // bound(s), if present) + actualParameterType = t.classBound; + } else if (t.interfaceBounds != null && !t.interfaceBounds.isEmpty()) { + // Use first interface bound of type variable as concrete type + // (ignores 2nd and subsequent interface bound(s), if present) + actualParameterType = t.interfaceBounds.get(0); + } else { + // Sanity check, should not happen + throw new IllegalArgumentException("TypeVariableSignature has no bounds"); + } + } else { + actualParameterType = parameterType; + } + parameterClasses.add(actualParameterType.loadClass()); } - final Class[] parameterClassesArr = parameterClasses.toArray(new Class[0]); + return parameterClasses.toArray(new Class[0]); + } + + /** + * Load the class this method is associated with, and get the {@link Method} reference for this method. Only + * call this if {@link #isConstructor()} returns false, otherwise an {@link IllegalArgumentException} will be + * thrown. Instead call {@link #loadClassAndGetConstructor()} for constructors. + * + * @return The {@link Method} reference for this method. + * @throws IllegalArgumentException + *

    + *
  • If the method's class can't be loaded
  • + *
  • If the method does not exist
  • + *
  • If the method is a constructor
  • + *
  • If one of the method's parameters references an unknown class
  • + *
  • If the method's return type references an unknown class
  • + *
+ */ + public Method loadClassAndGetMethod() throws IllegalArgumentException { + if (isConstructor()) { + throw new IllegalArgumentException( + "Need to call loadClassAndGetConstructor() for constructors, not loadClassAndGetMethod()"); + } + final Class[] parameterClassesArr = loadParameterClasses(); try { return loadClass().getMethod(getName(), parameterClassesArr); } catch (final NoSuchMethodException e1) { try { return loadClass().getDeclaredMethod(getName(), parameterClassesArr); - } catch (final NoSuchMethodException es2) { - throw new IllegalArgumentException("No such method: " + getClassName() + "." + getName()); + } catch (final NoSuchMethodException e2) { + throw new IllegalArgumentException("Method not found: " + getClassName() + "." + getName()); + } + } catch (final NoClassDefFoundError e3) { + // The method returns an unknown class + throw new IllegalArgumentException("Could not load method: " + getClassName() + "." + getName(), e3); + } + } + + /** + * Load the class this constructor is associated with, and get the {@link Constructor} reference for this + * constructor. Only call this if {@link #isConstructor()} returns true, otherwise an + * {@link IllegalArgumentException} will be thrown. Instead call {@link #loadClassAndGetMethod()} for non-method + * constructors. + * + * @return The {@link Constructor} reference for this constructor. + * @throws IllegalArgumentException + *
    + *
  • If the method's class can't be loaded
  • + *
  • If the constructor does not exist
  • + *
  • If the method is not a constructor
  • + *
  • If one of the constructor's parameters references an unknown class
  • + *
+ */ + public Constructor loadClassAndGetConstructor() throws IllegalArgumentException { + if (!isConstructor()) { + throw new IllegalArgumentException( + "Need to call loadClassAndGetMethod() for non-constructor methods, not " + + "loadClassAndGetConstructor()"); + } + final Class[] parameterClassesArr = loadParameterClasses(); + try { + return loadClass().getConstructor(parameterClassesArr); + } catch (final NoSuchMethodException e1) { + try { + return loadClass().getDeclaredConstructor(parameterClassesArr); + } catch (final NoSuchMethodException e2) { + throw new IllegalArgumentException("Constructor not found for class " + getClassName()); } } } @@ -561,7 +738,8 @@ public Method loadClassAndGetMethod() throws IllegalArgumentException { void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames) { if (annotationInfo != null) { annotationInfo.handleRepeatableAnnotations(allRepeatableAnnotationNames, getClassInfo(), - RelType.METHOD_ANNOTATIONS, RelType.CLASSES_WITH_METHOD_ANNOTATION); + RelType.METHOD_ANNOTATIONS, RelType.CLASSES_WITH_METHOD_ANNOTATION, + RelType.CLASSES_WITH_NONPRIVATE_METHOD_ANNOTATION); } if (parameterAnnotationInfo != null) { for (int i = 0; i < parameterAnnotationInfo.length; i++) { @@ -576,13 +754,11 @@ void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames) } if (hasRepeatableAnnotation) { final AnnotationInfoList aiList = new AnnotationInfoList(pai.length); - for (final AnnotationInfo ai : pai) { - aiList.add(ai); - } - // There is currently no RelType.CLASSES_WITH_METHOD_PARAMETER_ANNOTATION, so set - // RelType values to null - aiList.handleRepeatableAnnotations(allRepeatableAnnotationNames, getClassInfo(), null, - null); + aiList.addAll(Arrays.asList(pai)); + aiList.handleRepeatableAnnotations(allRepeatableAnnotationNames, getClassInfo(), + RelType.METHOD_PARAMETER_ANNOTATIONS, + RelType.CLASSES_WITH_METHOD_PARAMETER_ANNOTATION, + RelType.CLASSES_WITH_NONPRIVATE_METHOD_PARAMETER_ANNOTATION); parameterAnnotationInfo[i] = aiList.toArray(new AnnotationInfo[0]); } } @@ -592,17 +768,6 @@ void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames) // ------------------------------------------------------------------------------------------------------------- - /** - * Returns the declaring class name, so that super.getClassInfo() returns the {@link ClassInfo} object for the - * declaring class. - * - * @return the class name - */ - @Override - protected String getClassName() { - return declaringClassName; - } - /* (non-Javadoc) * @see io.github.classgraph.ScanResultObject#setScanResult(io.github.classgraph.ScanResult) */ @@ -634,34 +799,66 @@ void setScanResult(final ScanResult scanResult) { mpi.setScanResult(scanResult); } } + if (this.thrownExceptions != null) { + for (final ClassInfo thrownException : thrownExceptions) { + if (thrownException.scanResult == null) { // Prevent infinite loop + thrownException.setScanResult(scanResult); + } + } + } } /** - * Get the names of any classes in the type descriptor or type signature. + * Get {@link ClassInfo} objects for any classes referenced in the type descriptor or type signature. * - * @param classNames - * the class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info */ @Override - protected void findReferencedClassNames(final Set classNames) { - final MethodTypeSignature methodSig = getTypeSignature(); - if (methodSig != null) { - methodSig.findReferencedClassNames(classNames); + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + try { + final MethodTypeSignature methodSig = getTypeSignature(); + if (methodSig != null) { + methodSig.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + } + } catch (final IllegalArgumentException e) { + if (log != null) { + log.log("Illegal type signature for method " + getClassName() + "." + getName() + ": " + + getTypeSignatureStr()); + } } - final MethodTypeSignature methodDesc = getTypeDescriptor(); - if (methodDesc != null) { - methodDesc.findReferencedClassNames(classNames); + try { + final MethodTypeSignature methodDesc = getTypeDescriptor(); + if (methodDesc != null) { + methodDesc.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + } + } catch (final IllegalArgumentException e) { + if (log != null) { + log.log("Illegal type descriptor for method " + getClassName() + "." + getName() + ": " + + getTypeDescriptorStr()); + } } if (annotationInfo != null) { for (final AnnotationInfo ai : annotationInfo) { - ai.findReferencedClassNames(classNames); + ai.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } } for (final MethodParameterInfo mpi : getParameterInfo()) { final AnnotationInfo[] aiArr = mpi.annotationInfo; if (aiArr != null) { for (final AnnotationInfo ai : aiArr) { - ai.findReferencedClassNames(classNames); + ai.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + } + } + } + if (thrownExceptionNames != null) { + final ClassInfoList thrownExceptions = getThrownExceptions(); + if (thrownExceptions != null) { + for (int i = 0; i < thrownExceptions.size(); i++) { + classNameToClassInfo.put(thrownExceptionNames[i], thrownExceptions.get(i)); } } } @@ -678,13 +875,9 @@ protected void findReferencedClassNames(final Set classNames) { */ @Override public boolean equals(final Object obj) { - if (this == obj) { + if (obj == this) { return true; - } - if (obj == null) { - return false; - } - if (this.getClass() != obj.getClass()) { + } else if (!(obj instanceof MethodInfo)) { return false; } final MethodInfo other = (MethodInfo) obj; @@ -728,20 +921,21 @@ public int compareTo(final MethodInfo other) { * Get a string representation of the method. Note that constructors are named {@code ""}, and private * static class initializer blocks are named {@code ""}. * - * @return the string representation of the method. + * @param useSimpleNames + * the use simple names + * @param buf + * the buf */ @Override - public String toString() { + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { final MethodTypeSignature methodType = getTypeSignatureOrTypeDescriptor(); - final StringBuilder buf = new StringBuilder(); - if (annotationInfo != null) { for (final AnnotationInfo annotation : annotationInfo) { if (buf.length() > 0) { buf.append(' '); } - annotation.toString(buf); + annotation.toString(useSimpleNames, buf); } } @@ -762,8 +956,7 @@ public String toString() { if (i > 0) { buf.append(", "); } - final String typeParamStr = typeParameters.get(i).toString(); - buf.append(typeParamStr); + typeParameters.get(i).toString(useSimpleNames, buf); } buf.append('>'); } @@ -772,12 +965,15 @@ public String toString() { if (buf.length() > 0) { buf.append(' '); } - buf.append(methodType.getResultType().toString()); + methodType.getResultType().toStringInternal(useSimpleNames, /* annotationsToExclude = */ annotationInfo, + buf); } - buf.append(' '); + if (buf.length() > 0) { + buf.append(' '); + } if (name != null) { - buf.append(name); + buf.append(useSimpleNames ? ClassInfo.getSimpleName(name) : name); } // If at least one param is named, then use placeholder names for unnamed params, @@ -817,51 +1013,79 @@ public String toString() { if (paramInfo.annotationInfo != null) { for (final AnnotationInfo ai : paramInfo.annotationInfo) { - ai.toString(buf); + ai.toString(useSimpleNames, buf); buf.append(' '); } } MethodParameterInfo.modifiersToString(paramInfo.getModifiers(), buf); - final TypeSignature paramType = paramInfo.getTypeSignatureOrTypeDescriptor(); - if (i == varArgsParamIndex) { - // Show varargs params correctly -- replace last "[]" with "..." - if (!(paramType instanceof ArrayTypeSignature)) { - throw new IllegalArgumentException( - "Got non-array type for last parameter of varargs method " + name); - } - final ArrayTypeSignature arrayType = (ArrayTypeSignature) paramType; - if (arrayType.getNumDimensions() == 0) { - throw new IllegalArgumentException( - "Got a zero-dimension array type for last parameter of varargs method " + name); + final TypeSignature paramTypeSignature = paramInfo.getTypeSignatureOrTypeDescriptor(); + // Param type signature may be null in the case of a `synthetic`, `bridge`, or `mandated` parameter + // implicitly added to a non-generic method + if (paramTypeSignature != null) { + if (i == varArgsParamIndex) { + // Show varargs params correctly -- replace last "[]" with "..." + if (!(paramTypeSignature instanceof ArrayTypeSignature)) { + throw new IllegalArgumentException( + "Got non-array type for last parameter of varargs method " + name); + } + final ArrayTypeSignature arrayType = (ArrayTypeSignature) paramTypeSignature; + if (arrayType.getNumDimensions() == 0) { + throw new IllegalArgumentException( + "Got a zero-dimension array type for last parameter of varargs method " + name); + } + arrayType.getElementTypeSignature().toString(useSimpleNames, buf); + for (int j = 0; j < arrayType.getNumDimensions() - 1; j++) { + buf.append("[]"); + } + buf.append("..."); + } else { + // Exclude parameter annotations from type annotations at toplevel of type signature, + // so that annotation is not listed twice + final AnnotationInfoList annotationsToExclude; + if (paramInfo.annotationInfo == null || paramInfo.annotationInfo.length == 0) { + annotationsToExclude = null; + } else { + annotationsToExclude = new AnnotationInfoList(paramInfo.annotationInfo.length); + annotationsToExclude.addAll(Arrays.asList(paramInfo.annotationInfo)); + } + paramTypeSignature.toStringInternal(useSimpleNames, annotationsToExclude, buf); } - buf.append(new ArrayTypeSignature(arrayType.getElementTypeSignature(), - arrayType.getNumDimensions() - 1).toString()); - buf.append("..."); - } else { - buf.append(paramType.toString()); } if (hasParamNames) { final String paramName = paramInfo.getName(); if (paramName != null) { - buf.append(' '); + if (buf.charAt(buf.length() - 1) != ' ') { + buf.append(' '); + } buf.append(paramName); } } } buf.append(')'); + // when throws signature is present, it includes both generic type variables and class names if (!methodType.getThrowsSignatures().isEmpty()) { buf.append(" throws "); for (int i = 0; i < methodType.getThrowsSignatures().size(); i++) { if (i > 0) { buf.append(", "); } - buf.append(methodType.getThrowsSignatures().get(i).toString()); + methodType.getThrowsSignatures().get(i).toString(useSimpleNames, buf); + } + } else { + if (thrownExceptionNames != null && thrownExceptionNames.length > 0) { + buf.append(" throws "); + for (int i = 0; i < thrownExceptionNames.length; i++) { + if (i > 0) { + buf.append(", "); + } + buf.append(useSimpleNames ? ClassInfo.getSimpleName(thrownExceptionNames[i]) + : thrownExceptionNames[i]); + } } } - return buf.toString(); } } diff --git a/src/main/java/io/github/classgraph/MethodInfoList.java b/src/main/java/io/github/classgraph/MethodInfoList.java index 3fa938887..3eed537d4 100644 --- a/src/main/java/io/github/classgraph/MethodInfoList.java +++ b/src/main/java/io/github/classgraph/MethodInfoList.java @@ -33,98 +33,70 @@ import java.util.Map; import java.util.Set; +import nonapi.io.github.classgraph.utils.LogNode; + /** A list of {@link MethodInfo} objects. */ public class MethodInfoList extends InfoList { + /** serialVersionUID */ + private static final long serialVersionUID = 1L; + + /** An unmodifiable empty {@link MethodInfoList}. */ + static final MethodInfoList EMPTY_LIST = new MethodInfoList(); + static { + EMPTY_LIST.makeUnmodifiable(); + } - /** Construct a list of {@link MethodInfo} objects. */ - MethodInfoList() { + /** + * Return an unmodifiable empty {@link MethodInfoList}. + * + * @return the unmodifiable empty {@link MethodInfoList}. + */ + public static MethodInfoList emptyList() { + return EMPTY_LIST; + } + + /** Construct a new modifiable empty list of {@link MethodInfo} objects. */ + public MethodInfoList() { super(); } /** - * Constructor. + * Construct a new modifiable empty list of {@link MethodInfo} objects, given a size hint. * * @param sizeHint * the size hint */ - MethodInfoList(final int sizeHint) { + public MethodInfoList(final int sizeHint) { super(sizeHint); } /** - * Constructor. + * Construct a new modifiable empty {@link MethodInfoList}, given an initial collection of {@link MethodInfo} + * objects. * * @param methodInfoCollection - * the method info collection + * the collection of {@link MethodInfo} objects. */ - MethodInfoList(final Collection methodInfoCollection) { + public MethodInfoList(final Collection methodInfoCollection) { super(methodInfoCollection); } - /** An unmodifiable empty {@link MethodInfoList}. */ - static final MethodInfoList EMPTY_LIST = new MethodInfoList() { - @Override - public boolean add(final MethodInfo e) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void add(final int index, final MethodInfo element) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean remove(final Object o) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public MethodInfo remove(final int index) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final int index, final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean removeAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean retainAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void clear() { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public MethodInfo set(final int index, final MethodInfo element) { - throw new IllegalArgumentException("List is immutable"); - } - }; - // ------------------------------------------------------------------------------------------------------------- /** - * Find the names of any classes referenced in the methods in this list. + * Get {@link ClassInfo} objects for any classes referenced in the type descriptor or type signature. * - * @param referencedClassNames - * the referenced class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info + * @param log + * the log */ - void findReferencedClassNames(final Set referencedClassNames) { + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { for (final MethodInfo mi : this) { - mi.findReferencedClassNames(referencedClassNames); + mi.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } } @@ -174,13 +146,14 @@ public boolean containsName(final String methodName) { /** * Returns a list of all methods matching a given name. (There may be more than one method with a given name, - * due to overloading.) + * due to overloading, so this returns a {@link MethodInfoList} rather than a single {@link MethodInfo}.) * * @param methodName * The name of a method. - * @return A list of {@link MethodInfo} objects in the list with the given name (there may be more than one - * method with a given name, due to overloading). Returns the empty list if no method had a matching - * name. + * @return A {@link MethodInfoList} of {@link MethodInfo} objects from this list that have the given name (there + * may be more than one method with a given name, due to overloading, so this returns a + * {@link MethodInfoList} rather than a single {@link MethodInfo}). Returns the empty list if no method + * had a matching name. */ public MethodInfoList get(final String methodName) { boolean hasMethodWithName = false; @@ -229,7 +202,7 @@ public MethodInfo getSingleMethod(final String methodName) { return lastFoundMethod; } else { throw new IllegalArgumentException("There are multiple methods named \"" + methodName + "\" in class " - + iterator().next().getName()); + + iterator().next().getClassInfo().getName()); } } diff --git a/src/main/java/io/github/classgraph/MethodParameterInfo.java b/src/main/java/io/github/classgraph/MethodParameterInfo.java index 2608f561a..090e99208 100644 --- a/src/main/java/io/github/classgraph/MethodParameterInfo.java +++ b/src/main/java/io/github/classgraph/MethodParameterInfo.java @@ -28,9 +28,14 @@ */ package io.github.classgraph; +import java.lang.annotation.Annotation; import java.lang.annotation.Repeatable; import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.Collections; +import java.util.Objects; + +import nonapi.io.github.classgraph.utils.Assert; /** * Information on the parameters of a method. @@ -38,7 +43,6 @@ * @author lukehutch */ public class MethodParameterInfo { - /** The containing method. */ private final MethodInfo methodInfo; @@ -177,11 +181,25 @@ public AnnotationInfoList getAnnotationInfo() { } } + /** + * Get a the non-{@link Repeatable} annotation on this method, or null if the method parameter does not have the + * annotation. (Use {@link #getAnnotationInfoRepeatable(Class)} for {@link Repeatable} annotations.) + * + * @param annotation + * The annotation. + * @return An {@link AnnotationInfo} object representing the annotation on this method parameter, or null if the + * method parameter does not have the annotation. + */ + public AnnotationInfo getAnnotationInfo(final Class annotation) { + Assert.isAnnotation(annotation); + return getAnnotationInfo(annotation.getName()); + } + /** * Get a the named non-{@link Repeatable} annotation on this method, or null if the method parameter does not * have the named annotation. (Use {@link #getAnnotationInfoRepeatable(String)} for {@link Repeatable} * annotations.) - * + * * @param annotationName * The annotation name. * @return An {@link AnnotationInfo} object representing the named annotation on this method parameter, or null @@ -191,10 +209,24 @@ public AnnotationInfo getAnnotationInfo(final String annotationName) { return getAnnotationInfo().get(annotationName); } + /** + * Get a the {@link Repeatable} annotation on this method, or the empty list if the method parameter does not + * have the annotation. + * + * @param annotation + * The annotation. + * @return An {@link AnnotationInfoList} containing all instances of the annotation on this method parameter, or + * the empty list if the method parameter does not have the annotation. + */ + public AnnotationInfoList getAnnotationInfoRepeatable(final Class annotation) { + Assert.isAnnotation(annotation); + return getAnnotationInfoRepeatable(annotation.getName()); + } + /** * Get a the named {@link Repeatable} annotation on this method, or the empty list if the method parameter does * not have the named annotation. - * + * * @param annotationName * The annotation name. * @return An {@link AnnotationInfoList} containing all instances of the named annotation on this method @@ -204,6 +236,18 @@ public AnnotationInfoList getAnnotationInfoRepeatable(final String annotationNam return getAnnotationInfo().getRepeatable(annotationName); } + /** + * Check whether this method parameter has the annotation. + * + * @param annotation + * The annotation. + * @return true if this method parameter has the annotation. + */ + public boolean hasAnnotation(final Class annotation) { + Assert.isAnnotation(annotation); + return hasAnnotation(annotation.getName()); + } + /** * Check whether this method parameter has the named annotation. * @@ -238,8 +282,61 @@ protected void setScanResult(final ScanResult scanResult) { } } + /** + * Returns true if this method parameter is final. + * + * @return True if this method parameter is final. + */ + public boolean isFinal() { + return Modifier.isFinal(modifiers); + } + + /** + * Returns true if this method parameter is synthetic. + * + * @return True if this method parameter is synthetic. + */ + public boolean isSynthetic() { + return (modifiers & 0x1000) != 0; + } + + /** + * Returns true if this method parameter is mandated. + * + * @return True if this method parameter is mandated. + */ + public boolean isMandated() { + return (modifiers & 0x8000) != 0; + } + // ------------------------------------------------------------------------------------------------------------- + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof MethodParameterInfo)) { + return false; + } + final MethodParameterInfo other = (MethodParameterInfo) obj; + return Objects.equals(methodInfo, other.methodInfo) + && Objects.deepEquals(annotationInfo, other.annotationInfo) && modifiers == other.modifiers + && Objects.equals(typeDescriptor, other.typeDescriptor) + && Objects.equals(typeSignature, other.typeSignature) && Objects.equals(name, other.name); + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(methodInfo, Arrays.hashCode(annotationInfo), typeDescriptor, typeSignature, name) + + modifiers; + } + /** * Convert modifiers into a string representation, e.g. "public static final". * @@ -260,27 +357,52 @@ static void modifiersToString(final int modifiers, final StringBuilder buf) { } } - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - final StringBuilder buf = new StringBuilder(); + // ------------------------------------------------------------------------------------------------------------- + /** + * Render to string. + * + * @param useSimpleNames + * if true, use just the simple name of each class. + * @param buf + * the buf + */ + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { if (annotationInfo != null) { for (final AnnotationInfo anAnnotationInfo : annotationInfo) { - anAnnotationInfo.toString(buf); + anAnnotationInfo.toString(useSimpleNames, buf); buf.append(' '); } } modifiersToString(modifiers, buf); - buf.append(getTypeSignatureOrTypeDescriptor().toString()); + getTypeSignatureOrTypeDescriptor().toString(useSimpleNames, buf); buf.append(' '); buf.append(name == null ? "_unnamed_param" : name); + } + /** + * Render to string with simple names for classes. + * + * @return the string representation. + */ + public String toStringWithSimpleNames() { + final StringBuilder buf = new StringBuilder(); + toString(/* useSimpleNames = */ true, buf); + return buf.toString(); + } + + /** + * Render to string. + * + * @return the string representation. + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + toString(/* useSimpleNames = */ false, buf); return buf.toString(); } } diff --git a/src/main/java/io/github/classgraph/MethodTypeSignature.java b/src/main/java/io/github/classgraph/MethodTypeSignature.java index 1dfbe9354..9a45ca164 100644 --- a/src/main/java/io/github/classgraph/MethodTypeSignature.java +++ b/src/main/java/io/github/classgraph/MethodTypeSignature.java @@ -30,11 +30,15 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import io.github.classgraph.Classfile.TypePathNode; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.Parser; +import nonapi.io.github.classgraph.utils.LogNode; /** A method type signature (called "MethodSignature" in the classfile documentation). */ public final class MethodTypeSignature extends HierarchicalTypeSignature { @@ -50,6 +54,9 @@ public final class MethodTypeSignature extends HierarchicalTypeSignature { /** The throws type signatures. */ private final List throwsSignatures; + /** Any type annotation(s) on an explicit receiver parameter. */ + private AnnotationInfoList receiverTypeAnnotationInfo; + // ------------------------------------------------------------------------------------------------------------- /** @@ -76,13 +83,12 @@ private MethodTypeSignature(final List typeParameters, final List // ------------------------------------------------------------------------------------------------------------- /** - * Get the type parameters for the method. N.B. this is non-public, since the types have to be aligned with - * other parameter metadata. The type of a parameter can be obtained post-alignment from the parameter's - * {@link MethodParameterInfo} object. + * Get the type parameters for the method, if this is a + * generic method. * - * @return The type parameters for the method. + * @return The type parameters for the method, if any, otherwise null. */ - List getTypeParameters() { + public List getTypeParameters() { return typeParameters; } @@ -115,84 +121,33 @@ public List getThrowsSignatures() { return throwsSignatures; } - // ------------------------------------------------------------------------------------------------------------- + @Override + protected void addTypeAnnotation(final List typePath, final AnnotationInfo annotationInfo) { + // Individual parts of a class' type each have their own addTypeAnnotation methods + throw new IllegalArgumentException( + "Cannot call this method on " + MethodTypeSignature.class.getSimpleName()); + } /** - * Parse a method signature. - * - * @param typeDescriptor - * The type descriptor of the method. - * @param definingClassName - * The name of the defining class (for resolving type variables). - * @return The parsed method type signature. - * @throws ParseException - * If method type signature could not be parsed. + * Add a type annotation for an explicit receiver parameter. + * + * @param annotationInfo + * the receiver type annotation */ - static MethodTypeSignature parse(final String typeDescriptor, final String definingClassName) - throws ParseException { - if (typeDescriptor.equals("")) { - // Special case for instance initialization method signatures in a CONSTANT_NameAndType_info structure: - // https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.4.2 - return new MethodTypeSignature(Collections. emptyList(), - Collections. emptyList(), new BaseTypeSignature("void"), - Collections. emptyList()); + void addRecieverTypeAnnotation(final AnnotationInfo annotationInfo) { + if (receiverTypeAnnotationInfo == null) { + receiverTypeAnnotationInfo = new AnnotationInfoList(1); } - final Parser parser = new Parser(typeDescriptor); - final List typeParameters = TypeParameter.parseList(parser, definingClassName); - parser.expect('('); - final List paramTypes = new ArrayList<>(); - while (parser.peek() != ')') { - if (!parser.hasMore()) { - throw new ParseException(parser, "Ran out of input while parsing method signature"); - } - final TypeSignature paramType = TypeSignature.parse(parser, definingClassName); - if (paramType == null) { - throw new ParseException(parser, "Missing method parameter type signature"); - } - paramTypes.add(paramType); - } - parser.expect(')'); - final TypeSignature resultType = TypeSignature.parse(parser, definingClassName); - if (resultType == null) { - throw new ParseException(parser, "Missing method result type signature"); - } - List throwsSignatures; - if (parser.peek() == '^') { - throwsSignatures = new ArrayList<>(); - while (parser.peek() == '^') { - parser.expect('^'); - final ClassRefTypeSignature classTypeSignature = ClassRefTypeSignature.parse(parser, - definingClassName); - if (classTypeSignature != null) { - throwsSignatures.add(classTypeSignature); - } else { - final TypeVariableSignature typeVariableSignature = TypeVariableSignature.parse(parser, - definingClassName); - if (typeVariableSignature != null) { - throwsSignatures.add(typeVariableSignature); - } else { - throw new ParseException(parser, "Missing type variable signature"); - } - } - } - } else { - throwsSignatures = Collections.emptyList(); - } - if (parser.hasMore()) { - throw new ParseException(parser, "Extra characters at end of type descriptor"); - } - final MethodTypeSignature methodSignature = new MethodTypeSignature(typeParameters, paramTypes, resultType, - throwsSignatures); - // Add back-links from type variable signature to the method signature it is part of, - // and to the enclosing class' type signature - @SuppressWarnings("unchecked") - final List typeVariableSignatures = (List) parser.getState(); - if (typeVariableSignatures != null) { - for (final TypeVariableSignature typeVariableSignature : typeVariableSignatures) { - typeVariableSignature.containingMethodSignature = methodSignature; - } - } - return methodSignature; + receiverTypeAnnotationInfo.add(annotationInfo); + } + + /** + * Get type annotations on the explicit receiver parameter, or null if none. + * + * @return type annotations on the explicit receiver parameter, or null if none. + */ + public AnnotationInfoList getReceiverTypeAnnotationInfo() { + return receiverTypeAnnotationInfo; } // ------------------------------------------------------------------------------------------------------------- @@ -240,29 +195,51 @@ void setScanResult(final ScanResult scanResult) { } } - /* (non-Javadoc) - * @see io.github.classgraph.HierarchicalTypeSignature#findReferencedClassNames(java.util.Set) + /** + * Get the names of any classes referenced in the type signature. + * + * @param refdClassNames + * the referenced class names. */ - @Override - void findReferencedClassNames(final Set classNameListOut) { + protected void findReferencedClassNames(final Set refdClassNames) { for (final TypeParameter typeParameter : typeParameters) { if (typeParameter != null) { - typeParameter.findReferencedClassNames(classNameListOut); + typeParameter.findReferencedClassNames(refdClassNames); } } for (final TypeSignature typeSignature : parameterTypeSignatures) { if (typeSignature != null) { - typeSignature.findReferencedClassNames(classNameListOut); + typeSignature.findReferencedClassNames(refdClassNames); } } - resultType.findReferencedClassNames(classNameListOut); + resultType.findReferencedClassNames(refdClassNames); for (final ClassRefOrTypeVariableSignature typeSignature : throwsSignatures) { if (typeSignature != null) { - typeSignature.findReferencedClassNames(classNameListOut); + typeSignature.findReferencedClassNames(refdClassNames); } } } + /** + * Get {@link ClassInfo} objects for any classes referenced in the type descriptor or type signature. + * + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info + */ + @Override + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + final Set refdClassNames = new HashSet<>(); + findReferencedClassNames(refdClassNames); + for (final String refdClassName : refdClassNames) { + final ClassInfo classInfo = ClassInfo.getOrCreateClassInfo(refdClassName, classNameToClassInfo); + classInfo.scanResult = scanResult; + refdClassInfo.add(classInfo); + } + } + // ------------------------------------------------------------------------------------------------------------- /* (non-Javadoc) @@ -279,7 +256,9 @@ public int hashCode() { */ @Override public boolean equals(final Object obj) { - if (!(obj instanceof MethodTypeSignature)) { + if (obj == this) { + return true; + } else if (!(obj instanceof MethodTypeSignature)) { return false; } final MethodTypeSignature o = (MethodTypeSignature) obj; @@ -288,21 +267,18 @@ public boolean equals(final Object obj) { && o.resultType.equals(this.resultType) && o.throwsSignatures.equals(this.throwsSignatures); } - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - final StringBuilder buf = new StringBuilder(); + // ------------------------------------------------------------------------------------------------------------- + @Override + protected void toStringInternal(final boolean useSimpleNames, final AnnotationInfoList annotationsToExclude, + final StringBuilder buf) { if (!typeParameters.isEmpty()) { buf.append('<'); for (int i = 0; i < typeParameters.size(); i++) { if (i > 0) { buf.append(", "); } - final String typeParamStr = typeParameters.get(i).toString(); - buf.append(typeParamStr); + typeParameters.get(i).toString(useSimpleNames, buf); } buf.append('>'); } @@ -317,7 +293,7 @@ public String toString() { if (i > 0) { buf.append(", "); } - buf.append(parameterTypeSignatures.get(i).toString()); + parameterTypeSignatures.get(i).toString(useSimpleNames, buf); } buf.append(')'); @@ -327,9 +303,88 @@ public String toString() { if (i > 0) { buf.append(", "); } - buf.append(throwsSignatures.get(i).toString()); + throwsSignatures.get(i).toString(useSimpleNames, buf); } } - return buf.toString(); + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Parse a method signature. + * + * @param typeDescriptor + * The type descriptor of the method. + * @param definingClassName + * The name of the defining class (for resolving type variables). + * @return The parsed method type signature. + * @throws ParseException + * If method type signature could not be parsed. + */ + static MethodTypeSignature parse(final String typeDescriptor, final String definingClassName) + throws ParseException { + if (typeDescriptor.equals("")) { + // Special case for instance initialization method signatures in a CONSTANT_NameAndType_info structure: + // https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.4.2 + return new MethodTypeSignature(Collections. emptyList(), + Collections. emptyList(), /* void */ new BaseTypeSignature('V'), + Collections. emptyList()); + } + final Parser parser = new Parser(typeDescriptor); + final List typeParameters = TypeParameter.parseList(parser, definingClassName); + parser.expect('('); + final List paramTypes = new ArrayList<>(); + while (parser.peek() != ')') { + if (!parser.hasMore()) { + throw new ParseException(parser, "Ran out of input while parsing method signature"); + } + final TypeSignature paramType = TypeSignature.parse(parser, definingClassName); + if (paramType == null) { + throw new ParseException(parser, "Missing method parameter type signature"); + } + paramTypes.add(paramType); + } + parser.expect(')'); + final TypeSignature resultType = TypeSignature.parse(parser, definingClassName); + if (resultType == null) { + throw new ParseException(parser, "Missing method result type signature"); + } + List throwsSignatures; + if (parser.peek() == '^') { + throwsSignatures = new ArrayList<>(); + while (parser.peek() == '^') { + parser.expect('^'); + final ClassRefTypeSignature classTypeSignature = ClassRefTypeSignature.parse(parser, + definingClassName); + if (classTypeSignature != null) { + throwsSignatures.add(classTypeSignature); + } else { + final TypeVariableSignature typeVariableSignature = TypeVariableSignature.parse(parser, + definingClassName); + if (typeVariableSignature != null) { + throwsSignatures.add(typeVariableSignature); + } else { + throw new ParseException(parser, "Missing type variable signature"); + } + } + } + } else { + throwsSignatures = Collections.emptyList(); + } + if (parser.hasMore()) { + throw new ParseException(parser, "Extra characters at end of type descriptor"); + } + final MethodTypeSignature methodSignature = new MethodTypeSignature(typeParameters, paramTypes, resultType, + throwsSignatures); + // Add back-links from type variable signature to the method signature it is part of, + // and to the enclosing class' type signature + @SuppressWarnings("unchecked") + final List typeVariableSignatures = (List) parser.getState(); + if (typeVariableSignatures != null) { + for (final TypeVariableSignature typeVariableSignature : typeVariableSignatures) { + typeVariableSignature.containingMethodSignature = methodSignature; + } + } + return methodSignature; } } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/ModuleInfo.java b/src/main/java/io/github/classgraph/ModuleInfo.java index 351fa2562..dfbc5ae22 100644 --- a/src/main/java/io/github/classgraph/ModuleInfo.java +++ b/src/main/java/io/github/classgraph/ModuleInfo.java @@ -28,11 +28,15 @@ */ package io.github.classgraph; +import java.lang.annotation.Annotation; import java.net.URI; -import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; +import nonapi.io.github.classgraph.utils.Assert; +import nonapi.io.github.classgraph.utils.CollectionUtils; + /** Holds metadata about a package encountered during a scan. */ public class ModuleInfo implements Comparable, HasName { /** The name of the module. */ @@ -47,7 +51,13 @@ public class ModuleInfo implements Comparable, HasName { /** The location of the module as a URI. */ private transient URI locationURI; - /** {@link AnnotationInfo} objects for any annotations on the package-info.class file, if present, else null. */ + /** + * Unique {@link AnnotationInfo} objects for any annotations on the module-info.class file, if present, else + * null. + */ + private Set annotationInfoSet; + + /** {@link AnnotationInfo} objects for any annotations on the module-info.class file, if present, else null. */ private AnnotationInfoList annotationInfo; /** {@link PackageInfo} objects for packages found within the class, if any, else null. */ @@ -201,12 +211,20 @@ public PackageInfoList getPackageInfo() { return new PackageInfoList(1); } final PackageInfoList packageInfoList = new PackageInfoList(packageInfoSet); - Collections.sort(packageInfoList); + CollectionUtils.sortIfNotEmpty(packageInfoList); return packageInfoList; } // ------------------------------------------------------------------------------------------------------------- + void setScanResult(final ScanResult scanResult) { + if (annotationInfoSet != null) { + for (final AnnotationInfo ai : annotationInfoSet) { + ai.setScanResult(scanResult); + } + } + } + /** * Add annotations found in a module descriptor classfile. * @@ -216,17 +234,29 @@ public PackageInfoList getPackageInfo() { void addAnnotations(final AnnotationInfoList moduleAnnotations) { // Currently only class annotations are used in the module-info.class file if (moduleAnnotations != null && !moduleAnnotations.isEmpty()) { - if (this.annotationInfo == null) { - this.annotationInfo = new AnnotationInfoList(moduleAnnotations); - } else { - this.annotationInfo.addAll(moduleAnnotations); + if (annotationInfoSet == null) { + annotationInfoSet = new LinkedHashSet<>(); } + annotationInfoSet.addAll(moduleAnnotations); } } + /** + * Get a the annotation on this module, or null if the module does not have the annotation. + * + * @param annotation + * The annotation. + * @return An {@link AnnotationInfo} object representing the annotation on this module, or null if the module + * does not have the annotation. + */ + public AnnotationInfo getAnnotationInfo(final Class annotation) { + Assert.isAnnotation(annotation); + return getAnnotationInfo(annotation.getName()); + } + /** * Get a the named annotation on this module, or null if the module does not have the named annotation. - * + * * @param annotationName * The annotation name. * @return An {@link AnnotationInfo} object representing the named annotation on this module, or null if the @@ -242,12 +272,32 @@ public AnnotationInfo getAnnotationInfo(final String annotationName) { * @return the list of {@link AnnotationInfo} objects for annotations on the {@code package-info.class} file. */ public AnnotationInfoList getAnnotationInfo() { - return annotationInfo == null ? AnnotationInfoList.EMPTY_LIST : annotationInfo; + if (annotationInfo == null) { + if (annotationInfoSet == null) { + annotationInfo = AnnotationInfoList.EMPTY_LIST; + } else { + annotationInfo = new AnnotationInfoList(); + annotationInfo.addAll(annotationInfoSet); + } + } + return annotationInfo; + } + + /** + * Check if this module has the annotation. + * + * @param annotation + * The annotation. + * @return true if this module has the annotation. + */ + public boolean hasAnnotation(final Class annotation) { + Assert.isAnnotation(annotation); + return hasAnnotation(annotation.getName()); } /** * Check if this module has the named annotation. - * + * * @param annotationName * The name of an annotation. * @return true if this module has the named annotation. @@ -287,13 +337,13 @@ public int hashCode() { * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(final Object o) { - if (this == o) { + public boolean equals(final Object obj) { + if (obj == this) { return true; - } else if (!(o instanceof ModuleInfo)) { + } else if (!(obj instanceof ModuleInfo)) { return false; } - return this.compareTo((ModuleInfo) o) == 0; + return this.compareTo((ModuleInfo) obj) == 0; } /* (non-Javadoc) diff --git a/src/main/java/io/github/classgraph/ModuleInfoList.java b/src/main/java/io/github/classgraph/ModuleInfoList.java index 16bd01e81..475ea152b 100644 --- a/src/main/java/io/github/classgraph/ModuleInfoList.java +++ b/src/main/java/io/github/classgraph/ModuleInfoList.java @@ -32,6 +32,8 @@ /** A list of {@link ModuleInfo} objects. */ public class ModuleInfoList extends MappableInfoList { + /** serialVersionUID */ + private static final long serialVersionUID = 1L; /** * Constructor. diff --git a/src/main/java/io/github/classgraph/ModulePathInfo.java b/src/main/java/io/github/classgraph/ModulePathInfo.java index d5d1192f1..9700ae5e9 100644 --- a/src/main/java/io/github/classgraph/ModulePathInfo.java +++ b/src/main/java/io/github/classgraph/ModulePathInfo.java @@ -29,14 +29,15 @@ package io.github.classgraph; import java.io.File; -import java.lang.management.ManagementFactory; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; import nonapi.io.github.classgraph.utils.JarUtils; -import nonapi.io.github.classgraph.utils.Join; +import nonapi.io.github.classgraph.utils.StringUtils; /** * Information on the module path. Note that this will only include module system parameters actually listed in @@ -125,24 +126,48 @@ public class ModulePathInfo { '\0', // --add-opens (only one param per switch) '\0' // --add-reads (only one param per switch) ); - - /** Construct a {@link ModulePathInfo}. */ + + /* Module path info. */ public ModulePathInfo() { - final List commandlineArguments = ManagementFactory.getRuntimeMXBean().getInputArguments(); - for (final String arg : commandlineArguments) { - for (int i = 0; i < fields.size(); i++) { - final String argSwitch = argSwitches.get(i); - if (arg.startsWith(argSwitch)) { - final String argParam = arg.substring(argSwitch.length()); - final Set argField = fields.get(i); - final char sepChar = argPartSeparatorChars.get(i); - if (sepChar == '\0') { - // Only one param per switch - argField.add(argParam); - } else { - // Split arg param into parts - for (final String argPart : JarUtils.smartPathSplit(argParam, sepChar)) { - argField.add(argPart); + } + + /** Set to true once {@link #getRuntimeInfo()} is called. */ + private final AtomicBoolean gotRuntimeInfo = new AtomicBoolean(); + + /** Fill in module info from VM commandline parameters. */ + void getRuntimeInfo(final ReflectionUtils reflectionUtils) { + // Only call this reflective method if ModulePathInfo is specifically requested, to avoid illegal + // access warning on some JREs, e.g. Adopt JDK 11 (#605) + if (!gotRuntimeInfo.getAndSet(true)) { + // Read the raw commandline arguments to get the module path override parameters. + // If the java.management module is not present in the deployed runtime (for JDK 9+), or the runtime + // does not contain the java.lang.management package (e.g. the Android build system, which also does + // not support JPMS currently), then skip trying to read the commandline arguments (#404). + final Class managementFactory = reflectionUtils + .classForNameOrNull("java.lang.management.ManagementFactory"); + final Object runtimeMXBean = managementFactory == null ? null + : reflectionUtils.invokeStaticMethod(/* throwException = */ false, managementFactory, + "getRuntimeMXBean"); + @SuppressWarnings("unchecked") + final List commandlineArguments = runtimeMXBean == null ? null + : (List) reflectionUtils.invokeMethod(/* throwException = */ false, runtimeMXBean, + "getInputArguments"); + if (commandlineArguments != null) { + for (final String arg : commandlineArguments) { + for (int i = 0; i < fields.size(); i++) { + final String argSwitch = argSwitches.get(i); + if (arg.startsWith(argSwitch)) { + final String argParam = arg.substring(argSwitch.length()); + final Set argField = fields.get(i); + final char sepChar = argPartSeparatorChars.get(i); + if (sepChar == '\0') { + // Only one param per switch + argField.add(argParam); + } else { + // Split arg param into parts + argField.addAll(Arrays + .asList(JarUtils.smartPathSplit(argParam, sepChar, /* scanSpec = */ null))); + } } } } @@ -160,14 +185,14 @@ public String toString() { final StringBuilder buf = new StringBuilder(1024); if (!modulePath.isEmpty()) { buf.append("--module-path="); - buf.append(Join.join(File.pathSeparator, modulePath)); + buf.append(StringUtils.join(File.pathSeparator, modulePath)); } if (!addModules.isEmpty()) { if (buf.length() > 0) { buf.append(' '); } buf.append("--add-modules="); - buf.append(Join.join(",", addModules)); + buf.append(StringUtils.join(",", addModules)); } for (final String patchModulesEntry : patchModules) { if (buf.length() > 0) { diff --git a/src/main/java/io/github/classgraph/ModuleReaderProxy.java b/src/main/java/io/github/classgraph/ModuleReaderProxy.java index d4ab1ec06..2ec6ec657 100644 --- a/src/main/java/io/github/classgraph/ModuleReaderProxy.java +++ b/src/main/java/io/github/classgraph/ModuleReaderProxy.java @@ -31,14 +31,14 @@ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.nio.ByteBuffer; import java.util.List; -import nonapi.io.github.classgraph.utils.ReflectionUtils; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; /** A ModuleReader proxy, written using reflection to preserve backwards compatibility with JDK 7 and 8. */ public class ModuleReaderProxy implements Closeable { - /** The module reader. */ private final AutoCloseable moduleReader; @@ -48,14 +48,7 @@ public class ModuleReaderProxy implements Closeable { /** Collector> collectorsToList = Collectors.toList(); */ private static Object collectorsToList; - static { - collectorClass = ReflectionUtils.classForNameOrNull("java.util.stream.Collector"); - final Class collectorsClass = ReflectionUtils.classForNameOrNull("java.util.stream.Collectors"); - if (collectorsClass != null) { - collectorsToList = ReflectionUtils.invokeStaticMethod(collectorsClass, "toList", - /* throwException = */ true); - } - } + private ReflectionUtils reflectionUtils; /** * Constructor. @@ -67,8 +60,17 @@ public class ModuleReaderProxy implements Closeable { */ ModuleReaderProxy(final ModuleRef moduleRef) throws IOException { try { - moduleReader = (AutoCloseable) ReflectionUtils.invokeMethod(moduleRef.getReference(), "open", - /* throwException = */ true); + reflectionUtils = moduleRef.reflectionUtils; + if (collectorClass == null || collectorsToList == null) { + collectorClass = reflectionUtils.classForNameOrNull("java.util.stream.Collector"); + final Class collectorsClass = reflectionUtils.classForNameOrNull("java.util.stream.Collectors"); + if (collectorsClass != null) { + collectorsToList = reflectionUtils.invokeStaticMethod(/* throwException = */ true, + collectorsClass, "toList"); + } + } + moduleReader = (AutoCloseable) reflectionUtils.invokeMethod(/* throwException = */ true, + moduleRef.getReference(), "open"); if (moduleReader == null) { throw new IllegalArgumentException("moduleReference.open() should not return null"); } @@ -105,13 +107,13 @@ public List list() throws SecurityException { if (collectorsToList == null) { throw new IllegalArgumentException("Could not call Collectors.toList()"); } - final Object /* Stream */ resourcesStream = ReflectionUtils.invokeMethod(moduleReader, "list", - /* throwException = */ true); + final Object /* Stream */ resourcesStream = reflectionUtils + .invokeMethod(/* throwException = */ true, moduleReader, "list"); if (resourcesStream == null) { throw new IllegalArgumentException("Could not call moduleReader.list()"); } - final Object resourcesList = ReflectionUtils.invokeMethod(resourcesStream, "collect", collectorClass, - collectorsToList, /* throwException = */ true); + final Object resourcesList = reflectionUtils.invokeMethod(/* throwException = */ true, resourcesStream, + "collect", collectorClass, collectorsToList); if (resourcesList == null) { throw new IllegalArgumentException("Could not call moduleReader.list().collect(Collectors.toList())"); } @@ -125,44 +127,30 @@ public List list() throws SecurityException { * * @param path * The path to the resource to open. - * @param open - * if true, call moduleReader.open(name).get() (returning an InputStream), otherwise call - * moduleReader.read(name).get() (returning a ByteBuffer). + * * @return An {@link InputStream} for the content of the resource. * @throws SecurityException * If the module cannot be accessed. + * @throws IllegalArgumentException + * If the module cannot be accessed. */ - private Object openOrRead(final String path, final boolean open) throws SecurityException { - final String methodName = open ? "open" : "read"; - final Object /* Optional */ optionalInputStream = ReflectionUtils.invokeMethod(moduleReader, - methodName, String.class, path, /* throwException = */ true); + public InputStream open(final String path) throws SecurityException { + final Object /* Optional */ optionalInputStream = reflectionUtils + .invokeMethod(/* throwException = */ true, moduleReader, "open", String.class, path); if (optionalInputStream == null) { - throw new IllegalArgumentException("Got null result from moduleReader." + methodName + "(name)"); + throw new IllegalArgumentException("Got null result from ModuleReader#open for path " + path); } - final Object /* InputStream */ inputStream = ReflectionUtils.invokeMethod(optionalInputStream, "get", - /* throwException = */ true); + final InputStream inputStream = (InputStream) reflectionUtils.invokeMethod(/* throwException = */ true, + optionalInputStream, "get"); if (inputStream == null) { - throw new IllegalArgumentException("Got null result from moduleReader." + methodName + "(name).get()"); + throw new IllegalArgumentException("Got null result from ModuleReader#open(String)#get()"); } return inputStream; } /** - * Use the proxied ModuleReader to open the named resource as an InputStream. - * - * @param path - * The path to the resource to open. - * @return An {@link InputStream} for the content of the resource. - * @throws SecurityException - * If the module cannot be accessed. - */ - public InputStream open(final String path) throws SecurityException { - return (InputStream) openOrRead(path, /* open = */ true); - } - - /** - * Use the proxied ModuleReader to open the named resource as a ByteBuffer. Call release(byteBuffer) when you - * have finished with the ByteBuffer. + * Use the proxied ModuleReader to open the named resource as a ByteBuffer. Call {@link #release(ByteBuffer)} + * when you have finished with the ByteBuffer. * * @param path * The path to the resource to open. @@ -173,7 +161,17 @@ public InputStream open(final String path) throws SecurityException { * if the resource is larger than 2GB, the maximum capacity of a byte buffer. */ public ByteBuffer read(final String path) throws SecurityException, OutOfMemoryError { - return (ByteBuffer) openOrRead(path, /* open = */ false); + final Object /* Optional */ optionalByteBuffer = reflectionUtils + .invokeMethod(/* throwException = */ true, moduleReader, "read", String.class, path); + if (optionalByteBuffer == null) { + throw new IllegalArgumentException("Got null result from ModuleReader#read(String)"); + } + final ByteBuffer byteBuffer = (ByteBuffer) reflectionUtils.invokeMethod(/* throwException = */ true, + optionalByteBuffer, "get"); + if (byteBuffer == null) { + throw new IllegalArgumentException("Got null result from ModuleReader#read(String).get()"); + } + return byteBuffer; } /** @@ -183,7 +181,29 @@ public ByteBuffer read(final String path) throws SecurityException, OutOfMemoryE * The {@link ByteBuffer} to release. */ public void release(final ByteBuffer byteBuffer) { - ReflectionUtils.invokeMethod(moduleReader, "release", ByteBuffer.class, byteBuffer, - /* throwException = */ true); + reflectionUtils.invokeMethod(/* throwException = */ true, moduleReader, "release", ByteBuffer.class, + byteBuffer); + } + + /** + * Use the proxied ModuleReader to find the named resource as a URI. + * + * @param path + * The path to the resource to open. + * @return A {@link URI} for the resource. + * @throws SecurityException + * If the module cannot be accessed. + */ + public URI find(final String path) { + final Object /* Optional */ optionalURI = reflectionUtils.invokeMethod(/* throwException = */ true, + moduleReader, "find", String.class, path); + if (optionalURI == null) { + throw new IllegalArgumentException("Got null result from ModuleReader#find(String)"); + } + final URI uri = (URI) reflectionUtils.invokeMethod(/* throwException = */ true, optionalURI, "get"); + if (uri == null) { + throw new IllegalArgumentException("Got null result from ModuleReader#find(String).get()"); + } + return uri; } -} \ No newline at end of file +} diff --git a/src/main/java/io/github/classgraph/ModuleRef.java b/src/main/java/io/github/classgraph/ModuleRef.java index 784a5e121..97c09ab3f 100644 --- a/src/main/java/io/github/classgraph/ModuleRef.java +++ b/src/main/java/io/github/classgraph/ModuleRef.java @@ -32,11 +32,11 @@ import java.io.IOException; import java.net.URI; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Set; -import nonapi.io.github.classgraph.utils.ReflectionUtils; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.utils.CollectionUtils; /** A ModuleReference proxy, written using reflection to preserve backwards compatibility with JDK 7 and 8. */ public class ModuleRef implements Comparable { @@ -70,6 +70,8 @@ public class ModuleRef implements Comparable { /** The ClassLoader that loads classes in the module. May be null, to represent the bootstrap classloader. */ private final ClassLoader classLoader; + ReflectionUtils reflectionUtils; + /** * Constructor. * @@ -77,8 +79,11 @@ public class ModuleRef implements Comparable { * The module reference, of JPMS type ModuleReference. * @param moduleLayer * The module layer, of JPMS type ModuleLayer + * @param reflectionUtils + * The ReflectionUtils instance. */ - public ModuleRef(final Object moduleReference, final Object moduleLayer) { + public ModuleRef(final Object moduleReference, final Object moduleLayer, + final ReflectionUtils reflectionUtils) { if (moduleReference == null) { throw new IllegalArgumentException("moduleReference cannot be null"); } @@ -87,49 +92,48 @@ public ModuleRef(final Object moduleReference, final Object moduleLayer) { } this.reference = moduleReference; this.layer = moduleLayer; + this.reflectionUtils = reflectionUtils; - this.descriptor = ReflectionUtils.invokeMethod(moduleReference, "descriptor", /* throwException = */ true); + this.descriptor = reflectionUtils.invokeMethod(/* throwException = */ true, moduleReference, "descriptor"); if (this.descriptor == null) { // Should not happen throw new IllegalArgumentException("moduleReference.descriptor() should not return null"); } - final String moduleName = (String) ReflectionUtils.invokeMethod(this.descriptor, "name", - /* throwException = */ true); - this.name = moduleName; + this.name = (String) reflectionUtils.invokeMethod(/* throwException = */ true, this.descriptor, "name"); @SuppressWarnings("unchecked") - final Set modulePackages = (Set) ReflectionUtils.invokeMethod(this.descriptor, "packages", - /* throwException = */ true); + final Set modulePackages = (Set) reflectionUtils.invokeMethod(/* throwException = */ true, + this.descriptor, "packages"); if (modulePackages == null) { // Should not happen throw new IllegalArgumentException("moduleReference.descriptor().packages() should not return null"); } this.packages = new ArrayList<>(modulePackages); - Collections.sort(this.packages); - final Object optionalRawVersion = ReflectionUtils.invokeMethod(this.descriptor, "rawVersion", - /* throwException = */ true); + CollectionUtils.sortIfNotEmpty(this.packages); + final Object optionalRawVersion = reflectionUtils.invokeMethod(/* throwException = */ true, this.descriptor, + "rawVersion"); if (optionalRawVersion != null) { - final Boolean isPresent = (Boolean) ReflectionUtils.invokeMethod(optionalRawVersion, "isPresent", - /* throwException = */ true); + final Boolean isPresent = (Boolean) reflectionUtils.invokeMethod(/* throwException = */ true, + optionalRawVersion, "isPresent"); if (isPresent != null && isPresent) { - this.rawVersion = (String) ReflectionUtils.invokeMethod(optionalRawVersion, "get", - /* throwException = */ true); + this.rawVersion = (String) reflectionUtils.invokeMethod(/* throwException = */ true, + optionalRawVersion, "get"); } } - final Object moduleLocationOptional = ReflectionUtils.invokeMethod(moduleReference, "location", - /* throwException = */ true); + final Object moduleLocationOptional = reflectionUtils.invokeMethod(/* throwException = */ true, + moduleReference, "location"); if (moduleLocationOptional == null) { // Should not happen throw new IllegalArgumentException("moduleReference.location() should not return null"); } - final Object moduleLocationIsPresent = ReflectionUtils.invokeMethod(moduleLocationOptional, "isPresent", - /* throwException = */ true); + final Object moduleLocationIsPresent = reflectionUtils.invokeMethod(/* throwException = */ true, + moduleLocationOptional, "isPresent"); if (moduleLocationIsPresent == null) { // Should not happen throw new IllegalArgumentException("moduleReference.location().isPresent() should not return null"); } if ((Boolean) moduleLocationIsPresent) { - this.location = (URI) ReflectionUtils.invokeMethod(moduleLocationOptional, "get", - /* throwException = */ true); + this.location = (URI) reflectionUtils.invokeMethod(/* throwException = */ true, moduleLocationOptional, + "get"); if (this.location == null) { // Should not happen throw new IllegalArgumentException("moduleReference.location().get() should not return null"); @@ -139,8 +143,8 @@ public ModuleRef(final Object moduleReference, final Object moduleLayer) { } // Find the classloader for the module - this.classLoader = (ClassLoader) ReflectionUtils.invokeMethod(moduleLayer, "findLoader", String.class, - this.name, /* throwException = */ true); + this.classLoader = (ClassLoader) reflectionUtils.invokeMethod(/* throwException = */ true, moduleLayer, + "findLoader", String.class, this.name); } /** @@ -265,11 +269,13 @@ public ClassLoader getClassLoader() { */ @Override public boolean equals(final Object obj) { - if (!(obj instanceof ModuleRef)) { + if (obj == this) { + return true; + } else if (!(obj instanceof ModuleRef)) { return false; } - final ModuleRef mr = (ModuleRef) obj; - return mr.reference.equals(this.reference) && mr.layer.equals(this.layer); + final ModuleRef modRef = (ModuleRef) obj; + return modRef.reference.equals(this.reference) && modRef.layer.equals(this.layer); } /* (non-Javadoc) diff --git a/src/main/java/io/github/classgraph/ObjectTypedValueWrapper.java b/src/main/java/io/github/classgraph/ObjectTypedValueWrapper.java index 644d64c1a..838f9f888 100644 --- a/src/main/java/io/github/classgraph/ObjectTypedValueWrapper.java +++ b/src/main/java/io/github/classgraph/ObjectTypedValueWrapper.java @@ -29,18 +29,24 @@ package io.github.classgraph; import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import nonapi.io.github.classgraph.utils.LogNode; + /** A union type, used for typesafe serialization/deserialization to/from JSON. Only one field is ever set. */ class ObjectTypedValueWrapper extends ScanResultObject { // Parameter value is split into different fields by type, so that serialization and deserialization // works properly (can't properly serialize a field of Object type, since the concrete type is not + // TODO: remove this class once JSON serialization is removed /** Enum value. */ // stored in JSON). - private AnnotationEnumValue enumValue; + private AnnotationEnumValue annotationEnumValue; /** Class ref. */ - private AnnotationClassRef classRef; + private AnnotationClassRef annotationClassRef; /** AnnotationInfo. */ private AnnotationInfo annotationInfo; @@ -109,6 +115,8 @@ public ObjectTypedValueWrapper() { super(); } + // ------------------------------------------------------------------------------------------------------------- + /** * Constructor. * @@ -148,9 +156,9 @@ public ObjectTypedValueWrapper(final Object annotationParamValue) { } } } else if (annotationParamValue instanceof AnnotationEnumValue) { - enumValue = (AnnotationEnumValue) annotationParamValue; + annotationEnumValue = (AnnotationEnumValue) annotationParamValue; } else if (annotationParamValue instanceof AnnotationClassRef) { - classRef = (AnnotationClassRef) annotationParamValue; + annotationClassRef = (AnnotationClassRef) annotationParamValue; } else if (annotationParamValue instanceof AnnotationInfo) { annotationInfo = (AnnotationInfo) annotationParamValue; } else if (annotationParamValue instanceof String) { @@ -191,10 +199,10 @@ public ObjectTypedValueWrapper(final Object annotationParamValue) { */ Object instantiateOrGet(final ClassInfo annotationClassInfo, final String paramName) { final boolean instantiate = annotationClassInfo != null; - if (enumValue != null) { - return instantiate ? enumValue.loadClassAndReturnEnumValue() : enumValue; - } else if (classRef != null) { - return instantiate ? classRef.loadClass() : classRef; + if (annotationEnumValue != null) { + return instantiate ? annotationEnumValue.loadClassAndReturnEnumValue() : annotationEnumValue; + } else if (annotationClassRef != null) { + return instantiate ? annotationClassRef.loadClass() : annotationClassRef; } else if (annotationInfo != null) { return instantiate ? annotationInfo.loadClassAndInstantiate() : annotationInfo; } else if (stringValue != null) { @@ -283,13 +291,14 @@ public Object get() { private Object getArrayValueClassOrName(final ClassInfo annotationClassInfo, final String paramName, final boolean getClass) { // Find the method in the annotation class with the same name as the annotation parameter. - final MethodInfoList annotationMethodList = annotationClassInfo.methodInfo == null ? null - : annotationClassInfo.methodInfo.get(paramName); - if (annotationMethodList != null && annotationMethodList.size() > 1) { - // There should only be one method with a given name in an annotation - throw new IllegalArgumentException("Duplicated annotation parameter method " + paramName - + "() in annotation class " + annotationClassInfo.getName()); - } else if (annotationMethodList != null && annotationMethodList.size() == 1) { + final MethodInfoList annotationMethodList = annotationClassInfo == null + || annotationClassInfo.methodInfo == null ? null : annotationClassInfo.methodInfo.get(paramName); + if (annotationClassInfo != null && annotationMethodList != null && !annotationMethodList.isEmpty()) { + if (annotationMethodList.size() > 1) { + // There should only be one method with a given name in an annotation + throw new IllegalArgumentException("Duplicated annotation parameter method " + paramName + "()" + + " in annotation class " + annotationClassInfo.getName()); + } // Get the result type of the method with the same name as the annotation parameter final TypeSignature annotationMethodResultTypeSig = annotationMethodList.get(0) .getTypeSignatureOrTypeDescriptor().getResultType(); @@ -307,8 +316,7 @@ private Object getArrayValueClassOrName(final ClassInfo annotationClassInfo, fin if (elementTypeSig instanceof ClassRefTypeSignature) { // Look up the name of the element type, for non-primitive arrays final ClassRefTypeSignature classRefTypeSignature = (ClassRefTypeSignature) elementTypeSig; - return getClass ? classRefTypeSignature.loadClass() - : classRefTypeSignature.getFullyQualifiedClassName(); + return getClass ? classRefTypeSignature.loadClass() : classRefTypeSignature.getClassName(); } else if (elementTypeSig instanceof BaseTypeSignature) { // Look up the name of the primitive class, for primitive arrays final BaseTypeSignature baseTypeSignature = (BaseTypeSignature) elementTypeSig; @@ -316,9 +324,10 @@ private Object getArrayValueClassOrName(final ClassInfo annotationClassInfo, fin } } else { // Could not find a method with this name -- this is an external class. - // Find first non-null object in array, and use its type as the type of the array. + // Find first non-null object in array, and use its type as the element type of the array. for (final ObjectTypedValueWrapper elt : objectArrayValue) { if (elt != null) { + // Primitive typed arrays will be turned into arrays of boxed types return elt.integerValue != null ? (getClass ? Integer.class : "int") : elt.longValue != null ? (getClass ? Long.class : "long") : elt.shortValue != null ? (getClass ? Short.class : "short") @@ -331,11 +340,14 @@ private Object getArrayValueClassOrName(final ClassInfo annotationClassInfo, fin : elt.floatValue != null ? (getClass ? Float.class : "float") - : (getClass ? null : ""); + : (getClass ? elt.getClass() + : elt.getClass() + .getName()); } } } - return getClass ? null : ""; + // Could not determine the element type -- just use Object + return getClass ? Object.class : "java.lang.Object"; } /** @@ -384,7 +396,8 @@ void convertWrapperArraysToPrimitiveArrays(final ClassInfo annotationClassInfo, if (elt == null) { throw new IllegalArgumentException("Illegal null value for array of element type " + targetElementTypeName + " in parameter " + paramName + " of annotation class " - + annotationClassInfo.getName()); + + (annotationClassInfo == null ? "" + : annotationClassInfo.getName())); } intArrayValue[j] = objectArrayValue[j].integerValue; } @@ -397,7 +410,8 @@ void convertWrapperArraysToPrimitiveArrays(final ClassInfo annotationClassInfo, if (elt == null) { throw new IllegalArgumentException("Illegal null value for array of element type " + targetElementTypeName + " in parameter " + paramName + " of annotation class " - + annotationClassInfo.getName()); + + (annotationClassInfo == null ? "" + : annotationClassInfo.getName())); } longArrayValue[j] = objectArrayValue[j].longValue; } @@ -410,7 +424,8 @@ void convertWrapperArraysToPrimitiveArrays(final ClassInfo annotationClassInfo, if (elt == null) { throw new IllegalArgumentException("Illegal null value for array of element type " + targetElementTypeName + " in parameter " + paramName + " of annotation class " - + annotationClassInfo.getName()); + + (annotationClassInfo == null ? "" + : annotationClassInfo.getName())); } shortArrayValue[j] = objectArrayValue[j].shortValue; } @@ -423,7 +438,8 @@ void convertWrapperArraysToPrimitiveArrays(final ClassInfo annotationClassInfo, if (elt == null) { throw new IllegalArgumentException("Illegal null value for array of element type " + targetElementTypeName + " in parameter " + paramName + " of annotation class " - + annotationClassInfo.getName()); + + (annotationClassInfo == null ? "" + : annotationClassInfo.getName())); } charArrayValue[j] = objectArrayValue[j].characterValue; } @@ -436,7 +452,8 @@ void convertWrapperArraysToPrimitiveArrays(final ClassInfo annotationClassInfo, if (elt == null) { throw new IllegalArgumentException("Illegal null value for array of element type " + targetElementTypeName + " in parameter " + paramName + " of annotation class " - + annotationClassInfo.getName()); + + (annotationClassInfo == null ? "" + : annotationClassInfo.getName())); } floatArrayValue[j] = objectArrayValue[j].floatValue; } @@ -449,7 +466,8 @@ void convertWrapperArraysToPrimitiveArrays(final ClassInfo annotationClassInfo, if (elt == null) { throw new IllegalArgumentException("Illegal null value for array of element type " + targetElementTypeName + " in parameter " + paramName + " of annotation class " - + annotationClassInfo.getName()); + + (annotationClassInfo == null ? "" + : annotationClassInfo.getName())); } doubleArrayValue[j] = objectArrayValue[j].doubleValue; } @@ -462,7 +480,8 @@ void convertWrapperArraysToPrimitiveArrays(final ClassInfo annotationClassInfo, if (elt == null) { throw new IllegalArgumentException("Illegal null value for array of element type " + targetElementTypeName + " in parameter " + paramName + " of annotation class " - + annotationClassInfo.getName()); + + (annotationClassInfo == null ? "" + : annotationClassInfo.getName())); } booleanArrayValue[j] = objectArrayValue[j].booleanValue; } @@ -475,7 +494,8 @@ void convertWrapperArraysToPrimitiveArrays(final ClassInfo annotationClassInfo, if (elt == null) { throw new IllegalArgumentException("Illegal null value for array of element type " + targetElementTypeName + " in parameter " + paramName + " of annotation class " - + annotationClassInfo.getName()); + + (annotationClassInfo == null ? "" + : annotationClassInfo.getName())); } byteArrayValue[j] = objectArrayValue[j].byteValue; } @@ -513,10 +533,10 @@ protected ClassInfo getClassInfo() { @Override void setScanResult(final ScanResult scanResult) { super.setScanResult(scanResult); - if (enumValue != null) { - enumValue.setScanResult(scanResult); - } else if (classRef != null) { - classRef.setScanResult(scanResult); + if (annotationEnumValue != null) { + annotationEnumValue.setScanResult(scanResult); + } else if (annotationClassRef != null) { + annotationClassRef.setScanResult(scanResult); } else if (annotationInfo != null) { annotationInfo.setScanResult(scanResult); } else if (objectArrayValue != null) { @@ -529,23 +549,123 @@ void setScanResult(final ScanResult scanResult) { } /** - * Get the names of any classes referenced in the annotation parameters. + * Get {@link ClassInfo} objects for any classes referenced in annotation parameters. * - * @param referencedClassNames - * referenced class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info */ @Override - void findReferencedClassNames(final Set referencedClassNames) { - if (enumValue != null) { - enumValue.findReferencedClassNames(referencedClassNames); - } else if (classRef != null) { - referencedClassNames.add(classRef.getClassName()); + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + if (annotationEnumValue != null) { + annotationEnumValue.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); + } else if (annotationClassRef != null) { + final ClassInfo classInfo = annotationClassRef.getClassInfo(); + if (classInfo != null) { + refdClassInfo.add(classInfo); + } } else if (annotationInfo != null) { - annotationInfo.findReferencedClassNames(referencedClassNames); + annotationInfo.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } else if (objectArrayValue != null) { for (final ObjectTypedValueWrapper item : objectArrayValue) { - item.findReferencedClassNames(referencedClassNames); + item.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log); } } } + + // ------------------------------------------------------------------------------------------------------------- + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return Objects.hash(annotationEnumValue, annotationClassRef, annotationInfo, stringValue, integerValue, + longValue, shortValue, booleanValue, characterValue, floatValue, doubleValue, byteValue, + Arrays.hashCode(stringArrayValue), Arrays.hashCode(intArrayValue), Arrays.hashCode(longArrayValue), + Arrays.hashCode(shortArrayValue), Arrays.hashCode(booleanArrayValue), + Arrays.hashCode(charArrayValue), Arrays.hashCode(floatArrayValue), + Arrays.hashCode(doubleArrayValue), Arrays.hashCode(byteArrayValue), + Arrays.hashCode(objectArrayValue)); + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object other) { + if (other == this) { + return true; + } else if (!(other instanceof ObjectTypedValueWrapper)) { + return false; + } + final ObjectTypedValueWrapper o = (ObjectTypedValueWrapper) other; + return Objects.equals(annotationEnumValue, o.annotationEnumValue) + && Objects.equals(annotationClassRef, o.annotationClassRef) + && Objects.equals(annotationInfo, o.annotationInfo) && Objects.equals(stringValue, o.stringValue) + && Objects.equals(integerValue, o.integerValue) && Objects.equals(longValue, o.longValue) + && Objects.equals(shortValue, o.shortValue) && Objects.equals(booleanValue, o.booleanValue) + && Objects.equals(characterValue, o.characterValue) && Objects.equals(floatValue, o.floatValue) + && Objects.equals(doubleValue, o.doubleValue) && Objects.equals(byteValue, o.byteValue) + && Arrays.equals(stringArrayValue, o.stringArrayValue) + && Arrays.equals(intArrayValue, o.intArrayValue) && Arrays.equals(longArrayValue, o.longArrayValue) + && Arrays.equals(shortArrayValue, o.shortArrayValue) + && Arrays.equals(floatArrayValue, o.floatArrayValue) + && Arrays.equals(byteArrayValue, o.byteArrayValue) + && Arrays.deepEquals(objectArrayValue, o.objectArrayValue); + } + + // ------------------------------------------------------------------------------------------------------------- + + @Override + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + if (annotationEnumValue != null) { + annotationEnumValue.toString(useSimpleNames, buf); + } else if (annotationClassRef != null) { + annotationClassRef.toString(useSimpleNames, buf); + } else if (annotationInfo != null) { + annotationInfo.toString(useSimpleNames, buf); + } else if (stringValue != null) { + buf.append(stringValue); + } else if (integerValue != null) { + buf.append(integerValue); + } else if (longValue != null) { + buf.append(longValue); + } else if (shortValue != null) { + buf.append(shortValue); + } else if (booleanValue != null) { + buf.append(booleanValue); + } else if (characterValue != null) { + buf.append(characterValue); + } else if (floatValue != null) { + buf.append(floatValue); + } else if (doubleValue != null) { + buf.append(doubleValue); + } else if (byteValue != null) { + buf.append(byteValue); + } else if (stringArrayValue != null) { + buf.append(Arrays.toString(stringArrayValue)); + } else if (intArrayValue != null) { + buf.append(Arrays.toString(intArrayValue)); + } else if (longArrayValue != null) { + buf.append(Arrays.toString(longArrayValue)); + } else if (shortArrayValue != null) { + buf.append(Arrays.toString(shortArrayValue)); + } else if (booleanArrayValue != null) { + buf.append(Arrays.toString(booleanArrayValue)); + } else if (charArrayValue != null) { + buf.append(Arrays.toString(charArrayValue)); + } else if (floatArrayValue != null) { + buf.append(Arrays.toString(floatArrayValue)); + } else if (doubleArrayValue != null) { + buf.append(Arrays.toString(doubleArrayValue)); + } else if (byteArrayValue != null) { + buf.append(Arrays.toString(byteArrayValue)); + } else if (objectArrayValue != null) { + // TODO this doesn't handle nested arrays, but this toString() method is only used for debugging + buf.append(Arrays.toString(objectArrayValue)); + } + } } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/PackageInfo.java b/src/main/java/io/github/classgraph/PackageInfo.java index dea0ae9a2..f617ed7e6 100644 --- a/src/main/java/io/github/classgraph/PackageInfo.java +++ b/src/main/java/io/github/classgraph/PackageInfo.java @@ -28,18 +28,29 @@ */ package io.github.classgraph; -import java.util.Collections; +import java.lang.annotation.Annotation; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.Assert; +import nonapi.io.github.classgraph.utils.CollectionUtils; + /** Holds metadata about a package encountered during a scan. */ public class PackageInfo implements Comparable, HasName { /** Name of the package. */ private String name; + /** + * Unique {@link AnnotationInfo} objects for any annotations on the package-info.class file, if present, else + * null. + */ + private Set annotationInfoSet; + /** {@link AnnotationInfo} for any annotations on the package-info.class file, if present, else null. */ private AnnotationInfoList annotationInfo; @@ -88,13 +99,12 @@ public String getName() { * the package annotations */ void addAnnotations(final AnnotationInfoList packageAnnotations) { - // Currently only class annotations are used in the package-info.class file + // Add class annotations from the package-info.class file if (packageAnnotations != null && !packageAnnotations.isEmpty()) { - if (this.annotationInfo == null) { - this.annotationInfo = new AnnotationInfoList(packageAnnotations); - } else { - this.annotationInfo.addAll(packageAnnotations); + if (annotationInfoSet == null) { + annotationInfoSet = new LinkedHashSet<>(); } + annotationInfoSet.addAll(packageAnnotations); } } @@ -114,9 +124,30 @@ void addClassInfo(final ClassInfo classInfo) { // ------------------------------------------------------------------------------------------------------------- + void setScanResult(final ScanResult scanResult) { + if (annotationInfoSet != null) { + for (final AnnotationInfo ai : annotationInfoSet) { + ai.setScanResult(scanResult); + } + } + } + /** - * Get a the named annotation on this package, or null if the package does not have the named annotation. + * Get a the annotation on this package, or null if the package does not have the annotation. * + * @param annotation + * The annotation. + * @return An {@link AnnotationInfo} object representing the annotation on this package, or null if the package + * does not have the annotation. + */ + public AnnotationInfo getAnnotationInfo(final Class annotation) { + Assert.isAnnotation(annotation); + return getAnnotationInfo(annotation.getName()); + } + + /** + * Get a the named annotation on this package, or null if the package does not have the named annotation. + * * @param annotationName * The annotation name. * @return An {@link AnnotationInfo} object representing the named annotation on this package, or null if the @@ -132,7 +163,27 @@ public AnnotationInfo getAnnotationInfo(final String annotationName) { * @return the annotations on the {@code package-info.class} file. */ public AnnotationInfoList getAnnotationInfo() { - return annotationInfo == null ? AnnotationInfoList.EMPTY_LIST : annotationInfo; + if (annotationInfo == null) { + if (annotationInfoSet == null) { + annotationInfo = AnnotationInfoList.EMPTY_LIST; + } else { + annotationInfo = new AnnotationInfoList(); + annotationInfo.addAll(annotationInfoSet); + } + } + return annotationInfo; + } + + /** + * Check if the package has the annotation. + * + * @param annotation + * The annotation. + * @return true if this package has the annotation. + */ + public boolean hasAnnotation(final Class annotation) { + Assert.isAnnotation(annotation); + return hasAnnotation(annotation.getName()); } /** @@ -168,7 +219,7 @@ public PackageInfoList getChildren() { } final PackageInfoList childrenSorted = new PackageInfoList(children); // Ensure children are sorted - Collections.sort(childrenSorted, new Comparator() { + CollectionUtils.sortIfNotEmpty(childrenSorted, new Comparator() { @Override public int compare(final PackageInfo o1, final PackageInfo o2) { return o1.name.compareTo(o2.name); @@ -189,7 +240,7 @@ public int compare(final PackageInfo o1, final PackageInfo o2) { * in this package. */ public ClassInfo getClassInfo(final String className) { - return memberClassNameToClassInfo.get(className); + return memberClassNameToClassInfo == null ? null : memberClassNameToClassInfo.get(className); } /** @@ -198,7 +249,8 @@ public ClassInfo getClassInfo(final String className) { * @return the {@link ClassInfo} objects for all classes that are members of this package. */ public ClassInfoList getClassInfo() { - return new ClassInfoList(new HashSet<>(memberClassNameToClassInfo.values()), /* sortByName = */ true); + return memberClassNameToClassInfo == null ? ClassInfoList.EMPTY_LIST + : new ClassInfoList(new HashSet<>(memberClassNameToClassInfo.values()), /* sortByName = */ true); } /** @@ -208,7 +260,9 @@ public ClassInfoList getClassInfo() { * the reachable class info */ private void obtainClassInfoRecursive(final Set reachableClassInfo) { - reachableClassInfo.addAll(memberClassNameToClassInfo.values()); + if (memberClassNameToClassInfo != null) { + reachableClassInfo.addAll(memberClassNameToClassInfo.values()); + } for (final PackageInfo subPackageInfo : getChildren()) { subPackageInfo.obtainClassInfoRecursive(reachableClassInfo); } @@ -252,10 +306,12 @@ static String getParentPackageName(final String packageOrClassName) { * the package name * @param packageNameToPackageInfo * a map from package name to package info + * @param scanSpec + * the ScanSpec. * @return the {@link PackageInfo} for the named package. */ static PackageInfo getOrCreatePackage(final String packageName, - final Map packageNameToPackageInfo) { + final Map packageNameToPackageInfo, final ScanSpec scanSpec) { // Get or create PackageInfo object for this package PackageInfo packageInfo = packageNameToPackageInfo.get(packageName); if (packageInfo != null) { @@ -269,16 +325,20 @@ static PackageInfo getOrCreatePackage(final String packageName, // If this is not the root package ("") if (!packageName.isEmpty()) { // Recursively create PackageInfo objects for parent packages (until a parent package that already - // exists is reached), and connect each ancestral package to its parent - final PackageInfo parentPackageInfo = getOrCreatePackage(getParentPackageName(packageInfo.name), - packageNameToPackageInfo); - if (parentPackageInfo != null) { - // Link package to parent - if (parentPackageInfo.children == null) { - parentPackageInfo.children = new HashSet<>(); + // exists or that is not accepted is reached), and connect each ancestral package to its parent + final String parentPackageName = getParentPackageName(packageInfo.name); + if (scanSpec.packageAcceptReject.isAcceptedAndNotRejected(parentPackageName) + || scanSpec.packagePrefixAcceptReject.isAcceptedAndNotRejected(parentPackageName)) { + final PackageInfo parentPackageInfo = getOrCreatePackage(parentPackageName, + packageNameToPackageInfo, scanSpec); + if (parentPackageInfo != null) { + // Link package to parent + if (parentPackageInfo.children == null) { + parentPackageInfo.children = new HashSet<>(); + } + parentPackageInfo.children.add(packageInfo); + packageInfo.parent = parentPackageInfo; } - parentPackageInfo.children.add(packageInfo); - packageInfo.parent = parentPackageInfo; } } @@ -308,13 +368,13 @@ public int hashCode() { * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(final Object o) { - if (this == o) { + public boolean equals(final Object obj) { + if (obj == this) { return true; - } else if (!(o instanceof PackageInfo)) { + } else if (!(obj instanceof PackageInfo)) { return false; } - return this.name.equals(((PackageInfo) o).name); + return this.name.equals(((PackageInfo) obj).name); } /* (non-Javadoc) diff --git a/src/main/java/io/github/classgraph/PackageInfoList.java b/src/main/java/io/github/classgraph/PackageInfoList.java index 1fdb6ff4e..f5811270c 100644 --- a/src/main/java/io/github/classgraph/PackageInfoList.java +++ b/src/main/java/io/github/classgraph/PackageInfoList.java @@ -32,6 +32,8 @@ /** A list of {@link PackageInfo} objects. */ public class PackageInfoList extends MappableInfoList { + /** serialVersionUID */ + private static final long serialVersionUID = 1L; /** * Constructor. @@ -62,6 +64,9 @@ public class PackageInfoList extends MappableInfoList { /** An unmodifiable {@link PackageInfoList}. */ static final PackageInfoList EMPTY_LIST = new PackageInfoList() { + /** serialVersionUID */ + private static final long serialVersionUID = 1L; + @Override public boolean add(final PackageInfo e) { throw new IllegalArgumentException("List is immutable"); diff --git a/src/main/java/io/github/classgraph/PotentiallyUnmodifiableList.java b/src/main/java/io/github/classgraph/PotentiallyUnmodifiableList.java new file mode 100644 index 000000000..d8b70a66e --- /dev/null +++ b/src/main/java/io/github/classgraph/PotentiallyUnmodifiableList.java @@ -0,0 +1,294 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.ListIterator; + +/** + * A potentially unmodifiable list of objects. + * + * @param + * the element type + */ +class PotentiallyUnmodifiableList extends ArrayList { + /** serialVersionUID. */ + static final long serialVersionUID = 1L; + + /** Whether or not the list is modifiable. */ + boolean modifiable = true; + + /** + * Constructor. + */ + PotentiallyUnmodifiableList() { + super(); + } + + /** + * Constructor. + * + * @param sizeHint + * the size hint + */ + PotentiallyUnmodifiableList(final int sizeHint) { + super(sizeHint); + } + + /** + * Constructor. + * + * @param collection + * the initial elements. + */ + PotentiallyUnmodifiableList(final Collection collection) { + super(collection); + } + + // Keep Scrutinizer happy + @Override + public boolean equals(final Object o) { + return super.equals(o); + } + + // Keep Scrutinizer happy + @Override + public int hashCode() { + return super.hashCode(); + } + + /** Make this list unmodifiable. */ + void makeUnmodifiable() { + modifiable = false; + } + + @Override + public boolean add(final T element) { + if (!modifiable) { + throw new IllegalArgumentException("List is immutable"); + } else { + return super.add(element); + } + } + + @Override + public void add(final int index, final T element) { + if (!modifiable) { + throw new IllegalArgumentException("List is immutable"); + } else { + super.add(index, element); + } + } + + @Override + public boolean remove(final Object o) { + if (!modifiable) { + throw new IllegalArgumentException("List is immutable"); + } else { + return super.remove(o); + } + } + + @Override + public T remove(final int index) { + if (!modifiable) { + throw new IllegalArgumentException("List is immutable"); + } else { + return super.remove(index); + } + } + + @Override + public boolean addAll(final Collection c) { + if (!modifiable && !c.isEmpty()) { + throw new IllegalArgumentException("List is immutable"); + } else { + return super.addAll(c); + } + } + + @Override + public boolean addAll(final int index, final Collection c) { + if (!modifiable && !c.isEmpty()) { + throw new IllegalArgumentException("List is immutable"); + } else { + return super.addAll(index, c); + } + } + + @Override + public boolean removeAll(final Collection c) { + if (!modifiable && !c.isEmpty()) { + throw new IllegalArgumentException("List is immutable"); + } else { + return super.removeAll(c); + } + } + + @Override + public boolean retainAll(final Collection c) { + if (!modifiable && !isEmpty()) { + throw new IllegalArgumentException("List is immutable"); + } else { + return super.retainAll(c); + } + } + + @Override + public void clear() { + if (!modifiable && !isEmpty()) { + throw new IllegalArgumentException("List is immutable"); + } else { + super.clear(); + } + } + + @Override + public T set(final int index, final T element) { + if (!modifiable) { + throw new IllegalArgumentException("List is immutable"); + } else { + return super.set(index, element); + } + } + + // Provide replacement iterators so that there is no chance of a thread that + // is trying to sort the empty list causing a ConcurrentModificationException + // in another thread that is iterating over the empty list (#334) + + @Override + public Iterator iterator() { + final Iterator iterator = super.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + if (isEmpty()) { + return false; + } else { + return iterator.hasNext(); + } + } + + @Override + public T next() { + return iterator.next(); + } + + @Override + public void remove() { + if (!modifiable) { + throw new IllegalArgumentException("List is immutable"); + } else { + iterator.remove(); + } + } + }; + } + + @Override + public ListIterator listIterator() { + final ListIterator iterator = super.listIterator(); + return new ListIterator() { + @Override + public boolean hasNext() { + if (isEmpty()) { + return false; + } else { + return iterator.hasNext(); + } + } + + @Override + public T next() { + return iterator.next(); + } + + @Override + public boolean hasPrevious() { + if (isEmpty()) { + return false; + } else { + return iterator.hasPrevious(); + } + } + + @Override + public T previous() { + return iterator.previous(); + } + + @Override + public int nextIndex() { + if (isEmpty()) { + return 0; + } else { + return iterator.nextIndex(); + } + } + + @Override + public int previousIndex() { + if (isEmpty()) { + return -1; + } else { + return iterator.previousIndex(); + } + } + + @Override + public void remove() { + if (!modifiable) { + throw new IllegalArgumentException("List is immutable"); + } else { + iterator.remove(); + } + } + + @Override + public void set(final T e) { + if (!modifiable) { + throw new IllegalArgumentException("List is immutable"); + } else { + iterator.set(e); + } + } + + @Override + public void add(final T e) { + if (!modifiable) { + throw new IllegalArgumentException("List is immutable"); + } else { + iterator.add(e); + } + } + }; + } +} diff --git a/src/main/java/io/github/classgraph/Resource.java b/src/main/java/io/github/classgraph/Resource.java index f442c2976..b7430850a 100644 --- a/src/main/java/io/github/classgraph/Resource.java +++ b/src/main/java/io/github/classgraph/Resource.java @@ -37,14 +37,17 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; import java.util.zip.ZipEntry; -import nonapi.io.github.classgraph.utils.FileUtils; -import nonapi.io.github.classgraph.utils.InputStreamOrByteBufferAdapter; +import nonapi.io.github.classgraph.fileslice.reader.ClassfileReader; import nonapi.io.github.classgraph.utils.LogNode; +import nonapi.io.github.classgraph.utils.URLPathEncoder; /** - * A classpath or module path resource (i.e. file) that was found in a whitelisted/non-blacklisted package inside a + * A classpath or module path resource (i.e. file) that was found in an accepted/non-rejected package inside a * classpath element or module. */ public abstract class Resource implements Closeable, Comparable { @@ -60,15 +63,12 @@ public abstract class Resource implements Closeable, Comparable { /** The length, or -1L for unknown. */ protected long length; - /** True if the resource is open. */ - private boolean isOpen; - /** The cached result of toString(). */ private String toString; /** * The {@link LogNode} used to log that the resource was found when classpath element paths are scanned. In the - * case of whitelisted classfile resources, sublog entries are added when the classfile's contents are scanned. + * case of accepted classfile resources, sublog entries are added when the classfile's contents are scanned. */ LogNode scanLog; @@ -89,238 +89,27 @@ public Resource(final ClasspathElement classpathElement, final long length) { // ------------------------------------------------------------------------------------------------------------- - /** - * Create an {@link InputStream} from a {@link ByteBuffer}. - * - * @return the input stream - */ - protected InputStream byteBufferToInputStream() { - return inputStream == null ? inputStream = FileUtils.byteBufferToInputStream(byteBuffer) : inputStream; - } - - /** - * Create a {@link ByteBuffer} from an {@link InputStream}. - * - * @return the byte buffer - * @throws IOException - * if an I/O exception occurs. - */ - protected ByteBuffer inputStreamToByteBuffer() throws IOException { - return byteBuffer == null ? byteBuffer = ByteBuffer.wrap(inputStreamToByteArray()) : byteBuffer; - } - - /** - * Read all bytes from an {@link InputStream} and return as a byte array. - * - * @return the contents of the {@link InputStream}. - * @throws IOException - * if an I/O exception occurs. - */ - protected byte[] inputStreamToByteArray() throws IOException { - return FileUtils.readAllBytesAsArray(inputStream, length); - } - - /** - * Read/copy contents of a {@link ByteBuffer} as a byte array. - * - * @return the contents of the {@link ByteBuffer} as a byte array. - */ - protected byte[] byteBufferToByteArray() { - if (byteBuffer.hasArray()) { - return byteBuffer.array(); - } else { - final byte[] byteArray = new byte[byteBuffer.remaining()]; - byteBuffer.get(byteArray); - return byteArray; - } - } - - // ------------------------------------------------------------------------------------------------------------- - - /** - * Class for closing the parent {@link Resource} when an {@link InputStream} opened on the resource is closed. - */ - protected class InputStreamResourceCloser extends InputStream { - - /** The input stream. */ - private InputStream inputStream; - - /** The parent resource. */ - private Resource parentResource; - - /** - * Constructor. - * - * @param parentResource - * the parent resource - * @param inputStream - * the input stream - * @throws IOException - * if an I/O exception occurs. - */ - protected InputStreamResourceCloser(final Resource parentResource, final InputStream inputStream) - throws IOException { - super(); - if (inputStream == null) { - throw new IOException("InputStream cannot be null"); - } - this.inputStream = inputStream; - this.parentResource = parentResource; - } - - /* (non-Javadoc) - * @see java.io.InputStream#read() - */ - @Override - public int read() throws IOException { - if (inputStream == null) { - throw new IOException("InputStream is not open"); - } - return inputStream.read(); - } - - /* (non-Javadoc) - * @see java.io.InputStream#read(byte[], int, int) - */ - @Override - public int read(final byte[] b, final int off, final int len) throws IOException { - if (inputStream == null) { - throw new IOException("InputStream is not open"); - } - return inputStream.read(b, off, len); - } - - /* (non-Javadoc) - * @see java.io.InputStream#read(byte[]) - */ - @Override - public int read(final byte[] b) throws IOException { - if (inputStream == null) { - throw new IOException("InputStream is not open"); - } - return inputStream.read(b); - } - - /* (non-Javadoc) - * @see java.io.InputStream#available() - */ - @Override - public int available() throws IOException { - if (inputStream == null) { - throw new IOException("InputStream is not open"); - } - return inputStream.available(); - } - - /* (non-Javadoc) - * @see java.io.InputStream#skip(long) - */ - @Override - public long skip(final long n) throws IOException { - if (inputStream == null) { - throw new IOException("InputStream is not open"); - } - return inputStream.skip(n); - } - - /* (non-Javadoc) - * @see java.io.InputStream#markSupported() - */ - @Override - public boolean markSupported() { - return inputStream.markSupported(); - } - - /* (non-Javadoc) - * @see java.io.InputStream#mark(int) - */ - @Override - public synchronized void mark(final int readlimit) { - inputStream.mark(readlimit); - } - - /* (non-Javadoc) - * @see java.io.InputStream#reset() - */ - @Override - public synchronized void reset() throws IOException { - if (inputStream == null) { - throw new IOException("InputStream is not open"); - } - inputStream.reset(); - } - - /** - * Close the wrapped InputStream, but don't close parent resource. - * - * @throws IOException - * if an I/O exception occurs. - */ - void closeInputStream() throws IOException { - if (inputStream != null) { - try { - inputStream.close(); - } catch (final IOException e) { - // Ignore - } - inputStream = null; - } - } - - /** - * Close the parent resource by calling {@link Resource#close()}, which will call - * {@link #closeInputStream()}. - * - * @throws IOException - * if an I/O exception occurs. - */ - @Override - public void close() throws IOException { - if (parentResource != null) { - parentResource.close(); - parentResource = null; - } - } - } - - // ------------------------------------------------------------------------------------------------------------- - - /** - * Mark the resource as open. - * - * @throws IOException - * If the resource is already open. - */ - protected void markAsOpen() throws IOException { - if (isOpen) { - throw new IOException("Resource is already open -- cannot open it again without first calling close()"); - } - isOpen = true; - } - - /** Mark the resource as closed. */ - protected void markAsClosed() { - isOpen = false; - } - - // ------------------------------------------------------------------------------------------------------------- - /** * Convert a URI to URL, catching "jrt:" URIs as invalid. * + * @param uri + * the uri * @return the URL. * @throws IllegalArgumentException * if the URI could not be converted to a URL, or the URI had "jrt:" scheme. */ private static URL uriToURL(final URI uri) { - if (uri.getScheme().equals("jrt")) { - // Currently URL cannot handle the "jrt:" scheme, used by system modules. - throw new IllegalArgumentException("Could not create URL from URI with \"jrt:\" scheme: " + uri); - } try { return uri.toURL(); - } catch (final MalformedURLException e) { - throw new IllegalArgumentException("Could not create URL from URI: " + uri + " -- " + e); + } catch (final IllegalArgumentException | MalformedURLException e) { + if (uri.getScheme().equals("jrt")) { + // Currently URL cannot handle the "jrt:" scheme, used by system modules. + throw new IllegalArgumentException("Could not create URL from URI with \"jrt:\" scheme " + + "(\"jrt:\" is not supported by the URL class without a custom URL protocol handler): " + + uri); + } else { + throw new IllegalArgumentException("Could not create URL from URI: " + uri + " -- " + e); + } } } @@ -333,12 +122,15 @@ private static URL uriToURL(final URI uri) { */ public URI getURI() { final URI locationURI = getClasspathElementURI(); - final String resourcePath = getPathRelativeToClasspathElement(); final String locationURIStr = locationURI.toString(); + final String resourcePath = getPathRelativeToClasspathElement(); // Check if this is a directory-based module (location URI will end in "/") final boolean isDir = locationURIStr.endsWith("/"); try { - return new URI((isDir ? "" : "jar:") + locationURIStr + (isDir ? "" : "!/") + resourcePath); + return new URI( + (isDir || locationURIStr.startsWith("jar:") || locationURIStr.startsWith("jrt:") ? "" : "jar:") + + locationURIStr + (isDir ? "" : locationURIStr.startsWith("jrt:") ? "/" : "!/") + + URLPathEncoder.encodePath(resourcePath)); } catch (final URISyntaxException e) { throw new IllegalArgumentException("Could not form URL for classpath element: " + locationURIStr + " ; path: " + resourcePath + " : " + e); @@ -348,7 +140,7 @@ public URI getURI() { /** * Get the {@link URL} representing the resource's location. Use {@link #getURI()} instead if the resource may * have come from a system module, or if this is a jlink'd runtime image, since "jrt:" URI schemes used by - * system modules and jlink'd runtime images are not suppored by {@link URL}, and this will cause + * system modules and jlink'd runtime images are not supported by {@link URL}, and this will cause * {@link IllegalArgumentException} to be thrown. * * @return A {@link URL} representing the resource's location. @@ -365,7 +157,7 @@ public URL getURL() { * * @return The {@link URL} of the classpath element or module that this resource was found within. * @throws IllegalArgumentException - * if the resource was obtained from a module and the module's location URI is null. + * if the classpath element does not have a valid URI (e.g. for modules whose location URI is null). */ public URI getClasspathElementURI() { return classpathElement.getURI(); @@ -375,7 +167,7 @@ public URI getClasspathElementURI() { * Get the {@link URL} of the classpath element or module that this resource was obtained from. Use * {@link #getClasspathElementURI()} instead if the resource may have come from a system module, or if this is a * jlink'd runtime image, since "jrt:" URI schemes used by system modules and jlink'd runtime images are not - * suppored by {@link URL}, and this will cause {@link IllegalArgumentException} to be thrown. + * supported by {@link URL}, and this will cause {@link IllegalArgumentException} to be thrown. * * @return The {@link URL} of the classpath element or module that this resource was found within. * @throws IllegalArgumentException @@ -389,9 +181,11 @@ public URL getClasspathElementURL() { /** * Get the classpath element {@link File}. * - * @return The {@link File} for the classpath element package root dir or jar that this {@Resource} was found - * within, or null if this {@link Resource} was found in a module backed by a "jrt:" URI, or a module - * with an unknown location. + * @return The {@link File} for the classpath element package root dir or jar that this {@link Resource} was + * found within, or null if this {@link Resource} was found in a module backed by a "jrt:" URI, or a + * module with an unknown location. May also return null if the classpath element was an http/https URL, + * and the jar was downloaded directly to RAM, rather than to a temp file on disk (e.g. if the temp dir + * is not writeable). */ public File getClasspathElementFile() { return classpathElement.getFile(); @@ -409,6 +203,20 @@ public ModuleRef getModuleRef() { : null; } + /** + * Convenience method to get the content of this {@link Resource} as a String. Assumes UTF8 encoding. (Calls + * {@link #close()} after completion.) + * + * @return the content of this {@link Resource} as a String. + * @throws IOException + * If an I/O exception occurred. + */ + public String getContentAsString() throws IOException { + final String content = new String(load(), StandardCharsets.UTF_8); + close(); + return content; + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -429,14 +237,17 @@ public ModuleRef getModuleRef() { * full path of {@code "BOOT-INF/classes/com/xyz/resource.xml"} or * {@code "META-INF/versions/11/com/xyz/resource.xml"}, not {@code "com/xyz/resource.xml"}. */ - public abstract String getPathRelativeToClasspathElement(); + public String getPathRelativeToClasspathElement() { + // Only overridden for jars + return getPath(); + } // ------------------------------------------------------------------------------------------------------------- /** * Open an {@link InputStream} for a classpath resource. Make sure you call {@link Resource#close()} when you * are finished with the {@link InputStream}, so that the {@link InputStream} is closed. - * + * * @return The opened {@link InputStream}. * @throws IOException * If the {@link InputStream} could not be opened. @@ -445,19 +256,42 @@ public ModuleRef getModuleRef() { /** * Open a {@link ByteBuffer} for a classpath resource. Make sure you call {@link Resource#close()} when you are - * finished with the {@link ByteBuffer}, so that the {@link ByteBuffer} is released or unmapped. - * + * finished with the {@link ByteBuffer}, so that the {@link ByteBuffer} is released or unmapped. See also + * {@link #readCloseable()}. + * * @return The allocated or mapped {@link ByteBuffer} for the resource file content. * @throws IOException - * If the resource could not be opened. + * If the resource could not be read. */ public abstract ByteBuffer read() throws IOException; + /** + * Open a {@link ByteBuffer} for a classpath resource, and wrap it in a {@link CloseableByteBuffer} instance, + * which implements the {@link Closeable#close()} method to free the underlying {@link ByteBuffer} when + * {@link CloseableByteBuffer#close()} is called, by automatically calling {@link Resource#close()}. + * + *

+ * Call {@link CloseableByteBuffer#getByteBuffer()} on the returned instance to access the underlying + * {@link ByteBuffer}. + * + * @return The allocated or mapped {@link ByteBuffer} for the resource file content. + * @throws IOException + * If the resource could not be read. + */ + public CloseableByteBuffer readCloseable() throws IOException { + return new CloseableByteBuffer(read(), new Runnable() { + @Override + public void run() { + close(); + } + }); + } + /** * Load a classpath resource and return its content as a byte array. Automatically calls * {@link Resource#close()} after loading the byte array and before returning it, so that the underlying * InputStream is closed or the underlying ByteBuffer is released or unmapped. - * + * * @return The contents of the resource file. * @throws IOException * If the file contents could not be loaded in their entirety. @@ -465,14 +299,13 @@ public ModuleRef getModuleRef() { public abstract byte[] load() throws IOException; /** - * Open a {@link ByteBuffer}, if there is an efficient underlying mechanism for opening one, otherwise open an - * {@link InputStream}. + * Open a {@link ClassfileReader} on the resource (for reading classfiles). * - * @return the {@link InputStreamOrByteBufferAdapter} + * @return the {@link ClassfileReader}. * @throws IOException * if an I/O exception occurs. */ - abstract InputStreamOrByteBufferAdapter openOrRead() throws IOException; + abstract ClassfileReader openClassfile() throws IOException; /** * Get the length of the resource. @@ -486,6 +319,33 @@ public long getLength() { return length; } + /** + * Get the last modified time for the resource, in milliseconds since the epoch. This time is obtained from the + * directory entry, if this resource is a file on disk, or from the zipfile central directory, if this resource + * is a zipfile entry. Timestamps are not available for resources obtained from system modules or jlink'd + * modules. + * + *

+ * Note: The ZIP format has no notion of timezone, so timestamps are only meaningful if it is known what + * timezone they were created in. We arbitrarily assume that zipfile timestamps are in the UTC timezone. This + * may be a wrong assumption, so you may need to apply a timezone correction if you know the timezone used by + * the zipfile creator. + * + * @return The millis since the epoch indicating the date / time that this file resource was last modified. + * Returns 0L if the last modified date is unknown. + */ + public abstract long getLastModified(); + + /** + * Get the POSIX file permissions for the resource. POSIX file permissions are obtained from the directory + * entry, if this resource is a file on disk, or from the zipfile central directory, if this resource is a + * zipfile entry. POSIX file permissions are not available for resources obtained from system modules or jlink'd + * modules, and may not be available on non-POSIX-compliant operating systems or non-POSIX filesystems. + * + * @return The set of {@link PosixFilePermission} permission flags for the resource, or null if unknown. + */ + public abstract Set getPosixFilePermissions(); + // ------------------------------------------------------------------------------------------------------------- /** @@ -498,10 +358,15 @@ public String toString() { if (toString != null) { return toString; } else { - return toString = getURL().toString(); + return toString = getURI().toString(); } } + /** + * Hash code. + * + * @return the int + */ /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @@ -510,17 +375,33 @@ public int hashCode() { return toString().hashCode(); } + /** + * Equals. + * + * @param obj + * the obj + * @return true, if successful + */ /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object obj) { - if (!(obj instanceof Resource)) { + if (obj == this) { + return true; + } else if (!(obj instanceof Resource)) { return false; } return this.toString().equals(obj.toString()); } + /** + * Compare to. + * + * @param o + * the o + * @return the int + */ /* (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ @@ -534,14 +415,10 @@ public int compareTo(final Resource o) { /** Close the underlying InputStream, or release/unmap the underlying ByteBuffer. */ @Override public void close() { - // Override in subclasses, and call super.close() + // Override in subclasses, and call super.close(), then at end, markAsClosed() if (inputStream != null) { try { - if (inputStream instanceof InputStreamResourceCloser) { - ((InputStreamResourceCloser) inputStream).closeInputStream(); - } else { - inputStream.close(); - } + inputStream.close(); } catch (final IOException e) { // Ignore } diff --git a/src/main/java/io/github/classgraph/ResourceList.java b/src/main/java/io/github/classgraph/ResourceList.java index 6063c6de1..32b3e348c 100644 --- a/src/main/java/io/github/classgraph/ResourceList.java +++ b/src/main/java/io/github/classgraph/ResourceList.java @@ -30,101 +30,98 @@ import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.net.URL; import java.nio.ByteBuffer; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import nonapi.io.github.classgraph.utils.CollectionUtils; + /** An AutoCloseable list of AutoCloseable {@link Resource} objects. */ -public class ResourceList extends ArrayList implements AutoCloseable { +public class ResourceList extends PotentiallyUnmodifiableList implements AutoCloseable { /** serialVersionUID. */ static final long serialVersionUID = 1L; /** An unmodifiable empty {@link ResourceList}. */ - static final ResourceList EMPTY_LIST = new ResourceList() { - @Override - public boolean add(final Resource e) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void add(final int index, final Resource element) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean remove(final Object o) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public Resource remove(final int index) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean addAll(final int index, final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean removeAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public boolean retainAll(final Collection c) { - throw new IllegalArgumentException("List is immutable"); - } - - @Override - public void clear() { - throw new IllegalArgumentException("List is immutable"); - } + static final ResourceList EMPTY_LIST = new ResourceList(); + static { + EMPTY_LIST.makeUnmodifiable(); + } - @Override - public Resource set(final int index, final Resource element) { - throw new IllegalArgumentException("List is immutable"); - } - }; + /** + * Return an unmodifiable empty {@link ResourceList}. + * + * @return the unmodifiable empty {@link ResourceList}. + */ + public static ResourceList emptyList() { + return EMPTY_LIST; + } /** - * Constructor. + * Create a new modifiable empty list of {@link Resource} objects. */ - ResourceList() { + public ResourceList() { super(); } /** - * Constructor. + * Create a new modifiable empty list of {@link Resource} objects, given a size hint. * * @param sizeHint * the size hint */ - ResourceList(final int sizeHint) { + public ResourceList(final int sizeHint) { super(sizeHint); } /** - * Constructor. + * Create a new modifiable empty {@link ResourceList}, given an initial collection of {@link Resource} objects. * - * @param collection - * the collection + * @param resourceCollection + * the collection of {@link Resource} objects. */ - ResourceList(final Collection collection) { - super(collection); + public ResourceList(final Collection resourceCollection) { + super(resourceCollection); + } + + /** + * Returns a list of all resources with the requested path. (There may be more than one resource with a given + * path, from different classpath elements or modules, so this returns a {@link ResourceList} rather than a + * single {@link Resource}.) + * + * @param resourcePath + * The path of a resource + * @return A {@link ResourceList} of {@link Resource} objects in this list that have the given path (there may + * be more than one resource with a given path, from different classpath elements or modules, so this + * returns a {@link ResourceList} rather than a single {@link Resource}.) Returns the empty list if no + * resource with is found with a matching path. + */ + public ResourceList get(final String resourcePath) { + boolean hasResourceWithPath = false; + for (final Resource res : this) { + if (res.getPath().equals(resourcePath)) { + hasResourceWithPath = true; + break; + } + } + if (!hasResourceWithPath) { + return EMPTY_LIST; + } else { + final ResourceList matchingResources = new ResourceList(2); + for (final Resource res : this) { + if (res.getPath().equals(resourcePath)) { + matchingResources.add(res); + } + } + return matchingResources; + } } // ------------------------------------------------------------------------------------------------------------- @@ -159,6 +156,9 @@ public List getPathsRelativeToClasspathElement() { /** * Get the URLs of all resources in this list, by calling {@link Resource#getURL()} for each item in the list. + * Note that any resource with a {@code jrt:} URI (e.g. a system resource, or a resource from a jlink'd image) + * will cause {@link IllegalArgumentException} to be thrown, since {@link URL} does not support this scheme, so + * {@link #getURIs()} is strongly preferred over {@link #getURLs()}. * * @return The URLs of all resources in this list. */ @@ -170,6 +170,19 @@ public List getURLs() { return resourceURLs; } + /** + * Get the URIs of all resources in this list, by calling {@link Resource#getURI()} for each item in the list. + * + * @return The URIs of all resources in this list. + */ + public List getURIs() { + final List resourceURLs = new ArrayList<>(this.size()); + for (final Resource resource : this) { + resourceURLs.add(resource.getURI()); + } + return resourceURLs; + } + // ------------------------------------------------------------------------------------------------------------- /** Returns true if a Resource has a path ending in ".class". */ @@ -249,7 +262,7 @@ public List> findDuplicatePaths() { duplicatePaths.add(new SimpleEntry<>(pathAndResourceList.getKey(), pathAndResourceList.getValue())); } } - Collections.sort(duplicatePaths, new Comparator>() { + CollectionUtils.sortIfNotEmpty(duplicatePaths, new Comparator>() { @Override public int compare(final Entry o1, final Entry o2) { // Sort in lexicographic order of path @@ -311,6 +324,26 @@ public interface ByteArrayConsumer { void accept(final Resource resource, final byte[] byteArray); } + /** + * A {@link FunctionalInterface} for consuming the contents of a {@link Resource} as a byte array, throwing + * {@link IOException} to the caller if an IO exception occurs. + */ + @FunctionalInterface + public interface ByteArrayConsumerThrowsIOException { + /** + * Consume the complete content of a {@link Resource} as a byte array, possibly throwing + * {@link IOException}. + * + * @param resource + * The {@link Resource} used to load the byte array. + * @param byteArray + * The complete content of the resource. + * @throws IOException + * if an IO exception occurs. + */ + void accept(final Resource resource, final byte[] byteArray) throws IOException; + } + /** * Fetch the content of each {@link Resource} in this {@link ResourceList} as a byte array, pass the byte array * to the given {@link ByteArrayConsumer}, then close the underlying InputStream or release the underlying @@ -324,18 +357,18 @@ public interface ByteArrayConsumer { * @throws IllegalArgumentException * if ignoreExceptions is false, and an {@link IOException} is thrown while trying to load any of * the resources. + * @deprecated Use {@link #forEachByteArrayIgnoringIOException(ByteArrayConsumer)} or + * {@link #forEachByteArrayThrowingIOException(ByteArrayConsumerThrowsIOException)} instead. */ + @Deprecated public void forEachByteArray(final ByteArrayConsumer byteArrayConsumer, final boolean ignoreIOExceptions) { for (final Resource resource : this) { - try { - final byte[] resourceContent = resource.load(); - byteArrayConsumer.accept(resource, resourceContent); + try (final Resource resourceToClose = resource) { + byteArrayConsumer.accept(resourceToClose, resourceToClose.load()); } catch (final IOException e) { if (!ignoreIOExceptions) { throw new IllegalArgumentException("Could not load resource " + resource, e); } - } finally { - resource.close(); } } } @@ -348,10 +381,50 @@ public void forEachByteArray(final ByteArrayConsumer byteArrayConsumer, final bo * @param byteArrayConsumer * The {@link ByteArrayConsumer}. * @throws IllegalArgumentException - * if trying to load any of the resources results in an {@link IOException} being thrown. + * if an {@link IOException} is thrown while trying to load any of the resources. + * @deprecated Use {@link #forEachByteArrayThrowingIOException(ByteArrayConsumerThrowsIOException)} instead. */ + @Deprecated public void forEachByteArray(final ByteArrayConsumer byteArrayConsumer) { - forEachByteArray(byteArrayConsumer, /* ignoreIOExceptions = */ false); + forEachByteArray(byteArrayConsumer, false); + } + + /** + * Fetch the content of each {@link Resource} in this {@link ResourceList} as a byte array, pass the byte array + * to the given {@link ByteArrayConsumer}, then close the underlying InputStream or release the underlying + * ByteBuffer by calling {@link Resource#close()} for each {@link Resource}. If an {@link IOException} occurs + * while opening or reading from any resource, the resource is silently skipped. + * + * @param byteArrayConsumer + * The {@link ByteArrayConsumer}. + */ + public void forEachByteArrayIgnoringIOException(final ByteArrayConsumer byteArrayConsumer) { + for (final Resource resource : this) { + try (Resource resourceToClose = resource) { + byteArrayConsumer.accept(resourceToClose, resourceToClose.load()); + } catch (final IOException e) { + // Ignore + } + } + } + + /** + * Fetch the content of each {@link Resource} in this {@link ResourceList} as a byte array, pass the byte array + * to the given {@link ByteArrayConsumer}, then close the underlying InputStream or release the underlying + * ByteBuffer by calling {@link Resource#close()}. + * + * @param byteArrayConsumerThrowsIOException + * The {@link ByteArrayConsumerThrowsIOException}. + * @throws IOException + * if trying to load any of the resources results in an {@link IOException} being thrown. + */ + public void forEachByteArrayThrowingIOException( + final ByteArrayConsumerThrowsIOException byteArrayConsumerThrowsIOException) throws IOException { + for (final Resource resource : this) { + try (Resource resourceToClose = resource) { + byteArrayConsumerThrowsIOException.accept(resourceToClose, resourceToClose.load()); + } + } } // ------------------------------------------------------------------------------------------------------------- @@ -370,10 +443,30 @@ public interface InputStreamConsumer { void accept(final Resource resource, final InputStream inputStream); } + /** + * A {@link FunctionalInterface} for consuming the contents of a {@link Resource} as an {@link InputStream}, + * throwing {@link IOException} to the caller if an IO exception occurs. + */ + @FunctionalInterface + public interface InputStreamConsumerThrowsIOException { + /** + * Consume the complete content of a {@link Resource} as a byte array, possibly throwing + * {@link IOException}. + * + * @param resource + * The {@link Resource} used to load the byte array. + * @param inputStream + * The {@link InputStream} opened on the resource. + * @throws IOException + * if an IO exception occurs. + */ + void accept(final Resource resource, final InputStream inputStream) throws IOException; + } + /** * Fetch an {@link InputStream} for each {@link Resource} in this {@link ResourceList}, pass the * {@link InputStream} to the given {@link InputStreamConsumer}, then close the {@link InputStream} after the - * {@link InputStreamConsumer} returns, by calling {@link Resource#close()}. + * {@link InputStreamConsumer} returns, by calling {@link Resource#close()} for each {@link Resource}. * * @param inputStreamConsumer * The {@link InputStreamConsumer}. @@ -383,18 +476,19 @@ public interface InputStreamConsumer { * @throws IllegalArgumentException * if ignoreExceptions is false, and an {@link IOException} is thrown while trying to open any of * the resources. + * @deprecated Use {@link #forEachInputStreamIgnoringIOException(InputStreamConsumer)} or + * {@link #forEachInputStreamThrowingIOException(InputStreamConsumerThrowsIOException)} instead. */ + @Deprecated public void forEachInputStream(final InputStreamConsumer inputStreamConsumer, final boolean ignoreIOExceptions) { for (final Resource resource : this) { - try { - inputStreamConsumer.accept(resource, resource.open()); + try (final Resource resourceToClose = resource) { + inputStreamConsumer.accept(resourceToClose, resourceToClose.open()); } catch (final IOException e) { if (!ignoreIOExceptions) { throw new IllegalArgumentException("Could not load resource " + resource, e); } - } finally { - resource.close(); } } } @@ -402,15 +496,56 @@ public void forEachInputStream(final InputStreamConsumer inputStreamConsumer, /** * Fetch an {@link InputStream} for each {@link Resource} in this {@link ResourceList}, pass the * {@link InputStream} to the given {@link InputStreamConsumer}, then close the {@link InputStream} after the - * {@link InputStreamConsumer} returns, by calling {@link Resource#close()}. + * {@link InputStreamConsumer} returns, by calling {@link Resource#close()} for each {@link Resource}. * * @param inputStreamConsumer * The {@link InputStreamConsumer}. * @throws IllegalArgumentException - * if trying to open any of the resources results in an {@link IOException} being thrown. + * an {@link IOException} is thrown while trying to open any of the resources. + * @deprecated Use {@link #forEachInputStreamThrowingIOException(InputStreamConsumerThrowsIOException)} instead. */ + @Deprecated public void forEachInputStream(final InputStreamConsumer inputStreamConsumer) { - forEachInputStream(inputStreamConsumer, /* ignoreIOExceptions = */ false); + forEachInputStream(inputStreamConsumer, false); + } + + /** + * Fetch an {@link InputStream} for each {@link Resource} in this {@link ResourceList}, pass the + * {@link InputStream} to the given {@link InputStreamConsumer}, then close the {@link InputStream} after the + * {@link InputStreamConsumer} returns, by calling {@link Resource#close()} for each {@link Resource}. If an + * {@link IOException} occurs while opening or reading from any resource, the resource is silently skipped. + * + * @param inputStreamConsumer + * The {@link InputStreamConsumer}. + */ + public void forEachInputStreamIgnoringIOException(final InputStreamConsumer inputStreamConsumer) { + for (final Resource resource : this) { + try (final Resource resourceToClose = resource) { + inputStreamConsumer.accept(resourceToClose, resourceToClose.open()); + } catch (final IOException e) { + // Ignore + } + } + } + + /** + * Fetch an {@link InputStream} for each {@link Resource} in this {@link ResourceList}, pass the + * {@link InputStream} to the given {@link InputStreamConsumer}, then close the {@link InputStream} after the + * {@link InputStreamConsumer} returns, by calling {@link Resource#close()}. + * + * @param inputStreamConsumerThrowsIOException + * The {@link InputStreamConsumerThrowsIOException}. + * @throws IOException + * if trying to open or read from any of the resources results in an {@link IOException} being + * thrown. + */ + public void forEachInputStreamThrowingIOException( + final InputStreamConsumerThrowsIOException inputStreamConsumerThrowsIOException) throws IOException { + for (final Resource resource : this) { + try (final Resource resourceToClose = resource) { + inputStreamConsumerThrowsIOException.accept(resourceToClose, resourceToClose.open()); + } + } } // ------------------------------------------------------------------------------------------------------------- @@ -419,7 +554,7 @@ public void forEachInputStream(final InputStreamConsumer inputStreamConsumer) { @FunctionalInterface public interface ByteBufferConsumer { /** - * Consume a {@link Resource} as a {@link ByteBuffer}. + * Consume a {@link Resource} as a {@link ByteBuffer}, possibly throwing {@link IOException}. * * @param resource * The {@link Resource} whose content is reflected in the {@link ByteBuffer}. @@ -429,10 +564,29 @@ public interface ByteBufferConsumer { void accept(final Resource resource, final ByteBuffer byteBuffer); } + /** + * A {@link FunctionalInterface} for consuming the contents of a {@link Resource} as a {@link ByteBuffer}, + * throwing {@link IOException} to the caller if an IO exception occurs. + */ + @FunctionalInterface + public interface ByteBufferConsumerThrowsIOException { + /** + * Consume the complete content of a {@link Resource} as a byte array. + * + * @param resource + * The {@link Resource} used to load the byte array, possibly throwing {@link IOException}. + * @param byteBuffer + * The {@link ByteBuffer} mapped to the resource. + * @throws IOException + * if an IO exception occurs. + */ + void accept(final Resource resource, final ByteBuffer byteBuffer) throws IOException; + } + /** * Read each {@link Resource} in this {@link ResourceList} as a {@link ByteBuffer}, pass the {@link ByteBuffer} * to the given {@link InputStreamConsumer}, then release the {@link ByteBuffer} after the - * {@link ByteBufferConsumer} returns, by calling {@link Resource#close()}. + * {@link ByteBufferConsumer} returns, by calling {@link Resource#close()} for each {@link Resource}. * * @param byteBufferConsumer * The {@link ByteBufferConsumer}. @@ -442,18 +596,18 @@ public interface ByteBufferConsumer { * @throws IllegalArgumentException * if ignoreExceptions is false, and an {@link IOException} is thrown while trying to load any of * the resources. + * @deprecated Use {@link #forEachByteBufferIgnoringIOException(ByteBufferConsumer)} or + * {@link #forEachByteBufferThrowingIOException(ByteBufferConsumerThrowsIOException)} instead. */ + @Deprecated public void forEachByteBuffer(final ByteBufferConsumer byteBufferConsumer, final boolean ignoreIOExceptions) { for (final Resource resource : this) { - try { - final ByteBuffer byteBuffer = resource.read(); - byteBufferConsumer.accept(resource, byteBuffer); + try (final Resource resourceToClose = resource) { + byteBufferConsumer.accept(resourceToClose, resourceToClose.read()); } catch (final IOException e) { if (!ignoreIOExceptions) { throw new IllegalArgumentException("Could not load resource " + resource, e); } - } finally { - resource.close(); } } } @@ -461,15 +615,55 @@ public void forEachByteBuffer(final ByteBufferConsumer byteBufferConsumer, final /** * Read each {@link Resource} in this {@link ResourceList} as a {@link ByteBuffer}, pass the {@link ByteBuffer} * to the given {@link InputStreamConsumer}, then release the {@link ByteBuffer} after the - * {@link ByteBufferConsumer} returns, by calling {@link Resource#close()}. + * {@link ByteBufferConsumer} returns, by calling {@link Resource#close()} for each {@link Resource}. * * @param byteBufferConsumer * The {@link ByteBufferConsumer}. * @throws IllegalArgumentException - * if trying to load any of the resources results in an {@link IOException} being thrown. + * if an {@link IOException} is thrown while trying to load any of the resources. + * @deprecated Use {@link #forEachByteBufferThrowingIOException(ByteBufferConsumerThrowsIOException)} instead. */ + @Deprecated public void forEachByteBuffer(final ByteBufferConsumer byteBufferConsumer) { - forEachByteBuffer(byteBufferConsumer, /* ignoreIOExceptions = */ false); + forEachByteBuffer(byteBufferConsumer, false); + } + + /** + * Read each {@link Resource} in this {@link ResourceList} as a {@link ByteBuffer}, pass the {@link ByteBuffer} + * to the given {@link InputStreamConsumer}, then release the {@link ByteBuffer} after the + * {@link ByteBufferConsumer} returns, by calling {@link Resource#close()} for each {@link Resource}. If an + * {@link IOException} occurs while opening or reading from any resource, the resource is silently skipped. + * + * @param byteBufferConsumer + * The {@link ByteBufferConsumer}. + */ + public void forEachByteBufferIgnoringIOException(final ByteBufferConsumer byteBufferConsumer) { + for (final Resource resource : this) { + try (final Resource resourceToClose = resource) { + byteBufferConsumer.accept(resourceToClose, resourceToClose.read()); + } catch (final IOException e) { + // Ignore + } + } + } + + /** + * Read each {@link Resource} in this {@link ResourceList} as a {@link ByteBuffer}, pass the {@link ByteBuffer} + * to the given {@link InputStreamConsumer}, then release the {@link ByteBuffer} after the + * {@link ByteBufferConsumer} returns, by calling {@link Resource#close()}. + * + * @param byteBufferConsumerThrowsIOException + * The {@link ByteBufferConsumerThrowsIOException}. + * @throws IOException + * if trying to load any of the resources results in an {@link IOException} being thrown. + */ + public void forEachByteBufferThrowingIOException( + final ByteBufferConsumerThrowsIOException byteBufferConsumerThrowsIOException) throws IOException { + for (final Resource resource : this) { + try (final Resource resourceToClose = resource) { + byteBufferConsumerThrowsIOException.accept(resourceToClose, resourceToClose.read()); + } + } } // ------------------------------------------------------------------------------------------------------------- diff --git a/src/main/java/io/github/classgraph/ScanResult.java b/src/main/java/io/github/classgraph/ScanResult.java index e027976db..83bbaec8e 100644 --- a/src/main/java/io/github/classgraph/ScanResult.java +++ b/src/main/java/io/github/classgraph/ScanResult.java @@ -30,10 +30,12 @@ import java.io.Closeable; import java.io.File; +import java.lang.annotation.Annotation; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -45,35 +47,52 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClasspathFinder; +import nonapi.io.github.classgraph.concurrency.AutoCloseableExecutorService; import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; import nonapi.io.github.classgraph.json.JSONDeserializer; import nonapi.io.github.classgraph.json.JSONSerializer; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.AcceptReject; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.Assert; +import nonapi.io.github.classgraph.utils.CollectionUtils; import nonapi.io.github.classgraph.utils.FileUtils; import nonapi.io.github.classgraph.utils.JarUtils; import nonapi.io.github.classgraph.utils.LogNode; -/** The result of a scan. */ -public final class ScanResult implements Closeable, AutoCloseable { +/** + * The result of a scan. You should assign a ScanResult in a try-with-resources block, or manually close it when you + * have finished with the result of a scan. + */ +public final class ScanResult implements Closeable { /** The order of raw classpath elements. */ private List rawClasspathEltOrderStrs; - /** The order of classpath elements, after inner jars have been extracted to temporary files, etc. */ + /** + * The order of classpath elements, after inner jars have been extracted to temporary files, etc. + */ private List classpathOrder; - /** A list of all files that were found in whitelisted packages. */ - private ResourceList allWhitelistedResourcesCached; + /** A list of all files that were found in accepted packages. */ + private ResourceList allAcceptedResourcesCached; + + /** + * The number of times {@link #getResourcesWithPath(String)} has been called. + */ + private final AtomicInteger getResourcesWithPathCallCount = new AtomicInteger(); /** * The map from path (relative to package root) to a list of {@link Resource} elements with the matching path. */ - private Map pathToWhitelistedResourcesCached; + private Map pathToAcceptedResourcesCached; /** The map from class name to {@link ClassInfo}. */ - private Map classNameToClassInfo; + Map classNameToClassInfo; /** The map from package name to {@link PackageInfo}. */ private Map packageNameToPackageInfo; @@ -88,18 +107,16 @@ public final class ScanResult implements Closeable, AutoCloseable { */ private Map fileToLastModified; - /** If true, this {@link ScanResult} was produced by {@link ScanResult#fromJSON(String)}. */ + /** + * If true, this {@link ScanResult} was produced by {@link ScanResult#fromJSON(String)}. + */ private boolean isObtainedFromDeserialization; /** A custom ClassLoader that can load classes found during the scan. */ private ClassGraphClassLoader classGraphClassLoader; - /** - * The default order in which ClassLoaders are called to load classes. Used when a specific class does not have - * a record of which ClassLoader provided the URL used to locate the class (e.g. if the class is found using - * java.class.path). - */ - ClassLoader[] envClassLoaderOrder; + /** The {@link ClasspathFinder}. */ + ClasspathFinder classpathFinder; /** The nested jar handler instance. */ private NestedJarHandler nestedJarHandler; @@ -110,6 +127,8 @@ public final class ScanResult implements Closeable, AutoCloseable { /** If true, this ScanResult has already been closed. */ private final AtomicBoolean closed = new AtomicBoolean(false); + protected ReflectionUtils reflectionUtils; + /** The toplevel log. */ private final LogNode topLevelLog; @@ -122,15 +141,20 @@ public final class ScanResult implements Closeable, AutoCloseable { * The set of WeakReferences to non-closed ScanResult objects. Uses WeakReferences so that garbage collection is * not blocked. (Bug #233) */ - private static final Set> nonClosedWeakReferences = Collections + private static Set> nonClosedWeakReferences = Collections .newSetFromMap(new ConcurrentHashMap, Boolean>()); + /** If true, ScanResult#staticInit() has been run. */ + private static final AtomicBoolean initialized = new AtomicBoolean(false); + // ------------------------------------------------------------------------------------------------------------- /** The current serialization format. */ - private static final String CURRENT_SERIALIZATION_FORMAT = "9"; + private static final String CURRENT_SERIALIZATION_FORMAT = "10"; - /** A class to hold a serialized ScanResult along with the ScanSpec that was used to scan. */ + /** + * A class to hold a serialized ScanResult along with the ScanSpec that was used to scan. + */ private static class SerializationFormat { /** The serialization format. */ public String format; @@ -187,23 +211,27 @@ public SerializationFormat(final String serializationFormatStr, final ScanSpec s } // ------------------------------------------------------------------------------------------------------------- - // Shutdown hook - - static { - // Add runtime shutdown hook to remove temporary files on Ctrl-C or System.exit(). - Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { - for (final WeakReference nonClosedWeakReference : new ArrayList<>( - nonClosedWeakReferences)) { - final ScanResult scanResult = nonClosedWeakReference.get(); - if (scanResult != null) { - scanResult.close(); - } - nonClosedWeakReferences.remove(nonClosedWeakReference); - } - } - }); + // Shutdown hook init code + + /** + * Static initialization (warm up classloading), called when the ClassGraph class is initialized. + */ + static void init(final ReflectionUtils reflectionUtils) { + if (!initialized.getAndSet(true)) { + // Pre-load non-system classes necessary for calling scanResult.close(), so that + // classes that need + // to be loaded to close resources are already loaded and cached. This was + // originally for use in + // a shutdown hook (#331), which has now been removed, but it is probably still + // a good idea to + // ensure that classes needed to unmap DirectByteBuffer instances are available + // at init. + // We achieve this by mmap'ing a file and then closing it, since the only + // problematic classes are + // the PriviledgedAction anonymous inner classes used by + // FileUtils::closeDirectByteBuffer. + FileUtils.closeDirectByteBuffer(ByteBuffer.allocateDirect(32), reflectionUtils, /* log = */ null); + } } // ------------------------------------------------------------------------------------------------------------- @@ -218,8 +246,8 @@ public void run() { * the classpath order * @param rawClasspathEltOrderStrs * the raw classpath element order - * @param envClassLoaderOrder - * the environment classloader order + * @param classpathFinder + * the {@link ClasspathFinder} * @param classNameToClassInfo * a map from class name to class info * @param packageNameToPackageInfo @@ -234,7 +262,7 @@ public void run() { * the toplevel log */ ScanResult(final ScanSpec scanSpec, final List classpathOrder, - final List rawClasspathEltOrderStrs, final ClassLoader[] envClassLoaderOrder, + final List rawClasspathEltOrderStrs, final ClasspathFinder classpathFinder, final Map classNameToClassInfo, final Map packageNameToPackageInfo, final Map moduleNameToModuleInfo, final Map fileToLastModified, @@ -242,16 +270,17 @@ public void run() { this.scanSpec = scanSpec; this.rawClasspathEltOrderStrs = rawClasspathEltOrderStrs; this.classpathOrder = classpathOrder; - this.envClassLoaderOrder = envClassLoaderOrder; + this.classpathFinder = classpathFinder; this.fileToLastModified = fileToLastModified; this.classNameToClassInfo = classNameToClassInfo; this.packageNameToPackageInfo = packageNameToPackageInfo; this.moduleNameToModuleInfo = moduleNameToModuleInfo; this.nestedJarHandler = nestedJarHandler; + this.reflectionUtils = nestedJarHandler.reflectionUtils; this.topLevelLog = topLevelLog; if (classNameToClassInfo != null) { - indexResourcesAndClassInfo(); + indexResourcesAndClassInfo(topLevelLog); } if (classNameToClassInfo != null) { @@ -291,35 +320,48 @@ public void run() { nonClosedWeakReferences.add(this.weakReference); } - /** Index {@link Resource} and {@link ClassInfo} objects. */ - private void indexResourcesAndClassInfo() { + /** + * Index {@link Resource} and {@link ClassInfo} objects. + * + * @param log + * the log + */ + private void indexResourcesAndClassInfo(final LogNode log) { // Add backrefs from Info objects back to this ScanResult final Collection allClassInfo = classNameToClassInfo.values(); for (final ClassInfo classInfo : allClassInfo) { classInfo.setScanResult(this); } - // If inter-class dependencies are enabled, create placeholder ClassInfo objects for any referenced + // If inter-class dependencies are enabled, create placeholder ClassInfo objects + // for any referenced // classes that were not scanned if (scanSpec.enableInterClassDependencies) { for (final ClassInfo ci : new ArrayList<>(classNameToClassInfo.values())) { - final Set refdClasses = new HashSet<>(); - for (final String refdClassName : ci.findReferencedClassNames()) { - // Don't add circular dependencies - if (!ci.getName().equals(refdClassName)) { - // Get ClassInfo object for the named class, or create one if it doesn't exist - final ClassInfo refdClassInfo = ClassInfo.getOrCreateClassInfo(refdClassName, - /* classModifiers are unknown */ 0, classNameToClassInfo); + final Set refdClassesFiltered = new HashSet<>(); + for (final ClassInfo refdClassInfo : ci.findReferencedClassInfo(log)) { + // Don't add self-references, or references to Object + if (refdClassInfo != null && !ci.equals(refdClassInfo) + && !refdClassInfo.getName().equals("java.lang.Object") + // Only add class to result if it is accepted, or external classes are enabled + && (!refdClassInfo.isExternalClass() || scanSpec.enableExternalClasses)) { refdClassInfo.setScanResult(this); - if (!refdClassInfo.isExternalClass() || scanSpec.enableExternalClasses) { - // Only add class to result if it is whitelisted, or external classes are enabled - refdClasses.add(refdClassInfo); - } + refdClassesFiltered.add(refdClassInfo); } } - ci.setReferencedClasses(new ClassInfoList(refdClasses, /* sortByName = */ true)); + ci.setReferencedClasses(new ClassInfoList(refdClassesFiltered, /* sortByName = */ true)); } } + + if (scanSpec.enableClassInfo) { + for (final PackageInfo pkgInfo : packageNameToPackageInfo.values()) { + pkgInfo.setScanResult(this); + } + + for (final ModuleInfo moduleInfo : moduleNameToModuleInfo.values()) { + moduleInfo.setScanResult(this); + } + } } // ------------------------------------------------------------------------------------------------------------- @@ -371,9 +413,10 @@ public List getClasspathURIs() { final List classpathElementOrderURIs = new ArrayList<>(); for (final ClasspathElement classpathElement : classpathOrder) { try { - final URI uri = classpathElement.getURI(); - if (uri != null) { - classpathElementOrderURIs.add(uri); + for (final URI uri : classpathElement.getAllURIs()) { + if (uri != null) { + classpathElementOrderURIs.add(uri); + } } } catch (final IllegalArgumentException e) { // Skip null location URIs @@ -394,12 +437,9 @@ public List getClasspathURLs() { throw new IllegalArgumentException("Cannot use a ScanResult after it has been closed"); } final List classpathElementOrderURLs = new ArrayList<>(); - for (final ClasspathElement classpathElement : classpathOrder) { + for (final URI uri : getClasspathURIs()) { try { - final URI uri = classpathElement.getURI(); - if (uri != null) { - classpathElementOrderURLs.add(uri.toURL()); - } + classpathElementOrderURLs.add(uri.toURL()); } catch (final IllegalArgumentException | MalformedURLException e) { // Skip "jrt:" URIs and malformed URLs } @@ -438,6 +478,7 @@ public List getModules() { * @return The {@link ModulePathInfo}. */ public ModulePathInfo getModulePathInfo() { + scanSpec.modulePathInfo.getRuntimeInfo(reflectionUtils); return scanSpec.modulePathInfo; } @@ -447,85 +488,105 @@ public ModulePathInfo getModulePathInfo() { /** * Get the list of all resources. * - * @return A list of all resources (including classfiles and non-classfiles) found in whitelisted packages. + * @return A list of all resources (including classfiles and non-classfiles) found in accepted packages. */ public ResourceList getAllResources() { - if (allWhitelistedResourcesCached == null) { - // Index Resource objects by path - final ResourceList whitelistedResourcesList = new ResourceList(); - for (final ClasspathElement classpathElt : classpathOrder) { - if (classpathElt.whitelistedResources != null) { - whitelistedResourcesList.addAll(classpathElt.whitelistedResources); + synchronized (this) { + if (allAcceptedResourcesCached == null) { + // Index Resource objects by path + final ResourceList acceptedResourcesList = new ResourceList(); + for (final ClasspathElement classpathElt : classpathOrder) { + acceptedResourcesList.addAll(classpathElt.acceptedResources); } + // Set atomically for thread safety + allAcceptedResourcesCached = acceptedResourcesList; } - // Set atomically for thread safety - allWhitelistedResourcesCached = whitelistedResourcesList; + return allAcceptedResourcesCached; } - return allWhitelistedResourcesCached; } /** * Get a map from resource path to {@link Resource} for all resources (including classfiles and non-classfiles) - * found in whitelisted packages. + * found in accepted packages. * * @return The map from resource path to {@link Resource} for all resources (including classfiles and - * non-classfiles) found in whitelisted packages. + * non-classfiles) found in accepted packages. */ public Map getAllResourcesAsMap() { - if (pathToWhitelistedResourcesCached == null) { - final Map pathToWhitelistedResourceListMap = new HashMap<>(); - for (final Resource res : getAllResources()) { - ResourceList resList = pathToWhitelistedResourceListMap.get(res.getPath()); - if (resList == null) { - pathToWhitelistedResourceListMap.put(res.getPath(), resList = new ResourceList()); + synchronized (this) { + if (pathToAcceptedResourcesCached == null) { + final Map pathToAcceptedResourceListMap = new HashMap<>(); + for (final Resource res : getAllResources()) { + ResourceList resList = pathToAcceptedResourceListMap.get(res.getPath()); + if (resList == null) { + pathToAcceptedResourceListMap.put(res.getPath(), resList = new ResourceList()); + } + resList.add(res); } - resList.add(res); + // Set atomically for thread safety + pathToAcceptedResourcesCached = pathToAcceptedResourceListMap; } - // Set atomically for thread safety - pathToWhitelistedResourcesCached = pathToWhitelistedResourceListMap; + return pathToAcceptedResourcesCached; } - return pathToWhitelistedResourcesCached; } /** - * Get the list of all resources found in whitelisted packages that have the given path, relative to the package + * Get the list of all resources found in accepted packages that have the given path, relative to the package * root of the classpath element. May match several resources, up to one per classpath element. * * @param resourcePath * A complete resource path, relative to the classpath entry package root. - * @return A list of all resources found in whitelisted packages that have the given path, relative to the - * package root of the classpath element. May match several resources, up to one per classpath element. + * @return A list of all resources found in accepted packages that have the given path, relative to the package + * root of the classpath element. May match several resources, up to one per classpath element. */ public ResourceList getResourcesWithPath(final String resourcePath) { if (closed.get()) { throw new IllegalArgumentException("Cannot use a ScanResult after it has been closed"); } - final ResourceList allWhitelistedResources = getAllResources(); - if (allWhitelistedResources.isEmpty()) { - return ResourceList.EMPTY_LIST; + final String path = FileUtils.sanitizeEntryPath(resourcePath, /* removeInitialSlash = */ true, + /* removeFinalSlash = */ true); + ResourceList matchingResources = null; + if (getResourcesWithPathCallCount.incrementAndGet() > 3) { + // If numerous calls are made, produce and cache a single HashMap for O(1) + // access time + matchingResources = getAllResourcesAsMap().get(path); } else { - final String path = FileUtils.sanitizeEntryPath(resourcePath, /* removeInitialSlash = */ true); - final ResourceList resourceList = getAllResourcesAsMap().get(path); - return (resourceList == null ? new ResourceList(1) : resourceList); + // If just a few calls are made, directly search for resource with the requested + // path + for (final ClasspathElement classpathElt : classpathOrder) { + for (final Resource res : classpathElt.acceptedResources) { + if (res.getPath().equals(path)) { + if (matchingResources == null) { + matchingResources = new ResourceList(); + } + matchingResources.add(res); + } + } + } } + return matchingResources == null ? ResourceList.EMPTY_LIST : matchingResources; } /** - * Get the list of all resources found in any classpath element, whether in whitelisted packages or not (as - * long as the resource is not blacklisted), that have the given path, relative to the package root of the - * classpath element. May match several resources, up to one per classpath element. + * Get the list of all resources found in any classpath element, whether in accepted packages or not (as long + * as the resource is not rejected), that have the given path, relative to the package root of the classpath + * element. May match several resources, up to one per classpath element. Note that this may not return a + * non-accepted resource, particularly when scanning directory classpath elements, because recursive scanning + * terminates once there are no possible accepted resources below a given directory. However, resources in + * ancestral directories of accepted directories can be found using this method. * * @param resourcePath * A complete resource path, relative to the classpath entry package root. - * @return A list of all resources found in any classpath element, whether in whitelisted packages or not (as - * long as the resource is not blacklisted), that have the given path, relative to the package root - * of the classpath element. May match several resources, up to one per classpath element. + * @return A list of all resources found in any classpath element, whether in accepted packages or not (as + * long as the resource is not rejected), that have the given path, relative to the package root of + * the classpath element. May match several resources, up to one per classpath element. */ - public ResourceList getResourcesWithPathIgnoringWhitelist(final String resourcePath) { + public ResourceList getResourcesWithPathIgnoringAccept(final String resourcePath) { if (closed.get()) { throw new IllegalArgumentException("Cannot use a ScanResult after it has been closed"); } - final String path = FileUtils.sanitizeEntryPath(resourcePath, /* removeInitialSlash = */ true); + final String path = FileUtils.sanitizeEntryPath(resourcePath, /* removeInitialSlash = */ true, + /* removeFinalSlash = */ true); final ResourceList matchingResources = new ResourceList(); for (final ClasspathElement classpathElt : classpathOrder) { final Resource matchingResource = classpathElt.getResource(path); @@ -537,22 +598,37 @@ public ResourceList getResourcesWithPathIgnoringWhitelist(final String resourceP } /** - * Get the list of all resources found in whitelisted packages that have the requested leafname. + * Use {@link #getResourcesWithPathIgnoringAccept(String)} instead. + * + * @param resourcePath + * A complete resource path, relative to the classpath entry package root. + * @return A list of all resources found in any classpath element, whether in accepted packages or not (as + * long as the resource is not rejected), that have the given path, relative to the package root of + * the classpath element. May match several resources, up to one per classpath element. + * @deprecated Use {@link #getResourcesWithPathIgnoringAccept(String)} instead. + */ + @Deprecated + public ResourceList getResourcesWithPathIgnoringWhitelist(final String resourcePath) { + return getResourcesWithPathIgnoringAccept(resourcePath); + } + + /** + * Get the list of all resources found in accepted packages that have the requested leafname. * * @param leafName * A resource leaf filename. - * @return A list of all resources found in whitelisted packages that have the requested leafname. + * @return A list of all resources found in accepted packages that have the requested leafname. */ public ResourceList getResourcesWithLeafName(final String leafName) { if (closed.get()) { throw new IllegalArgumentException("Cannot use a ScanResult after it has been closed"); } - final ResourceList allWhitelistedResources = getAllResources(); - if (allWhitelistedResources.isEmpty()) { + final ResourceList allAcceptedResources = getAllResources(); + if (allAcceptedResources.isEmpty()) { return ResourceList.EMPTY_LIST; } else { final ResourceList filteredResources = new ResourceList(); - for (final Resource classpathResource : allWhitelistedResources) { + for (final Resource classpathResource : allAcceptedResources) { final String relativePath = classpathResource.getPath(); final int lastSlashIdx = relativePath.lastIndexOf('/'); if (relativePath.substring(lastSlashIdx + 1).equals(leafName)) { @@ -564,18 +640,18 @@ public ResourceList getResourcesWithLeafName(final String leafName) { } /** - * Get the list of all resources found in whitelisted packages that have the requested filename extension. + * Get the list of all resources found in accepted packages that have the requested filename extension. * * @param extension * A filename extension, e.g. "xml" to match all resources ending in ".xml". - * @return A list of all resources found in whitelisted packages that have the requested filename extension. + * @return A list of all resources found in accepted packages that have the requested filename extension. */ public ResourceList getResourcesWithExtension(final String extension) { if (closed.get()) { throw new IllegalArgumentException("Cannot use a ScanResult after it has been closed"); } - final ResourceList allWhitelistedResources = getAllResources(); - if (allWhitelistedResources.isEmpty()) { + final ResourceList allAcceptedResources = getAllResources(); + if (allAcceptedResources.isEmpty()) { return ResourceList.EMPTY_LIST; } else { String bareExtension = extension; @@ -583,7 +659,7 @@ public ResourceList getResourcesWithExtension(final String extension) { bareExtension = bareExtension.substring(1); } final ResourceList filteredResources = new ResourceList(); - for (final Resource classpathResource : allWhitelistedResources) { + for (final Resource classpathResource : allAcceptedResources) { final String relativePath = classpathResource.getPath(); final int lastSlashIdx = relativePath.lastIndexOf('/'); final int lastDotIdx = relativePath.lastIndexOf('.'); @@ -597,23 +673,23 @@ public ResourceList getResourcesWithExtension(final String extension) { } /** - * Get the list of all resources found in whitelisted packages that have a path matching the requested pattern. + * Get the list of all resources found in accepted packages that have a path matching the requested regex + * pattern. See also {{@link #getResourcesMatchingWildcard(String)}. * * @param pattern * A pattern to match {@link Resource} paths with. - * @return A list of all resources found in whitelisted packages that have a path matching the requested - * pattern. + * @return A list of all resources found in accepted packages that have a path matching the requested pattern. */ public ResourceList getResourcesMatchingPattern(final Pattern pattern) { if (closed.get()) { throw new IllegalArgumentException("Cannot use a ScanResult after it has been closed"); } - final ResourceList allWhitelistedResources = getAllResources(); - if (allWhitelistedResources.isEmpty()) { + final ResourceList allAcceptedResources = getAllResources(); + if (allAcceptedResources.isEmpty()) { return ResourceList.EMPTY_LIST; } else { final ResourceList filteredResources = new ResourceList(); - for (final Resource classpathResource : allWhitelistedResources) { + for (final Resource classpathResource : allAcceptedResources) { final String relativePath = classpathResource.getPath(); if (pattern.matcher(relativePath).matches()) { filteredResources.add(classpathResource); @@ -623,6 +699,36 @@ public ResourceList getResourcesMatchingPattern(final Pattern pattern) { } } + /** + * Get the list of all resources found in accepted packages that have a path matching the requested wildcard + * string. + * + *

+ * The wildcard string may contain: + *

    + *
  • Single asterisks, to match zero or more of any character other than '/'
  • + *
  • Double asterisks, to match zero or more of any character
  • + *
  • Question marks, to match one character
  • + *
  • Any other regexp-style syntax, such as character sets (denoted by square brackets) -- the remainder of + * the expression is passed through to the Java regex parser, after escaping dot characters.
  • + *
+ * + *

+ * The wildcard string is translated in a simplistic way into a regex. If you need more complex pattern + * matching, use a regex directly, via {@link #getResourcesMatchingPattern(Pattern)}. + * + * @param wildcardString + * A wildcard (glob) pattern to match {@link Resource} paths with. + * @return A list of all resources found in accepted packages that have a path matching the requested wildcard + * string. + */ + public ResourceList getResourcesMatchingWildcard(final String wildcardString) { + if (closed.get()) { + throw new IllegalArgumentException("Cannot use a ScanResult after it has been closed"); + } + return getResourcesMatchingPattern(AcceptReject.globToPattern(wildcardString, /* simpleGlob = */ false)); + } + // ------------------------------------------------------------------------------------------------------------- // Modules @@ -699,15 +805,15 @@ public PackageInfoList getPackageInfo() { // Class dependencies /** - * Get a map from the {@link ClassInfo} object for each whitelisted class to a list of the classes referenced by + * Get a map from the {@link ClassInfo} object for each accepted class to a list of the classes referenced by * that class (i.e. returns a map from dependents to dependencies). Note that you need to call * {@link ClassGraph#enableInterClassDependencies()} before {@link ClassGraph#scan()} for this method to work. * You should also call {@link ClassGraph#enableExternalClasses()} before {@link ClassGraph#scan()} if you want - * non-whitelisted classes to appear in the result. See also {@link #getReverseClassDependencyMap()}, which - * inverts the map. + * non-accepted classes to appear in the result. See also {@link #getReverseClassDependencyMap()}, which inverts + * the map. * - * @return A map from a {@link ClassInfo} object for each whitelisted class to a list of the classes referenced - * by that class (i.e. returns a map from dependents to dependencies). Each map value is the result of + * @return A map from a {@link ClassInfo} object for each accepted class to a list of the classes referenced by + * that class (i.e. returns a map from dependents to dependencies). Each map value is the result of * calling {@link ClassInfo#getClassDependencies()} on the corresponding key. */ public Map getClassDependencyMap() { @@ -720,15 +826,15 @@ public Map getClassDependencyMap() { /** * Get the reverse class dependency map, i.e. a map from the {@link ClassInfo} object for each dependency class - * (whitelisted or not) to a list of the whitelisted classes that referenced that class as a dependency (i.e. - * returns a map from dependencies to dependents). Note that you need to call + * (accepted or not) to a list of the accepted classes that referenced that class as a dependency (i.e. returns + * a map from dependencies to dependents). Note that you need to call * {@link ClassGraph#enableInterClassDependencies()} before {@link ClassGraph#scan()} for this method to work. * You should also call {@link ClassGraph#enableExternalClasses()} before {@link ClassGraph#scan()} if you want - * non-whitelisted classes to appear in the result. See also {@link #getClassDependencyMap}. + * non-accepted classes to appear in the result. See also {@link #getClassDependencyMap}. * - * @return A map from a {@link ClassInfo} object for each dependency class (whitelisted or not) to a list of the - * whitelisted classes that referenced that class as a dependency (i.e. returns a map from dependencies - * to dependents). + * @return A map from a {@link ClassInfo} object for each dependency class (accepted or not) to a list of the + * accepted classes that referenced that class as a dependency (i.e. returns a map from dependencies to + * dependents). */ public Map getReverseClassDependencyMap() { final Map> revMapSet = new HashMap<>(); @@ -736,7 +842,7 @@ public Map getReverseClassDependencyMap() { for (final ClassInfo dep : ci.getClassDependencies()) { Set set = revMapSet.get(dep); if (set == null) { - revMapSet.put(dep, set = new HashSet()); + revMapSet.put(dep, set = new HashSet<>()); } set.add(ci); } @@ -753,7 +859,7 @@ public Map getReverseClassDependencyMap() { /** * Get the {@link ClassInfo} object for the named class, or null if no class of the requested name was found in - * a whitelisted/non-blacklisted package during the scan. + * an accepted/non-rejected package during the scan. * * @param className * The class name. @@ -772,7 +878,7 @@ public ClassInfo getClassInfo(final String className) { /** * Get all classes, interfaces and annotations found during the scan. * - * @return A list of all whitelisted classes found during the scan, or the empty list if none. + * @return A list of all accepted classes found during the scan, or the empty list if none. */ public ClassInfoList getAllClasses() { if (closed.get()) { @@ -784,6 +890,36 @@ public ClassInfoList getAllClasses() { return ClassInfo.getAllClasses(classNameToClassInfo.values(), scanSpec); } + /** + * Get all {@link Enum} classes found during the scan. + * + * @return A list of all {@link Enum} classes found during the scan, or the empty list if none. + */ + public ClassInfoList getAllEnums() { + if (closed.get()) { + throw new IllegalArgumentException("Cannot use a ScanResult after it has been closed"); + } + if (!scanSpec.enableClassInfo) { + throw new IllegalArgumentException("Please call ClassGraph#enableClassInfo() before #scan()"); + } + return ClassInfo.getAllEnums(classNameToClassInfo.values(), scanSpec); + } + + /** + * Get all {@code record} classes found during the scan (JDK 14+). + * + * @return A list of all {@code record} classes found during the scan, or the empty list if none. + */ + public ClassInfoList getAllRecords() { + if (closed.get()) { + throw new IllegalArgumentException("Cannot use a ScanResult after it has been closed"); + } + if (!scanSpec.enableClassInfo) { + throw new IllegalArgumentException("Please call ClassGraph#enableClassInfo() before #scan()"); + } + return ClassInfo.getAllRecords(classNameToClassInfo.values(), scanSpec); + } + /** * Get a map from class name to {@link ClassInfo} object for all classes, interfaces and annotations found * during the scan. @@ -804,7 +940,7 @@ public Map getAllClassesAsMap() { /** * Get all standard (non-interface/non-annotation) classes found during the scan. * - * @return A list of all whitelisted standard classes found during the scan, or the empty list if none. + * @return A list of all accepted standard classes found during the scan, or the empty list if none. */ public ClassInfoList getAllStandardClasses() { if (closed.get()) { @@ -816,6 +952,17 @@ public ClassInfoList getAllStandardClasses() { return ClassInfo.getAllStandardClasses(classNameToClassInfo.values(), scanSpec); } + /** + * Get all subclasses of the superclass. + * + * @param superclass + * The superclass. + * @return A list of subclasses of the superclass, or the empty list if none. + */ + public ClassInfoList getSubclasses(final Class superclass) { + return getSubclasses(superclass.getName()); + } + /** * Get all subclasses of the named superclass. * @@ -857,6 +1004,29 @@ public ClassInfoList getSuperclasses(final String subclassName) { return subclass == null ? ClassInfoList.EMPTY_LIST : subclass.getSuperclasses(); } + /** + * Get superclasses of the subclass. + * + * @param subclass + * The subclass. + * @return A list of superclasses of the named subclass, or the empty list if none. + */ + public ClassInfoList getSuperclasses(final Class subclass) { + return getSuperclasses(subclass.getName()); + } + + /** + * Get classes that have a method with an annotation of the named type. + * + * @param methodAnnotation + * the method annotation. + * @return A list of classes with a method that has an annotation of the named type, or the empty list if none. + */ + public ClassInfoList getClassesWithMethodAnnotation(final Class methodAnnotation) { + Assert.isAnnotation(methodAnnotation); + return getClassesWithMethodAnnotation(methodAnnotation.getName()); + } + /** * Get classes that have a method with an annotation of the named type. * @@ -876,6 +1046,20 @@ public ClassInfoList getClassesWithMethodAnnotation(final String methodAnnotatio return classInfo == null ? ClassInfoList.EMPTY_LIST : classInfo.getClassesWithMethodAnnotation(); } + /** + * Get classes that have a method with a parameter that is annotated with an annotation of the named type. + * + * @param methodParameterAnnotation + * the method parameter annotation. + * @return A list of classes that have a method with a parameter annotated with the named annotation type, or + * the empty list if none. + */ + public ClassInfoList getClassesWithMethodParameterAnnotation( + final Class methodParameterAnnotation) { + Assert.isAnnotation(methodParameterAnnotation); + return getClassesWithMethodParameterAnnotation(methodParameterAnnotation.getName()); + } + /** * Get classes that have a method with a parameter that is annotated with an annotation of the named type. * @@ -896,6 +1080,18 @@ public ClassInfoList getClassesWithMethodParameterAnnotation(final String method return classInfo == null ? ClassInfoList.EMPTY_LIST : classInfo.getClassesWithMethodParameterAnnotation(); } + /** + * Get classes that have a field with an annotation of the named type. + * + * @param fieldAnnotation + * the field annotation. + * @return A list of classes that have a field with an annotation of the named type, or the empty list if none. + */ + public ClassInfoList getClassesWithFieldAnnotation(final Class fieldAnnotation) { + Assert.isAnnotation(fieldAnnotation); + return getClassesWithFieldAnnotation(fieldAnnotation.getName()); + } + /** * Get classes that have a field with an annotation of the named type. * @@ -922,7 +1118,7 @@ public ClassInfoList getClassesWithFieldAnnotation(final String fieldAnnotationN * Get all interface classes found during the scan (not including annotations, which are also technically * interfaces). See also {@link #getAllInterfacesAndAnnotations()}. * - * @return A list of all whitelisted interfaces found during the scan, or the empty list if none. + * @return A list of all accepted interfaces found during the scan, or the empty list if none. */ public ClassInfoList getAllInterfaces() { if (closed.get()) { @@ -935,8 +1131,8 @@ public ClassInfoList getAllInterfaces() { } /** - * Get all interfaces implemented by the named class or by one of its superclasses, if this is a standard class, - * or the superinterfaces extended by this interface, if this is an interface. + * Get all interfaces implemented by the named class or by one of its superclasses, if the named class is a + * standard class, or the superinterfaces extended by this interface, if it is an interface. * * @param className * The class name. @@ -954,6 +1150,32 @@ public ClassInfoList getInterfaces(final String className) { return classInfo == null ? ClassInfoList.EMPTY_LIST : classInfo.getInterfaces(); } + /** + * Get all interfaces implemented by the class or by one of its superclasses, if the given class is a standard + * class, or the superinterfaces extended by this interface, if it is an interface. + * + * @param classRef + * The class. + * @return A list of interfaces implemented by the given class (or superinterfaces extended by the given + * interface), or the empty list if none. + */ + public ClassInfoList getInterfaces(final Class classRef) { + return getInterfaces(classRef.getName()); + } + + /** + * Get all classes that implement (or have superclasses that implement) the interface (or one of its + * subinterfaces). + * + * @param interfaceClass + * The interface class. + * @return A list of all classes that implement the interface, or the empty list if none. + */ + public ClassInfoList getClassesImplementing(final Class interfaceClass) { + Assert.isInterface(interfaceClass); + return getClassesImplementing(interfaceClass.getName()); + } + /** * Get all classes that implement (or have superclasses that implement) the named interface (or one of its * subinterfaces). @@ -996,7 +1218,7 @@ public ClassInfoList getAllAnnotations() { * Get all interface or annotation classes found during the scan. (Annotations are technically interfaces, and * they can be implemented.) * - * @return A list of all whitelisted interfaces found during the scan, or the empty list if none. + * @return A list of all accepted interfaces found during the scan, or the empty list if none. */ public ClassInfoList getAllInterfacesAndAnnotations() { if (closed.get()) { @@ -1009,6 +1231,55 @@ public ClassInfoList getAllInterfacesAndAnnotations() { return ClassInfo.getAllInterfacesOrAnnotationClasses(classNameToClassInfo.values(), scanSpec); } + /** + * Get classes with the class annotation or meta-annotation. + * + * @param annotation + * The class annotation or meta-annotation. + * @return A list of all non-annotation classes that were found with the class annotation during the scan, or + * the empty list if none. + */ + public ClassInfoList getClassesWithAnnotation(final Class annotation) { + Assert.isAnnotation(annotation); + return getClassesWithAnnotation(annotation.getName()); + } + + /** + * Get classes with all of the specified class annotations or meta-annotation. + * + * @param annotations + * The class annotations or meta-annotations. + * @return A list of all non-annotation classes that were found with any of the class annotations during the + * scan, or the empty list if none. + */ + @SuppressWarnings("unchecked") + public ClassInfoList getClassesWithAllAnnotations(final Class... annotations) { + final List annotationNames = new ArrayList<>(); + for (final Class cls : annotations) { + Assert.isAnnotation(cls); + annotationNames.add(cls.getName()); + } + return getClassesWithAllAnnotations(annotationNames.toArray(new String[0])); + } + + /** + * Get classes with any of the specified class annotations or meta-annotation. + * + * @param annotations + * The class annotations or meta-annotations. + * @return A list of all non-annotation classes that were found with any of the class annotations during the + * scan, or the empty list if none. + */ + @SuppressWarnings("unchecked") + public ClassInfoList getClassesWithAnyAnnotation(final Class... annotations) { + final List annotationNames = new ArrayList<>(); + for (final Class cls : annotations) { + Assert.isAnnotation(cls); + annotationNames.add(cls.getName()); + } + return getClassesWithAnyAnnotation(annotationNames.toArray(new String[0])); + } + /** * Get classes with the named class annotation or meta-annotation. * @@ -1029,6 +1300,50 @@ public ClassInfoList getClassesWithAnnotation(final String annotationName) { return classInfo == null ? ClassInfoList.EMPTY_LIST : classInfo.getClassesWithAnnotation(); } + /** + * Get classes with all of the named class annotations or meta-annotation. + * + * @param annotationNames + * The name of the class annotations or meta-annotations. + * @return A list of all non-annotation classes that were found with all of the named class annotations during + * the scan, or the empty list if none. + */ + public ClassInfoList getClassesWithAllAnnotations(final String... annotationNames) { + ClassInfoList foundClassInfo = null; + for (final String annotationName : annotationNames) { + final ClassInfoList classInfoList = getClassesWithAnnotation(annotationName); + if (foundClassInfo == null) { + foundClassInfo = classInfoList; + } else { + foundClassInfo = foundClassInfo.intersect(classInfoList); + } + } + CollectionUtils.sortIfNotEmpty(foundClassInfo); + return foundClassInfo == null ? ClassInfoList.EMPTY_LIST : foundClassInfo; + } + + /** + * Get classes with any of the named class annotations or meta-annotation. + * + * @param annotationNames + * The name of the class annotations or meta-annotations. + * @return A list of all non-annotation classes that were found with any of the named class annotations during + * the scan, or the empty list if none. + */ + public ClassInfoList getClassesWithAnyAnnotation(final String... annotationNames) { + ClassInfoList foundClassInfo = null; + for (final String annotationName : annotationNames) { + final ClassInfoList classInfoList = getClassesWithAnnotation(annotationName); + if (foundClassInfo == null) { + foundClassInfo = classInfoList; + } else { + foundClassInfo = foundClassInfo.union(classInfoList); + } + } + CollectionUtils.sortIfNotEmpty(foundClassInfo); + return foundClassInfo == null ? ClassInfoList.EMPTY_LIST : foundClassInfo; + } + /** * Get annotations on the named class. This only returns the annotating classes; to read annotation parameters, * call {@link #getClassInfo(String)} to get the {@link ClassInfo} object for the named class, then if the @@ -1058,8 +1373,8 @@ public ClassInfoList getAnnotationsOnClass(final String className) { /** * Determine whether the classpath contents have been modified since the last scan. Checks the timestamps of * files and jarfiles encountered during the previous scan to see if they have changed. Does not perform a full - * scan, so cannot detect the addition of directories that newly match whitelist criteria -- you need to perform - * a full scan to detect those changes. + * scan, so cannot detect the addition of directories that newly match accept criteria -- you need to perform a + * full scan to detect those changes. * * @return true if the classpath contents have been modified since the last scan. */ @@ -1080,16 +1395,16 @@ public boolean classpathContentsModifiedSinceScan() { } /** - * Find the maximum last-modified timestamp of any whitelisted file/directory/jarfile encountered during the - * scan. Checks the current timestamps, so this should increase between calls if something changes in - * whitelisted paths. Assumes both file and system timestamps were generated from clocks whose time was - * accurate. Ignores timestamps greater than the system time. + * Find the maximum last-modified timestamp of any accepted file/directory/jarfile encountered during the scan. + * Checks the current timestamps, so this should increase between calls if something changes in accepted paths. + * Assumes both file and system timestamps were generated from clocks whose time was accurate. Ignores + * timestamps greater than the system time. * *

* This method cannot in general tell if classpath has changed (or modules have been added or removed) if it is * run twice during the same runtime session. * - * @return the maximum last-modified time for whitelisted files/directories/jars encountered during the scan. + * @return the maximum last-modified time for accepted files/directories/jars encountered during the scan. */ public long classpathContentsLastModifiedTime() { if (closed.get()) { @@ -1110,6 +1425,15 @@ public long classpathContentsLastModifiedTime() { // ------------------------------------------------------------------------------------------------------------- // Classloading + /** + * Get the ClassLoader order, respecting parent-first/parent-last delegation order. + * + * @return the class loader order. + */ + ClassLoader[] getClassLoaderOrderRespectingParentDelegation() { + return classpathFinder.getClassLoaderOrderRespectingParentDelegation(); + } + /** * Load a class given a class name. If ignoreExceptions is false, and the class cannot be loaded (due to * classloading error, or due to an exception being thrown in the class initialization block), an @@ -1146,7 +1470,7 @@ public Class loadClass(final String className, final boolean returnNullIfClas if (returnNullIfClassNotFound) { return null; } else { - throw new IllegalArgumentException("Could not load class " + className + " : " + e); + throw new IllegalArgumentException("Could not load class " + className + " : " + e, e); } } } @@ -1223,6 +1547,7 @@ public Class loadClass(final String className, final Class superclassO * The JSON string for the serialized {@link ScanResult}. * @return The deserialized {@link ScanResult}. */ + @SuppressWarnings("null") public static ScanResult fromJSON(final String json) { final Matcher matcher = Pattern.compile("\\{[\\n\\r ]*\"format\"[ ]?:[ ]?\"([^\"]+)\"").matcher(json); if (!matcher.find()) { @@ -1238,25 +1563,27 @@ public static ScanResult fromJSON(final String json) { // Deserialize the JSON final SerializationFormat deserialized = JSONDeserializer.deserializeObject(SerializationFormat.class, json); - if (!deserialized.format.equals(CURRENT_SERIALIZATION_FORMAT)) { - // Probably the deserialization failed before now anyway, if fields have changed, etc. + if (deserialized == null || !deserialized.format.equals(CURRENT_SERIALIZATION_FORMAT)) { + // Probably the deserialization failed before now anyway, if fields have + // changed, etc. throw new IllegalArgumentException("JSON was serialized by newer version of ClassGraph"); } - // Perform a new "scan" with performScan set to false, which resolves all the ClasspathElement objects - // and scans classpath element paths (needed for classloading), but does not scan the actual classfiles + // Perform a new "scan" with performScan set to false, which resolves all the + // ClasspathElement objects + // and scans classpath element paths (needed for classloading), but does not + // scan the actual classfiles final ClassGraph classGraph = new ClassGraph(); classGraph.scanSpec = deserialized.scanSpec; - classGraph.scanSpec.performScan = false; - if (classGraph.scanSpec.overrideClasspath == null) { - // Use the same classpath as before, if classpath was not overridden - classGraph.overrideClasspath(deserialized.classpath); + final ScanResult scanResult; + try (AutoCloseableExecutorService executorService = new AutoCloseableExecutorService( + ClassGraph.DEFAULT_NUM_WORKER_THREADS)) { + scanResult = classGraph.getClasspathScanResult(executorService); } - final ScanResult scanResult = classGraph.scan(); scanResult.rawClasspathEltOrderStrs = deserialized.classpath; - scanResult.scanSpec.performScan = true; - // Set the fields related to ClassInfo in the new ScanResult, based on the deserialized JSON + // Set the fields related to ClassInfo in the new ScanResult, based on the + // deserialized JSON scanResult.scanSpec = deserialized.scanSpec; scanResult.classNameToClassInfo = new HashMap<>(); if (deserialized.classInfo != null) { @@ -1278,8 +1605,8 @@ public static ScanResult fromJSON(final String json) { } } - // Index Resource and ClassInfo objects - scanResult.indexResourcesAndClassInfo(); + // Index Resource and ClassInfo objects + scanResult.indexResourcesAndClassInfo(/* log = */ null); scanResult.isObtainedFromDeserialization = true; return scanResult; @@ -1300,11 +1627,11 @@ public String toJSON(final int indentWidth) { throw new IllegalArgumentException("Please call ClassGraph#enableClassInfo() before #scan()"); } final List allClassInfo = new ArrayList<>(classNameToClassInfo.values()); - Collections.sort(allClassInfo); + CollectionUtils.sortIfNotEmpty(allClassInfo); final List allPackageInfo = new ArrayList<>(packageNameToPackageInfo.values()); - Collections.sort(allPackageInfo); + CollectionUtils.sortIfNotEmpty(allPackageInfo); final List allModuleInfo = new ArrayList<>(moduleNameToModuleInfo.values()); - Collections.sort(allModuleInfo); + CollectionUtils.sortIfNotEmpty(allModuleInfo); return JSONSerializer.serializeObject(new SerializationFormat(CURRENT_SERIALIZATION_FORMAT, scanSpec, allClassInfo, allPackageInfo, allModuleInfo, rawClasspathEltOrderStrs), indentWidth, false); } @@ -1339,25 +1666,30 @@ public boolean isObtainedFromDeserialization() { @Override public void close() { if (!closed.getAndSet(true)) { + nonClosedWeakReferences.remove(weakReference); if (classpathOrder != null) { classpathOrder.clear(); classpathOrder = null; } - if (allWhitelistedResourcesCached != null) { - for (final Resource classpathResource : allWhitelistedResourcesCached) { + if (allAcceptedResourcesCached != null) { + for (final Resource classpathResource : allAcceptedResourcesCached) { classpathResource.close(); } - allWhitelistedResourcesCached.clear(); - allWhitelistedResourcesCached = null; + allAcceptedResourcesCached.clear(); + allAcceptedResourcesCached = null; } - if (pathToWhitelistedResourcesCached != null) { - pathToWhitelistedResourcesCached.clear(); - pathToWhitelistedResourcesCached = null; + if (pathToAcceptedResourcesCached != null) { + pathToAcceptedResourcesCached.clear(); + pathToAcceptedResourcesCached = null; } classGraphClassLoader = null; if (classNameToClassInfo != null) { - classNameToClassInfo.clear(); - classNameToClassInfo = null; + // Don't clear classNameToClassInfo, since it may be used by + // ClassGraphClassLoader (#399). + // Just rely on the garbage collector to collect these once the ScanResult goes + // out of scope. + // classNameToClassInfo.clear(); + // classNameToClassInfo = null; } if (packageNameToPackageInfo != null) { packageNameToPackageInfo.clear(); @@ -1371,20 +1703,47 @@ public void close() { fileToLastModified.clear(); fileToLastModified = null; } - // nestedJarHandler should be closed last, since it needs to have all MappedByteBuffer refs - // dropped before it tries to delete any temporary files that were written to disk + // nestedJarHandler should be closed last, since it needs to have all + // MappedByteBuffer refs + // dropped before it tries to delete any temporary files that were written to + // disk if (nestedJarHandler != null) { nestedJarHandler.close(topLevelLog); nestedJarHandler = null; } classGraphClassLoader = null; - envClassLoaderOrder = null; - // Remove WeakReference to this ScanResult, so shutdown hook does not try to close this - nonClosedWeakReferences.remove(weakReference); - // Flush log on exit, in case additional log entries were generated after scan() completed + classpathFinder = null; + reflectionUtils = null; + // Flush log on exit, in case additional log entries were generated after scan() + // completed if (topLevelLog != null) { topLevelLog.flush(); } } } + + /** + * Returns whether this ScanResult has been closed yet or not. + * @return {@code true} if this ScanResult has been closed + */ + public boolean isClosed() { + return closed.get(); + } + + /** + * Close all {@link ScanResult} instances that have not yet been closed. Note that this will close all open + * {@link ScanResult} instances for any class that uses the classloader that the {@link ScanResult} class is + * cached in -- so if you call this method, you need to ensure that the lifecycle of the classloader matches the + * lifecycle of your application, or that two concurrent applications don't share the same classloader, + * otherwise one application might close another application's {@link ScanResult} instances while they are still + * in use. + */ + public static void closeAll() { + for (final WeakReference nonClosedWeakReference : new ArrayList<>(nonClosedWeakReferences)) { + final ScanResult scanResult = nonClosedWeakReference.get(); + if (scanResult != null) { + scanResult.close(); + } + } + } } diff --git a/src/main/java/io/github/classgraph/ScanResultObject.java b/src/main/java/io/github/classgraph/ScanResultObject.java index a4ab82e4f..20d577adc 100644 --- a/src/main/java/io/github/classgraph/ScanResultObject.java +++ b/src/main/java/io/github/classgraph/ScanResultObject.java @@ -29,13 +29,15 @@ package io.github.classgraph; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; +import nonapi.io.github.classgraph.utils.LogNode; + /** * A superclass of objects accessible from a {@link ScanResult} that are associated with a {@link ClassInfo} object. */ abstract class ScanResultObject { - /** The scan result. */ transient protected ScanResult scanResult; @@ -43,7 +45,9 @@ abstract class ScanResultObject { private transient ClassInfo classInfo; /** The class ref, once the class is loaded. */ - private transient Class classRef; + protected transient Class classRef; + + // ------------------------------------------------------------------------------------------------------------- /** * Set ScanResult backreferences in info objects after scan has completed. @@ -56,25 +60,39 @@ void setScanResult(final ScanResult scanResult) { } /** - * Get the names of all referenced classes. + * Get {@link ClassInfo} objects for any classes referenced by this object. * - * @return the referenced class names + * @param log + * the log + * @return the referenced class info. */ - Set findReferencedClassNames() { - final Set allReferencedClassNames = new LinkedHashSet<>(); - findReferencedClassNames(allReferencedClassNames); - // Remove references to java.lang.Object - allReferencedClassNames.remove("java.lang.Object"); - return allReferencedClassNames; + final Set findReferencedClassInfo(final LogNode log) { + final Set refdClassInfo = new LinkedHashSet<>(); + if (scanResult != null) { + findReferencedClassInfo(scanResult.classNameToClassInfo, refdClassInfo, log); + } + return refdClassInfo; } /** - * Get any class names referenced in type descriptors of this object. + * Get {@link ClassInfo} objects for any classes referenced by this object. * - * @param refdClassNames - * the referenced class names + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info + * @param log + * the log */ - abstract void findReferencedClassNames(Set refdClassNames); + protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + final ClassInfo ci = getClassInfo(); + if (ci != null) { + refdClassInfo.add(ci); + } + } + + // ------------------------------------------------------------------------------------------------------------- /** * The name of the class (used by {@link #getClassInfo()} to fetch the {@link ClassInfo} object for the class). @@ -111,7 +129,12 @@ ClassInfo getClassInfo() { */ private String getClassInfoNameOrClassName() { String className; - ClassInfo ci = getClassInfo(); + ClassInfo ci = null; + try { + ci = getClassInfo(); + } catch (final IllegalArgumentException e) { + // Just ignore wrong access to array classInfo + } if (ci == null) { ci = classInfo; } @@ -128,6 +151,8 @@ private String getClassInfoNameOrClassName() { return className; } + // ------------------------------------------------------------------------------------------------------------- + /** * Load the class named returned by {@link #getClassInfo()}, or if that returns null, the class named by * {@link #getClassName()}. Returns a {@code Class} reference for the class, cast to the requested superclass @@ -145,13 +170,28 @@ private String getClassInfoNameOrClassName() { * if the class could not be loaded or cast, and ignoreExceptions was false. */ Class loadClass(final Class superclassOrInterfaceType, final boolean ignoreExceptions) { - if (classRef == null) { - classRef = scanResult.loadClass(getClassInfoNameOrClassName(), superclassOrInterfaceType, - ignoreExceptions); + synchronized (this) { + // If class is not already loaded, try loading class + if (classRef == null) { + final String className = getClassInfoNameOrClassName(); + try { + classRef = scanResult != null + ? scanResult.loadClass(className, superclassOrInterfaceType, ignoreExceptions) + // Fallback, if scanResult is not set + : Class.forName(className); + if (classRef == null && !ignoreExceptions) { + throw new IllegalArgumentException("Could not load class " + className); + } + } catch (final Throwable t) { + if (!ignoreExceptions) { + throw new IllegalArgumentException("Could not load class " + className, t); + } + } + } + @SuppressWarnings("unchecked") + final Class classT = (Class) classRef; + return classT; } - @SuppressWarnings("unchecked") - final Class classT = (Class) classRef; - return classT; } /** @@ -185,7 +225,19 @@ Class loadClass(final Class superclassOrInterfaceType) { */ Class loadClass(final boolean ignoreExceptions) { if (classRef == null) { - classRef = scanResult.loadClass(getClassInfoNameOrClassName(), ignoreExceptions); + final String className = getClassInfoNameOrClassName(); + if (scanResult != null) { + classRef = scanResult.loadClass(className, ignoreExceptions); + } else { + // Fallback, if scanResult is not set + try { + classRef = Class.forName(className); + } catch (final Throwable t) { + if (!ignoreExceptions) { + throw new IllegalArgumentException("Could not load class " + className, t); + } + } + } } return classRef; } @@ -194,12 +246,61 @@ Class loadClass(final boolean ignoreExceptions) { * Load the class named returned by {@link #getClassInfo()}, or if that returns null, the class named by * {@link #getClassName()}. Returns a {@code Class} reference for the class. * - * @return The {@code Class} reference for the referenced class, or null if the class could not be loaded and - * ignoreExceptions is true. + * @return The {@code Class} reference for the referenced class. * @throws IllegalArgumentException - * if the class could not be loaded and ignoreExceptions was false. + * if the class could not be loaded. */ Class loadClass() { return loadClass(/* ignoreExceptions = */ false); } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Render to string. + * + * @param useSimpleNames + * if true, use just the simple name of each class. + * @param buf + * the buf + */ + protected abstract void toString(final boolean useSimpleNames, StringBuilder buf); + + /** + * Render to string, with simple names for classes if useSimpleNames is true. + * + * @param useSimpleNames + * if true, use just the simple name of each class. + * @return the string representation. + */ + String toString(final boolean useSimpleNames) { + final StringBuilder buf = new StringBuilder(); + toString(useSimpleNames, buf); + return buf.toString(); + } + + /** + * Render to string, using only simple + * names for classes. + * + * @return the string representation, using simple names for classes. + */ + public String toStringWithSimpleNames() { + final StringBuilder buf = new StringBuilder(); + toString(/* useSimpleNames = */ true, buf); + return buf.toString(); + } + + /** + * Render to string. + * + * @return the string representation. + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + toString(/* useSimpleNames = */ false, buf); + return buf.toString(); + } } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/Scanner.java b/src/main/java/io/github/classgraph/Scanner.java index 69e815763..d0f81c76c 100644 --- a/src/main/java/io/github/classgraph/Scanner.java +++ b/src/main/java/io/github/classgraph/Scanner.java @@ -29,8 +29,17 @@ package io.github.classgraph; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collection; @@ -40,7 +49,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Queue; import java.util.Set; import java.util.concurrent.Callable; @@ -54,16 +62,19 @@ import io.github.classgraph.ClassGraph.ScanResultProcessor; import io.github.classgraph.Classfile.ClassfileFormatException; import io.github.classgraph.Classfile.SkipClassException; -import nonapi.io.github.classgraph.ScanSpec; -import nonapi.io.github.classgraph.classpath.ClassLoaderAndModuleFinder; import nonapi.io.github.classgraph.classpath.ClasspathFinder; +import nonapi.io.github.classgraph.classpath.ClasspathOrder.ClasspathEntry; +import nonapi.io.github.classgraph.classpath.ModuleFinder; import nonapi.io.github.classgraph.concurrency.AutoCloseableExecutorService; import nonapi.io.github.classgraph.concurrency.InterruptionChecker; import nonapi.io.github.classgraph.concurrency.SingletonMap; -import nonapi.io.github.classgraph.concurrency.SingletonMap.NullSingletonException; +import nonapi.io.github.classgraph.concurrency.SingletonMap.NewInstanceFactory; import nonapi.io.github.classgraph.concurrency.WorkQueue; import nonapi.io.github.classgraph.concurrency.WorkQueue.WorkUnitProcessor; import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.CollectionUtils; import nonapi.io.github.classgraph.utils.FastPathResolver; import nonapi.io.github.classgraph.utils.FileUtils; import nonapi.io.github.classgraph.utils.JarUtils; @@ -75,6 +86,9 @@ class Scanner implements Callable { /** The scan spec. */ private final ScanSpec scanSpec; + /** If true, performing a scan. If false, only fetching the classpath. */ + public boolean performScan; + /** The nested jar handler. */ private final NestedJarHandler nestedJarHandler; @@ -99,20 +113,16 @@ class Scanner implements Callable { /** The classpath finder. */ private final ClasspathFinder classpathFinder; - /** The classloader and module finder. */ - private final ClassLoaderAndModuleFinder classLoaderAndModuleFinder; - /** The module order. */ - private final List moduleClasspathEltOrder; - - /** The context classloaders. */ - private final ClassLoader[] contextClassLoaders; + private final List moduleOrder; // ------------------------------------------------------------------------------------------------------------- /** - * The classpath scanner. - * + * The classpath scanner. Scanning is started by calling {@link #call()} on this object. + * + * @param performScan + * If true, performing a scan. If false, only fetching the classpath. * @param scanSpec * the scan spec * @param executorService @@ -125,18 +135,22 @@ class Scanner implements Callable { * the failure handler * @param topLevelLog * the log + * * @throws InterruptedException * if interrupted */ - Scanner(final ScanSpec scanSpec, final ExecutorService executorService, final int numParallelTasks, - final ScanResultProcessor scanResultProcessor, final FailureHandler failureHandler, - final LogNode topLevelLog) throws InterruptedException { + Scanner(final boolean performScan, final ScanSpec scanSpec, final ExecutorService executorService, + final int numParallelTasks, final ScanResultProcessor scanResultProcessor, + final FailureHandler failureHandler, final ReflectionUtils reflectionUtils, final LogNode topLevelLog) + throws InterruptedException { this.scanSpec = scanSpec; + this.performScan = performScan; scanSpec.sortPrefixes(); scanSpec.log(topLevelLog); if (topLevelLog != null) { - if (scanSpec.pathWhiteBlackList != null && scanSpec.pathWhiteBlackList.isSpecificallyWhitelisted("")) { - topLevelLog.log("Note: There is no need to whitelist the root package (\"\") -- not whitelisting " + if (scanSpec.pathAcceptReject != null + && scanSpec.packagePrefixAcceptReject.isSpecificallyAccepted("")) { + topLevelLog.log("Note: There is no need to accept the root package (\"\") -- not accepting " + "anything will have the same effect of causing all packages to be scanned"); } topLevelLog.log("Number of worker threads: " + numParallelTasks); @@ -146,85 +160,85 @@ class Scanner implements Callable { this.interruptionChecker = executorService instanceof AutoCloseableExecutorService ? ((AutoCloseableExecutorService) executorService).interruptionChecker : new InterruptionChecker(); - this.nestedJarHandler = new NestedJarHandler(scanSpec, interruptionChecker); + this.nestedJarHandler = new NestedJarHandler(scanSpec, interruptionChecker, reflectionUtils); this.numParallelTasks = numParallelTasks; this.scanResultProcessor = scanResultProcessor; this.failureHandler = failureHandler; this.topLevelLog = topLevelLog; final LogNode classpathFinderLog = topLevelLog == null ? null : topLevelLog.log("Finding classpath"); - this.classpathFinder = new ClasspathFinder(scanSpec, classpathFinderLog); - this.classLoaderAndModuleFinder = classpathFinder.getClassLoaderAndModuleFinder(); - this.contextClassLoaders = classLoaderAndModuleFinder.getContextClassLoaders(); - this.moduleClasspathEltOrder = getModuleOrder(classpathFinderLog); - } + this.classpathFinder = new ClasspathFinder(scanSpec, reflectionUtils, classpathFinderLog); - // ------------------------------------------------------------------------------------------------------------- - - /** - * Get the module order. - * - * @param log - * the log - * @return the module order - * @throws InterruptedException - * if interrupted - */ - private List getModuleOrder(final LogNode log) throws InterruptedException { - final List moduleCpEltOrder = new ArrayList<>(); - if (scanSpec.overrideClasspath == null && scanSpec.overrideClassLoaders == null && scanSpec.scanModules) { - // Add modules to start of classpath order, before traditional classpath - final List systemModuleRefs = classLoaderAndModuleFinder.getSystemModuleRefs(); - final ClassLoader defaultClassLoader = contextClassLoaders != null && contextClassLoaders.length != 0 - ? contextClassLoaders[0] - : null; - if (systemModuleRefs != null) { - for (final ModuleRef systemModuleRef : systemModuleRefs) { - final String moduleName = systemModuleRef.getName(); - if ( - // If scanning system packages and modules is enabled and white/blacklist is empty, - // then scan all system modules - (scanSpec.enableSystemJarsAndModules - && scanSpec.moduleWhiteBlackList.whitelistAndBlacklistAreEmpty()) - // Otherwise only scan specifically whitelisted system modules - || scanSpec.moduleWhiteBlackList - .isSpecificallyWhitelistedAndNotBlacklisted(moduleName)) { - // Create a new ClasspathElementModule - final ClasspathElementModule classpathElementModule = new ClasspathElementModule( - systemModuleRef, defaultClassLoader, nestedJarHandler, scanSpec); - moduleCpEltOrder.add(classpathElementModule); - // Open the ClasspathElementModule - classpathElementModule.open(/* ignored */ null, log); - } else { - if (log != null) { - log.log("Skipping non-whitelisted or blacklisted system module: " + moduleName); + try { + this.moduleOrder = new ArrayList<>(); + + // Check if modules should be scanned + final ModuleFinder moduleFinder = classpathFinder.getModuleFinder(); + if (moduleFinder != null) { + // Add modules to start of classpath order, before traditional classpath + final List systemModuleRefs = moduleFinder.getSystemModuleRefs(); + final ClassLoader[] classLoaderOrderRespectingParentDelegation = classpathFinder + .getClassLoaderOrderRespectingParentDelegation(); + final ClassLoader defaultClassLoader = classLoaderOrderRespectingParentDelegation != null + && classLoaderOrderRespectingParentDelegation.length != 0 + ? classLoaderOrderRespectingParentDelegation[0] + : null; + if (systemModuleRefs != null) { + for (final ModuleRef systemModuleRef : systemModuleRefs) { + final String moduleName = systemModuleRef.getName(); + if ( + // If scanning system packages and modules is enabled and accept/reject criteria are empty, + // then scan all system modules + (scanSpec.enableSystemJarsAndModules + && scanSpec.moduleAcceptReject.acceptAndRejectAreEmpty()) + // Otherwise only scan specifically accepted system modules + || scanSpec.moduleAcceptReject.isSpecificallyAcceptedAndNotRejected(moduleName)) { + // Create a new ClasspathElementModule + final ClasspathElementModule classpathElementModule = new ClasspathElementModule( + systemModuleRef, nestedJarHandler.moduleRefToModuleReaderProxyRecyclerMap, + new ClasspathEntryWorkUnit(null, defaultClassLoader, null, moduleOrder.size(), + ""), + scanSpec); + moduleOrder.add(classpathElementModule); + // Open the ClasspathElementModule + classpathElementModule.open(/* ignored */ null, classpathFinderLog); + } else { + if (classpathFinderLog != null) { + classpathFinderLog + .log("Skipping non-accepted or rejected system module: " + moduleName); + } } } } - } - final List nonSystemModuleRefs = classLoaderAndModuleFinder.getNonSystemModuleRefs(); - if (nonSystemModuleRefs != null) { - for (final ModuleRef nonSystemModuleRef : nonSystemModuleRefs) { - String moduleName = nonSystemModuleRef.getName(); - if (moduleName == null) { - moduleName = ""; - } - if (scanSpec.moduleWhiteBlackList.isWhitelistedAndNotBlacklisted(moduleName)) { - // Create a new ClasspathElementModule - final ClasspathElementModule classpathElementModule = new ClasspathElementModule( - nonSystemModuleRef, defaultClassLoader, nestedJarHandler, scanSpec); - moduleCpEltOrder.add(classpathElementModule); - // Open the ClasspathElementModule - classpathElementModule.open(/* ignored */ null, log); - } else { - if (log != null) { - log.log("Skipping non-whitelisted or blacklisted module: " + moduleName); + final List nonSystemModuleRefs = moduleFinder.getNonSystemModuleRefs(); + if (nonSystemModuleRefs != null) { + for (final ModuleRef nonSystemModuleRef : nonSystemModuleRefs) { + String moduleName = nonSystemModuleRef.getName(); + if (moduleName == null) { + moduleName = ""; + } + if (scanSpec.moduleAcceptReject.isAcceptedAndNotRejected(moduleName)) { + // Create a new ClasspathElementModule + final ClasspathElementModule classpathElementModule = new ClasspathElementModule( + nonSystemModuleRef, nestedJarHandler.moduleRefToModuleReaderProxyRecyclerMap, + new ClasspathEntryWorkUnit(null, defaultClassLoader, null, moduleOrder.size(), + ""), + scanSpec); + moduleOrder.add(classpathElementModule); + // Open the ClasspathElementModule + classpathElementModule.open(/* ignored */ null, classpathFinderLog); + } else { + if (classpathFinderLog != null) { + classpathFinderLog.log("Skipping non-accepted or rejected module: " + moduleName); + } } } } } + } catch (final InterruptedException e) { + nestedJarHandler.close(/* log = */ null); + throw e; } - return moduleCpEltOrder; } // ------------------------------------------------------------------------------------------------------------- @@ -243,70 +257,40 @@ private List getModuleOrder(final LogNode log) throws In private static void findClasspathOrderRec(final ClasspathElement currClasspathElement, final Set visitedClasspathElts, final List order) { if (visitedClasspathElts.add(currClasspathElement)) { + // The classpath order requires a preorder traversal of the DAG of classpath dependencies if (!currClasspathElement.skipClasspathElement) { // Don't add a classpath element if it is marked to be skipped. order.add(currClasspathElement); + // Whether or not a classpath element should be skipped, add any child classpath elements that are + // not marked to be skipped (i.e. keep recursing below) } - // Whether or not a classpath element should be skipped, add any child classpath elements that are - // not marked to be skipped (i.e. keep recursing) - for (final ClasspathElement childClasspathElt : currClasspathElement.childClasspathElementsOrdered) { + // Sort child elements into correct order, then traverse to them in order + final List childClasspathElementsSorted = CollectionUtils + .sortCopy(currClasspathElement.childClasspathElements); + for (final ClasspathElement childClasspathElt : childClasspathElementsSorted) { findClasspathOrderRec(childClasspathElt, visitedClasspathElts, order); } } } - /** Comparator used to sort ClasspathElement values into increasing order of integer index key. */ - private static final Comparator> INDEXED_CLASSPATH_ELEMENT_COMPARATOR = // - new Comparator>() { - @Override - public int compare(final Entry o1, - final Entry o2) { - return o1.getKey() - o2.getKey(); - } - }; - - /** - * Sort a collection of indexed ClasspathElements into increasing order of integer index key. - * - * @param classpathEltsIndexed - * the indexed classpath elts - * @return the classpath elements, ordered by index - */ - private static List orderClasspathElements( - final Collection> classpathEltsIndexed) { - final List> classpathEltsIndexedOrdered = new ArrayList<>( - classpathEltsIndexed); - Collections.sort(classpathEltsIndexedOrdered, INDEXED_CLASSPATH_ELEMENT_COMPARATOR); - final List classpathEltsOrdered = new ArrayList<>(classpathEltsIndexedOrdered.size()); - for (final Entry ent : classpathEltsIndexedOrdered) { - classpathEltsOrdered.add(ent.getValue()); - } - return classpathEltsOrdered; - } - /** * Recursively perform a depth-first traversal of child classpath elements, breaking cycles if necessary, to * determine the final classpath element order. This causes child classpath elements to be inserted in-place in * the classpath order, after the parent classpath element that contained them. * - * @param uniqueClasspathElements - * the unique classpath elements - * @param toplevelClasspathEltsIndexed + * @param toplevelClasspathElts * the toplevel classpath elts, indexed by order within the toplevel classpath * @return the final classpath order, after depth-first traversal of child classpath elements */ - private List findClasspathOrder(final Set uniqueClasspathElements, - final Queue> toplevelClasspathEltsIndexed) { - final List toplevelClasspathEltsOrdered = orderClasspathElements( - toplevelClasspathEltsIndexed); - for (final ClasspathElement classpathElt : uniqueClasspathElements) { - classpathElt.childClasspathElementsOrdered = orderClasspathElements( - classpathElt.childClasspathElementsIndexed); - } + private List findClasspathOrder(final Set toplevelClasspathElts) { + // Sort toplevel classpath elements into their correct order + final List toplevelClasspathEltsSorted = CollectionUtils.sortCopy(toplevelClasspathElts); + + // Perform a depth-first preorder traversal of the DAG of classpath elements final Set visitedClasspathElts = new HashSet<>(); final List order = new ArrayList<>(); - for (final ClasspathElement toplevelClasspathElt : toplevelClasspathEltsOrdered) { - findClasspathOrderRec(toplevelClasspathElt, visitedClasspathElts, order); + for (final ClasspathElement elt : toplevelClasspathEltsSorted) { + findClasspathOrderRec(elt, visitedClasspathElts, order); } return order; } @@ -344,164 +328,293 @@ private void processWorkUnits(final Collection workUnits, final LogNode l /** Used to enqueue classpath elements for opening. */ static class ClasspathEntryWorkUnit { - /** The raw classpath entry and associated {@link ClassLoader}. */ - private final Entry rawClasspathEntry; + /** The classpath entry object (a {@link String} path, {@link Path}, {@link URL} or {@link URI}). */ + Object classpathEntryObj; + + /** The classloader the classpath entry object was obtained from. */ + final ClassLoader classLoader; /** The parent classpath element. */ - private final ClasspathElement parentClasspathElement; + final ClasspathElement parentClasspathElement; /** The order within the parent classpath element. */ - private final int orderWithinParentClasspathElement; + final int classpathElementIdxWithinParent; + + /** The package root prefix (e.g. "BOOT-INF/classes/"). */ + final String packageRootPrefix; /** * Constructor. * - * @param rawClasspathEntry - * the raw classpath entry path and the classloader it was obtained from + * @param classpathEntryObj + * the raw classpath entry object + * @param classLoader + * the classloader the classpath entry object was obtained from * @param parentClasspathElement * the parent classpath element - * @param orderWithinParentClasspathElement + * @param classpathElementIdxWithinParent * the order within parent classpath element + * @param packageRootPrefix + * the package root prefix */ - public ClasspathEntryWorkUnit(final Entry rawClasspathEntry, - final ClasspathElement parentClasspathElement, final int orderWithinParentClasspathElement) { - this.rawClasspathEntry = rawClasspathEntry; + public ClasspathEntryWorkUnit(final Object classpathEntryObj, final ClassLoader classLoader, + final ClasspathElement parentClasspathElement, final int classpathElementIdxWithinParent, + final String packageRootPrefix) { + this.classpathEntryObj = classpathEntryObj; + this.classLoader = classLoader; this.parentClasspathElement = parentClasspathElement; - this.orderWithinParentClasspathElement = orderWithinParentClasspathElement; + this.classpathElementIdxWithinParent = classpathElementIdxWithinParent; + this.packageRootPrefix = packageRootPrefix; } } + // ------------------------------------------------------------------------------------------------------------- + /** - * The classpath element singleton map. For each classpath element path, canonicalize path, and create a - * ClasspathElement singleton. + * Normalize a classpath entry object so that it is mapped to a canonical {@link Path} object if possible, + * falling back to a {@link URL} or {@link URI} if not possible. This is needed to avoid treating + * "file:///path/to/x.jar" and "/path/to/x.jar" as different classpath elements. Maps URL("jar:file:x.jar!/") to + * Path("x.jar"), etc. + * + * @param classpathEntryObj + * The classpath entry object. + * @return The normalized classpath entry object. + * @throws IOException */ - private final SingletonMap, ClasspathElement, IOException> // - classpathEntryToClasspathElementSingletonMap = // - new SingletonMap, ClasspathElement, IOException>() { - @Override - public ClasspathElement newInstance(final Entry classpathEntry, - final LogNode log) throws IOException, InterruptedException { - final String classpathEntryPath = classpathEntry.getKey(); - final ClassLoader classLoader = classpathEntry.getValue(); - if (classpathEntryPath.regionMatches(true, 0, "http://", 0, 7) - || classpathEntryPath.regionMatches(true, 0, "https://", 0, 8)) { - // For remote URLs, must be a jar - return new ClasspathElementZip(classpathEntryPath, classLoader, nestedJarHandler, scanSpec); - } - // Normalize path -- strip off any leading "jar:" / "file:", and normalize separators - final String pathNormalized = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, - classpathEntryPath); - // Strip everything after first "!", to get path of base jarfile or dir - final int plingIdx = pathNormalized.indexOf('!'); - final String pathToCanonicalize = plingIdx < 0 ? pathNormalized - : pathNormalized.substring(0, plingIdx); - // Canonicalize base jarfile or dir (may throw IOException) - final File fileCanonicalized = new File(pathToCanonicalize).getCanonicalFile(); - // Test if base file or dir exists (and is a standard file or dir) - if (!fileCanonicalized.exists()) { - throw new FileNotFoundException(); - } - if (!FileUtils.canRead(fileCanonicalized)) { - throw new IOException("Cannot read file or directory"); - } - boolean isJar = classpathEntryPath.regionMatches(true, 0, "jar:", 0, 4) || plingIdx > 0; - if (fileCanonicalized.isFile()) { - // If a file, must be a jar - isJar = true; - } else if (fileCanonicalized.isDirectory()) { - if (isJar) { - throw new IOException("Expected jar, found directory"); - } - } else { - throw new IOException("Not a normal file or directory"); - } - // Check if canonicalized path is the same as pre-canonicalized path - final String baseFileCanonicalPathNormalized = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, - fileCanonicalized.getPath()); - final String canonicalPathNormalized = plingIdx < 0 ? baseFileCanonicalPathNormalized - : baseFileCanonicalPathNormalized + pathNormalized.substring(plingIdx); - if (!canonicalPathNormalized.equals(pathNormalized)) { - // If canonicalized path is not the same as pre-canonicalized path, need to recurse - // to map non-canonicalized path to singleton for canonicalized path (this should - // only recurse once, since File::getCanonicalFile and FastPathResolver::resolve are - // idempotent) + private static Object normalizeClasspathEntry(final Object classpathEntryObj) throws IOException { + if (classpathEntryObj == null) { + // Should not happen + throw new IOException("Got null classpath entry object"); + } + Object classpathEntryObjNormalized = classpathEntryObj; + + // Convert URL/URI (or anything other than URL/URI, or Path) into a String. + // Paths.get fails with "IllegalArgumentException: URI is not hierarchical" + // for paths like "jar:file:myjar.jar!/" (#625) -- need to strip the "!/" off the end. + // Also strip any "jar:file:" or "file:" off the beginning. + // This normalizes "file:x.jar" and "x.jar" to the same string, for example. + if (!(classpathEntryObjNormalized instanceof Path)) { + classpathEntryObjNormalized = FastPathResolver.resolve(FileUtils.currDirPath(), + classpathEntryObjNormalized.toString()); + } + + // If classpath entry object is a URL-formatted string, convert to (or back to) a URL instance. + if (classpathEntryObjNormalized instanceof String) { + String classpathEntStr = (String) classpathEntryObjNormalized; + final boolean isURL = JarUtils.URL_SCHEME_PATTERN.matcher(classpathEntStr).matches(); + final boolean isMultiSection = classpathEntStr.contains("!"); + if (isURL || isMultiSection) { + // Encode spaces and hash symbols in classpath entry as they potentially can be invalid when + // converted to a URL/URI + classpathEntStr = classpathEntStr.replace(" ", "%20").replace("#", "%23"); + // Convert back to URL (or URI) if this has a URL scheme or if this is a multi-section + // path (which needs the "jar:file:" scheme) + if (!isURL) { + // Add "file:" scheme if there is no scheme + classpathEntStr = "file:" + classpathEntStr; + } + if (isMultiSection) { + // Multi-section URL strings that do not already have a URL scheme need to + // have the "jar:file:" scheme + classpathEntStr = "jar:" + classpathEntStr; + // Also "jar:" URLs need at least one instance of "!/" -- if only "!" is used + // without a subsequent "/", replace it + classpathEntStr = classpathEntStr.replaceAll("!([^/])", "!/$1"); + } + try { + // Convert classpath entry to (or back to) a URL. + final URL classpathEntryURL = new URL(classpathEntStr); + classpathEntryObjNormalized = classpathEntryURL; + + // If this is not a multi-section URL, try converting URL to a Path + if (!isMultiSection) { try { - return this.get(new SimpleEntry<>(canonicalPathNormalized, classLoader), log); - } catch (final NullSingletonException e) { - throw new IOException("Cannot get classpath element for canonical path " - + canonicalPathNormalized + " : " + e); + final String scheme = classpathEntryURL.getProtocol(); + if (!"http".equals(scheme) && !"https".equals(scheme)) { + final URI classpathEntryURI = classpathEntryURL.toURI(); + // See if the URL resolves to a file or directory via the Path API + classpathEntryObjNormalized = Paths.get(classpathEntryURI); + } + } catch (final URISyntaxException | IllegalArgumentException | SecurityException e1) { + // URL cannot be represented as a URI or as a Path + } catch (final FileSystemNotFoundException e) { + // This is a custom URL scheme without a backing FileSystem } - } else { - // Otherwise path is already canonical, and this is the first time this path has - // been seen -- instantiate a ClasspathElementZip or ClasspathElementDir singleton - // for the classpath element path - return isJar - ? new ClasspathElementZip(canonicalPathNormalized, classLoader, nestedJarHandler, - scanSpec) - : new ClasspathElementDir(fileCanonicalized, classLoader, scanSpec); + } // else this is a remote jar URL + + } catch (final MalformedURLException e) { + // Try creating URI if URL creation fails, in case there is a URI-only scheme + try { + final URI classpathEntryURI = new URI(classpathEntStr); + classpathEntryObjNormalized = classpathEntryURI; + + final String scheme = classpathEntryURI.getScheme(); + if (!"http".equals(scheme) && !"https".equals(scheme)) { + // See if the URI resolves to a file or directory via the Path API + classpathEntryObjNormalized = Paths.get(classpathEntryURI); + } // else this is a remote jar URI + + } catch (final URISyntaxException e1) { + throw new IOException("Malformed URI: " + classpathEntryObjNormalized + " : " + e1); + } catch (final IllegalArgumentException | SecurityException e1) { + // URI cannot be represented as a Path + } catch (final FileSystemNotFoundException e1) { + // This is a custom URI scheme without a backing FileSystem + } + } + } + // Last-ditch effort -- try to convert String to Path + if (classpathEntryObjNormalized instanceof String) { + try { + classpathEntryObjNormalized = new File((String) classpathEntryObjNormalized).toPath(); + } catch (final Exception e) { + try { + classpathEntryObjNormalized = Paths.get((String) classpathEntryObjNormalized); + } catch (final InvalidPathException e2) { + throw new IOException("Malformed path: " + classpathEntryObj + " : " + e2); } } + } + } + // At this point, classpathEntryObjNormalized is either a Path wherever possible (where the + // classpath entry pointed to a jarfile or directory) or a URL/URI (for multi-section "jar:" + // URLs with "!" separators, custom URL schemes without backing filesystems, or URLs that + // can't be turned into a Path for any other reason). + + // Canonicalize Path objects so the same file is opened only once + if (classpathEntryObjNormalized instanceof Path) { + try { + // Canonicalize path, to avoid duplication + // Throws IOException if the file does not exist or an I/O error occurs + classpathEntryObjNormalized = ((Path) classpathEntryObjNormalized).toRealPath(); + } catch (final IOException | SecurityException e) { + // Ignore + } + } + + return classpathEntryObjNormalized; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * A singleton map used to eliminate creation of duplicate {@link ClasspathElement} objects, to reduce the + * chance that resources are scanned twice, by mapping canonicalized Path objects, URLs, etc. to + * ClasspathElements. + */ + private final SingletonMap // + classpathEntryObjToClasspathEntrySingletonMap = // + new SingletonMap() { + @Override + public ClasspathElement newInstance(final Object classpathEntryObj, final LogNode log) + throws IOException, InterruptedException { + // Overridden by a NewInstanceFactory + throw new IOException("Should not reach here"); + } }; + // ------------------------------------------------------------------------------------------------------------- + /** * Create a WorkUnitProcessor for opening traditional classpath entries (which are mapped to * {@link ClasspathElementDir} or {@link ClasspathElementZip} -- {@link ClasspathElementModule is handled * separately}). * - * @param openedClasspathElementsSet - * the opened classpath elements set - * @param toplevelClasspathEltOrder - * the toplevel classpath elt order + * @param allClasspathEltsOut + * on exit, the set of all classpath elements + * @param toplevelClasspathEltsOut + * on exit, the toplevel classpath elements * @return the work unit processor */ private WorkUnitProcessor newClasspathEntryWorkUnitProcessor( - final Set openedClasspathElementsSet, - final Queue> toplevelClasspathEltOrder) { + final Set allClasspathEltsOut, final Set toplevelClasspathEltsOut) { return new WorkUnitProcessor() { @Override public void processWorkUnit(final ClasspathEntryWorkUnit workUnit, final WorkQueue workQueue, final LogNode log) throws InterruptedException { try { - // Create a ClasspathElementZip or ClasspathElementDir for each entry in the classpath - ClasspathElement classpathElt; - try { - classpathElt = classpathEntryToClasspathElementSingletonMap.get(workUnit.rawClasspathEntry, - log); - } catch (final NullSingletonException e) { - throw new IOException("Cannot get classpath element for classpath entry " - + workUnit.rawClasspathEntry + " : " + e); - } + // Normalize the classpath entry object, and update it in the work unit + workUnit.classpathEntryObj = normalizeClasspathEntry(workUnit.classpathEntryObj); - // Only run open() once per ClasspathElement (it is possible for there to be - // multiple classpath elements with different non-canonical paths that map to - // the same canonical path, i.e. to the same ClasspathElement) - if (openedClasspathElementsSet.add(classpathElt)) { - // Check if the classpath element is valid (classpathElt.skipClasspathElement - // will be set if not). In case of ClasspathElementZip, open or extract nested - // jars as LogicalZipFile instances. Read manifest files for jarfiles to look - // for Class-Path manifest entries. Adds extra classpath elements to the work - // queue if they are found. - classpathElt.open(workQueue, log); - - // Create a new tuple consisting of the order of the new classpath element - // within its parent, and the new classpath element. - // N.B. even if skipClasspathElement is true, still possibly need to scan child - // classpath elements (so still need to connect parent to child here) - final SimpleEntry classpathEltEntry = // - new SimpleEntry<>(workUnit.orderWithinParentClasspathElement, classpathElt); - if (workUnit.parentClasspathElement != null) { - // Link classpath element to its parent, if it is not a toplevel element - workUnit.parentClasspathElement.childClasspathElementsIndexed.add(classpathEltEntry); + // Determine if classpath entry is a jar or dir + final boolean isJar; + if (workUnit.classpathEntryObj instanceof URL || workUnit.classpathEntryObj instanceof URI) { + // URLs and URIs always point to jars + isJar = true; + } else if (workUnit.classpathEntryObj instanceof Path) { + final Path path = (Path) workUnit.classpathEntryObj; + if ("JrtFileSystem".equals(path.getFileSystem().getClass().getSimpleName())) { + // Ignore JrtFileSystem (#553) -- paths are of form: + // /modules/java.base/module-info.class + throw new IOException("Ignoring JrtFS filesystem path " + + "(modules are scanned using the JPMS API): " + path); + } + if (!FileUtils.canRead(path)) { + throw new IOException("Cannot read path: " + path); } else { - // Record toplevel elements - toplevelClasspathEltOrder.add(classpathEltEntry); + final BasicFileAttributes attributes = Files.readAttributes(path, + BasicFileAttributes.class); + if (attributes.isRegularFile()) { + // classpathEntObj is a Path which points to a file, so it must be a jar + isJar = true; + } else if (attributes.isDirectory()) { + // classpathEntObj is a Path which points to a dir + isJar = false; + } else { + throw new IOException("Not a file or directory: " + path); + } } + } else { + // Should not happen + throw new IOException("Got unexpected classpath entry object type " + + workUnit.classpathEntryObj.getClass().getName() + " : " + + workUnit.classpathEntryObj); } - } catch (final IOException | SecurityException e) { + + // Create a ClasspathElementZip or ClasspathElementDir from the classpath entry + // Use a singleton map to ensure that classpath elements are only opened once + // per unique Path, URL, or URI + classpathEntryObjToClasspathEntrySingletonMap.get(workUnit.classpathEntryObj, log, + // A NewInstanceFactory is used here because workUnit has to be passed in, + // and the standard newInstance API doesn't support an extra parameter like this + new NewInstanceFactory() { + @Override + public ClasspathElement newInstance() throws IOException, InterruptedException { + final ClasspathElement classpathElement = isJar + ? new ClasspathElementZip(workUnit, nestedJarHandler, scanSpec) + : new ClasspathElementDir(workUnit, nestedJarHandler, scanSpec); + + allClasspathEltsOut.add(classpathElement); + + // Run open() on the ClasspathElement + final LogNode subLog = log == null ? null + : log.log(classpathElement.getURI().toString(), + "Opening classpath element " + classpathElement); + + // Check if the classpath element is valid (classpathElt.skipClasspathElement + // will be set if not). In case of ClasspathElementZip, open or extract nested + // jars as LogicalZipFile instances. Read manifest files for jarfiles to look + // for Class-Path manifest entries. Adds extra classpath elements to the work + // queue if they are found. + classpathElement.open(workQueue, subLog); + + if (workUnit.parentClasspathElement != null) { + // Link classpath element to its parent, if it is not a toplevel element + workUnit.parentClasspathElement.childClasspathElements + .add(classpathElement); + } else { + toplevelClasspathEltsOut.add(classpathElement); + } + + return classpathElement; + } + }); + + } catch (final Exception e) { if (log != null) { - log.log("Skipping invalid classpath element " + workUnit.rawClasspathEntry.getKey() + " : " - + e); + log.log("Skipping invalid classpath entry " + workUnit.classpathEntryObj + " : " + + (e.getCause() == null ? e : e.getCause())); } } } @@ -548,12 +661,24 @@ private static class ClassfileScannerWorkUnitProcessor implements WorkUnitProces /** The classpath order. */ private final List classpathOrder; - /** The class names scheduled for scanning. */ - private final Set classNamesScheduledForScanning; + /** + * The names of accepted classes found in the classpath while scanning paths within classpath elements. + */ + private final Set acceptedClassNamesFound; + + /** + * The names of external (non-accepted) classes scheduled for extended scanning (where scanning is extended + * upwards to superclasses, interfaces and annotations). + */ + private final Set classNamesScheduledForExtendedScanning = Collections + .newSetFromMap(new ConcurrentHashMap()); /** The valid {@link Classfile} objects created by scanning classfiles. */ private final Queue scannedClassfiles; + /** The string intern map. */ + private final ConcurrentHashMap stringInternMap = new ConcurrentHashMap<>(); + /** * Constructor. * @@ -561,20 +686,33 @@ private static class ClassfileScannerWorkUnitProcessor implements WorkUnitProces * the scan spec * @param classpathOrder * the classpath order - * @param classNamesScheduledForScanning - * the class names scheduled for scanning + * @param acceptedClassNamesFound + * the names of accepted classes found in the classpath while scanning paths within classpath + * elements. * @param scannedClassfiles * the {@link Classfile} objects created by scanning classfiles */ public ClassfileScannerWorkUnitProcessor(final ScanSpec scanSpec, - final List classpathOrder, final Set classNamesScheduledForScanning, + final List classpathOrder, final Set acceptedClassNamesFound, final Queue scannedClassfiles) { this.scanSpec = scanSpec; this.classpathOrder = classpathOrder; - this.classNamesScheduledForScanning = classNamesScheduledForScanning; + this.acceptedClassNamesFound = acceptedClassNamesFound; this.scannedClassfiles = scannedClassfiles; } + /** + * Process work unit. + * + * @param workUnit + * the work unit + * @param workQueue + * the work queue + * @param log + * the log + * @throws InterruptedException + * the interrupted exception + */ /* (non-Javadoc) * @see nonapi.io.github.classgraph.concurrency.WorkQueue.WorkUnitProcessor#processWorkUnit( * java.lang.Object, nonapi.io.github.classgraph.concurrency.WorkQueue) @@ -589,29 +727,38 @@ public void processWorkUnit(final ClassfileScanWorkUnit workUnit, final LogNode subLog = workUnit.classfileResource.scanLog == null ? null : workUnit.classfileResource.scanLog.log(workUnit.classfileResource.getPath(), "Parsing classfile"); + try { // Parse classfile binary format, creating a Classfile object final Classfile classfile = new Classfile(workUnit.classpathElement, classpathOrder, - classNamesScheduledForScanning, workUnit.classfileResource.getPath(), - workUnit.classfileResource, workUnit.isExternalClass, workQueue, scanSpec, subLog); + acceptedClassNamesFound, classNamesScheduledForExtendedScanning, + workUnit.classfileResource.getPath(), workUnit.classfileResource, workUnit.isExternalClass, + stringInternMap, workQueue, scanSpec, subLog); // Enqueue the classfile for linking scannedClassfiles.add(classfile); + if (subLog != null) { + subLog.addElapsedTime(); + } } catch (final SkipClassException e) { if (subLog != null) { - subLog.log("Skipping classfile: " + e.getMessage()); + subLog.log(workUnit.classfileResource.getPath(), "Skipping classfile: " + e.getMessage()); + subLog.addElapsedTime(); } } catch (final ClassfileFormatException e) { if (subLog != null) { - subLog.log("Invalid classfile: " + e.getMessage()); + subLog.log(workUnit.classfileResource.getPath(), "Invalid classfile: " + e.getMessage()); + subLog.addElapsedTime(); } } catch (final IOException e) { if (subLog != null) { - subLog.log("Could not read classfile: " + e); + subLog.log(workUnit.classfileResource.getPath(), "Could not read classfile: " + e); + subLog.addElapsedTime(); } - } finally { + } catch (final Exception e) { if (subLog != null) { + subLog.log(workUnit.classfileResource.getPath(), "Could not read classfile", e); subLog.addElapsedTime(); } } @@ -631,7 +778,7 @@ public void processWorkUnit(final ClassfileScanWorkUnit workUnit, private void findNestedClasspathElements(final List> classpathElts, final LogNode log) { // Sort classpath elements into lexicographic order - Collections.sort(classpathElts, new Comparator>() { + CollectionUtils.sortIfNotEmpty(classpathElts, new Comparator>() { @Override public int compare(final SimpleEntry o1, final SimpleEntry o2) { @@ -639,7 +786,6 @@ public int compare(final SimpleEntry o1, } }); // Find any nesting of elements within other elements - LogNode nestedClasspathRootNode = null; for (int i = 0; i < classpathElts.size(); i++) { // See if each classpath element is a prefix of any others (if so, they will immediately follow // in lexicographic order) @@ -669,11 +815,7 @@ public int compare(final SimpleEntry o1, } baseElement.nestedClasspathRootPrefixes.add(nestedClasspathRelativePath + "/"); if (log != null) { - if (nestedClasspathRootNode == null) { - nestedClasspathRootNode = log.log("Found nested classpath elements"); - } - nestedClasspathRootNode - .log(basePath + " is a prefix of the nested element " + comparePath); + log.log(basePath + " is a prefix of the nested element " + comparePath); } } } @@ -700,9 +842,10 @@ private void preprocessClasspathElementsByType(final List fina final List> classpathEltZips = new ArrayList<>(); for (final ClasspathElement classpathElt : finalTraditionalClasspathEltOrder) { if (classpathElt instanceof ClasspathElementDir) { - // Separate out ClasspathElementDir elements from other types - classpathEltDirs.add( - new SimpleEntry<>(((ClasspathElementDir) classpathElt).getFile().getPath(), classpathElt)); + // Separate out ClasspathElementFileDir and ClasspathElementPathDir elements from other types + final File file = classpathElt.getFile(); + final String path = file == null ? classpathElt.toString() : file.getPath(); + classpathEltDirs.add(new SimpleEntry<>(path, classpathElt)); } else if (classpathElt instanceof ClasspathElementZip) { // Separate out ClasspathElementZip elements from other types @@ -717,14 +860,14 @@ private void preprocessClasspathElementsByType(final List fina // A / pair in the value of an Add-Opens attribute has the same // meaning as the command-line option --add-opens /=ALL-UNNAMED." if (classpathEltZip.logicalZipFile.addExportsManifestEntryValue != null) { - for (final String addExports : JarUtils - .smartPathSplit(classpathEltZip.logicalZipFile.addExportsManifestEntryValue, ' ')) { + for (final String addExports : JarUtils.smartPathSplit( + classpathEltZip.logicalZipFile.addExportsManifestEntryValue, ' ', scanSpec)) { scanSpec.modulePathInfo.addExports.add(addExports + "=ALL-UNNAMED"); } } if (classpathEltZip.logicalZipFile.addOpensManifestEntryValue != null) { - for (final String addOpens : JarUtils - .smartPathSplit(classpathEltZip.logicalZipFile.addOpensManifestEntryValue, ' ')) { + for (final String addOpens : JarUtils.smartPathSplit( + classpathEltZip.logicalZipFile.addOpensManifestEntryValue, ' ', scanSpec)) { scanSpec.modulePathInfo.addOpens.add(addOpens + "=ALL-UNNAMED"); } } @@ -754,10 +897,10 @@ private void preprocessClasspathElementsByType(final List fina * the mask log */ private void maskClassfiles(final List classpathElementOrder, final LogNode maskLog) { - final Set whitelistedClasspathRelativePathsFound = new HashSet<>(); + final Set acceptedClasspathRelativePathsFound = new HashSet<>(); for (int classpathIdx = 0; classpathIdx < classpathElementOrder.size(); classpathIdx++) { final ClasspathElement classpathElement = classpathElementOrder.get(classpathIdx); - classpathElement.maskClassfiles(classpathIdx, whitelistedClasspathRelativePathsFound, maskLog); + classpathElement.maskClassfiles(classpathIdx, acceptedClasspathRelativePathsFound, maskLog); } if (maskLog != null) { maskLog.addElapsedTime(); @@ -773,8 +916,8 @@ private void maskClassfiles(final List classpathElementOrder, * the final classpath elt order * @param finalClasspathEltOrderStrs * the final classpath elt order strs - * @param contextClassLoaders - * the context classloaders + * @param classpathFinder + * the {@link ClasspathFinder} * @return the scan result * @throws InterruptedException * if the scan was interrupted @@ -782,7 +925,7 @@ private void maskClassfiles(final List classpathElementOrder, * if the scan threw an uncaught exception */ private ScanResult performScan(final List finalClasspathEltOrder, - final List finalClasspathEltOrderStrs, final ClassLoader[] contextClassLoaders) + final List finalClasspathEltOrderStrs, final ClasspathFinder classpathFinder) throws InterruptedException, ExecutionException { // Mask classfiles (remove any classfile resources that are shadowed by an earlier definition // of the same class) @@ -797,36 +940,50 @@ private ScanResult performScan(final List finalClasspathEltOrd fileToLastModified.putAll(classpathElement.fileToLastModified); } - // Scan classfiles, if scanSpec.enableClassInfo is true - final Map classNameToClassInfo = new HashMap<>(); + // Scan classfiles, if scanSpec.enableClassInfo is true. + // (classNameToClassInfo is a ConcurrentHashMap because it can be modified by + // ArrayTypeSignature.getArrayClassInfo() after scanning is complete) + final Map classNameToClassInfo = new ConcurrentHashMap<>(); final Map packageNameToPackageInfo = new HashMap<>(); final Map moduleNameToModuleInfo = new HashMap<>(); if (scanSpec.enableClassInfo) { - // Get whitelisted classfile order + // Get accepted classfile order final List classfileScanWorkItems = new ArrayList<>(); - final Set classNamesScheduledForScanning = Collections - .newSetFromMap(new ConcurrentHashMap()); + final Set acceptedClassNamesFound = new HashSet<>(); for (final ClasspathElement classpathElement : finalClasspathEltOrder) { // Get classfile scan order across all classpath elements - for (final Resource resource : classpathElement.whitelistedClassfileResources) { + for (final Resource resource : classpathElement.acceptedClassfileResources) { + // Create a set of names of all accepted classes found in classpath element paths, + // and double-check that a class is not going to be scanned twice + final String className = JarUtils.classfilePathToClassName(resource.getPath()); + if (!acceptedClassNamesFound.add(className) && !className.equals("module-info") + && !className.equals("package-info") && !className.endsWith(".package-info")) { + // The class should not be scheduled more than once for scanning, since classpath + // masking was already applied + throw new IllegalArgumentException("Class " + className + + " should not have been scheduled more than once for scanning due to classpath" + + " masking -- please report this bug at:" + + " https://github.com/classgraph/classgraph/issues"); + } + // Schedule class for scanning classfileScanWorkItems .add(new ClassfileScanWorkUnit(classpathElement, resource, /* isExternal = */ false)); - // Pre-seed scanned class names with all whitelisted classes (since these will - // be scanned for sure) - classNamesScheduledForScanning.add(JarUtils.classfilePathToClassName(resource.getPath())); } } - // Scan classfiles in parallel. + // Scan classfiles in parallel final Queue scannedClassfiles = new ConcurrentLinkedQueue<>(); + final ClassfileScannerWorkUnitProcessor classfileWorkUnitProcessor = // + new ClassfileScannerWorkUnitProcessor(scanSpec, finalClasspathEltOrder, + Collections.unmodifiableSet(acceptedClassNamesFound), scannedClassfiles); processWorkUnits(classfileScanWorkItems, topLevelLog == null ? null : topLevelLog.log("Scanning classfiles"), - new ClassfileScannerWorkUnitProcessor(scanSpec, finalClasspathEltOrder, - classNamesScheduledForScanning, scannedClassfiles)); + classfileWorkUnitProcessor); // Link the Classfile objects to produce ClassInfo objects. This needs to be done from a single thread. final LogNode linkLog = topLevelLog == null ? null : topLevelLog.log("Linking related classfiles"); - for (final Classfile c : scannedClassfiles) { + while (!scannedClassfiles.isEmpty()) { + final Classfile c = scannedClassfiles.remove(); c.link(classNameToClassInfo, packageNameToPackageInfo, moduleNameToModuleInfo); } @@ -862,9 +1019,17 @@ private ScanResult performScan(final List finalClasspathEltOrd } // Return a new ScanResult - return new ScanResult(scanSpec, finalClasspathEltOrder, finalClasspathEltOrderStrs, contextClassLoaders, - classNameToClassInfo, packageNameToPackageInfo, moduleNameToModuleInfo, fileToLastModified, - nestedJarHandler, topLevelLog); + final ScanResult scanResult = new ScanResult(scanSpec, finalClasspathEltOrder, finalClasspathEltOrderStrs, + classpathFinder, classNameToClassInfo, packageNameToPackageInfo, moduleNameToModuleInfo, + fileToLastModified, nestedJarHandler, topLevelLog); + + // Set the ScanResult in each classpath element, so that the classpath elements can determine when the + // ScanResult is closed + for (final ClasspathElement classpathElement : finalClasspathEltOrder) { + classpathElement.setScanResult(scanResult); + } + + return scanResult; } // ------------------------------------------------------------------------------------------------------------- @@ -881,41 +1046,47 @@ private ScanResult performScan(final List finalClasspathEltOrd * if a worker threw an uncaught exception */ private ScanResult openClasspathElementsThenScan() throws InterruptedException, ExecutionException { - final LogNode log = topLevelLog == null ? null : topLevelLog.log("Finding nested classpath elements"); - // Get order of elements in traditional classpath final List rawClasspathEntryWorkUnits = new ArrayList<>(); - for (final Entry rawClasspathEntry : classpathFinder.getClasspathOrder().getOrder()) { - rawClasspathEntryWorkUnits - .add(new ClasspathEntryWorkUnit(rawClasspathEntry, /* parentClasspathElement = */ null, - /* orderWithinParentClasspathElement = */ rawClasspathEntryWorkUnits.size())); + final List rawClasspathOrder = classpathFinder.getClasspathOrder().getOrder(); + for (final ClasspathEntry rawClasspathEntry : rawClasspathOrder) { + rawClasspathEntryWorkUnits.add(new ClasspathEntryWorkUnit(rawClasspathEntry.classpathEntryObj, + rawClasspathEntry.classLoader, /* parentClasspathElement = */ null, + // classpathElementIdxWithinParent is the original classpath index, + // for toplevel classpath elements + /* classpathElementIdxWithinParent = */ rawClasspathEntryWorkUnits.size(), + /* packageRootPrefix = */ "")); } // In parallel, create a ClasspathElement singleton for each classpath element, then call open() // on each ClasspathElement object, which in the case of jarfiles will cause LogicalZipFile instances // to be created for each (possibly nested) jarfile, then will read the manifest file and zip entries. - final Set openedClasspathEltsSet = Collections + final Set allClasspathElts = Collections + .newSetFromMap(new ConcurrentHashMap()); + final Set toplevelClasspathElts = Collections .newSetFromMap(new ConcurrentHashMap()); - final Queue> toplevelClasspathEltOrder = new ConcurrentLinkedQueue<>(); processWorkUnits(rawClasspathEntryWorkUnits, topLevelLog == null ? null : topLevelLog.log("Opening classpath elements"), - newClasspathEntryWorkUnitProcessor(openedClasspathEltsSet, toplevelClasspathEltOrder)); + newClasspathEntryWorkUnitProcessor(allClasspathElts, toplevelClasspathElts)); // Determine total ordering of classpath elements, inserting jars referenced in manifest Class-Path // entries in-place into the ordering, if they haven't been listed earlier in the classpath already. - final List classpathEltOrder = findClasspathOrder(openedClasspathEltsSet, - toplevelClasspathEltOrder); + final List classpathEltOrder = findClasspathOrder(toplevelClasspathElts); // Find classpath elements that are path prefixes of other classpath elements, and for // ClasspathElementZip, get module-related manifest entry values - preprocessClasspathElementsByType(classpathEltOrder, log); + preprocessClasspathElementsByType(classpathEltOrder, + topLevelLog == null ? null : topLevelLog.log("Finding nested classpath elements")); // Order modules before classpath elements from traditional classpath - final LogNode classpathOrderLog = log == null ? null : log.log("Final classpath element order:"); - final int numElts = moduleClasspathEltOrder.size() + classpathEltOrder.size(); + final LogNode classpathOrderLog = topLevelLog == null ? null + : topLevelLog.log("Final classpath element order:"); + final int numElts = moduleOrder.size() + classpathEltOrder.size(); final List finalClasspathEltOrder = new ArrayList<>(numElts); final List finalClasspathEltOrderStrs = new ArrayList<>(numElts); - for (final ClasspathElementModule classpathElt : moduleClasspathEltOrder) { + int classpathOrderIdx = 0; + for (final ClasspathElementModule classpathElt : moduleOrder) { + classpathElt.classpathElementIdx = classpathOrderIdx++; finalClasspathEltOrder.add(classpathElt); finalClasspathEltOrderStrs.add(classpathElt.toString()); if (classpathOrderLog != null) { @@ -924,6 +1095,7 @@ private ScanResult openClasspathElementsThenScan() throws InterruptedException, } } for (final ClasspathElement classpathElt : classpathEltOrder) { + classpathElt.classpathElementIdx = classpathOrderIdx++; finalClasspathEltOrder.add(classpathElt); finalClasspathEltOrderStrs.add(classpathElt.toString()); if (classpathOrderLog != null) { @@ -931,7 +1103,7 @@ private ScanResult openClasspathElementsThenScan() throws InterruptedException, } } - // In parallel, scan paths within each classpath element, comparing them against whitelist/blacklist + // In parallel, scan paths within each classpath element, comparing them against accept/reject processWorkUnits(finalClasspathEltOrder, topLevelLog == null ? null : topLevelLog.log("Scanning classpath elements"), new WorkUnitProcessor() { @@ -944,27 +1116,27 @@ public void processWorkUnit(final ClasspathElement classpathElement, } }); - // Filter out classpath elements that do not contain required whitelisted paths. + // Filter out classpath elements that do not contain required accepted paths. List finalClasspathEltOrderFiltered = finalClasspathEltOrder; - if (!scanSpec.classpathElementResourcePathWhiteBlackList.whitelistIsEmpty()) { + if (!scanSpec.classpathElementResourcePathAcceptReject.acceptIsEmpty()) { finalClasspathEltOrderFiltered = new ArrayList<>(finalClasspathEltOrder.size()); for (final ClasspathElement classpathElement : finalClasspathEltOrder) { - if (classpathElement.containsSpecificallyWhitelistedClasspathElementResourcePath) { + if (classpathElement.containsSpecificallyAcceptedClasspathElementResourcePath) { finalClasspathEltOrderFiltered.add(classpathElement); } } } - if (scanSpec.performScan) { + if (performScan) { // Scan classpath / modules, producing a ScanResult. - return performScan(finalClasspathEltOrderFiltered, finalClasspathEltOrderStrs, contextClassLoaders); + return performScan(finalClasspathEltOrderFiltered, finalClasspathEltOrderStrs, classpathFinder); } else { // Only getting classpath -- return a placeholder ScanResult to hold classpath elements if (topLevelLog != null) { topLevelLog.log("Only returning classpath elements (not performing a scan)"); } return new ScanResult(scanSpec, finalClasspathEltOrderFiltered, finalClasspathEltOrderStrs, - contextClassLoaders, /* classNameToClassInfo = */ null, /* packageNameToPackageInfo = */ null, + classpathFinder, /* classNameToClassInfo = */ null, /* packageNameToPackageInfo = */ null, /* moduleNameToModuleInfo = */ null, /* fileToLastModified = */ null, nestedJarHandler, topLevelLog); } @@ -987,8 +1159,8 @@ public void processWorkUnit(final ClasspathElement classpathElement, @Override public ScanResult call() throws InterruptedException, CancellationException, ExecutionException { ScanResult scanResult = null; - Exception exception = null; final long scanStart = System.currentTimeMillis(); + boolean removeTemporaryFilesAfterScan = scanSpec.removeTemporaryFilesAfterScan; try { // Perform the scan scanResult = openClasspathElementsThenScan(); @@ -1002,82 +1174,74 @@ public ScanResult call() throws InterruptedException, CancellationException, Exe // Call the ScanResultProcessor, if one was provided if (scanResultProcessor != null) { - scanResultProcessor.processScanResult(scanResult); + try { + scanResultProcessor.processScanResult(scanResult); + } catch (final Exception e) { + scanResult.close(); + throw new ExecutionException(e); + } + scanResult.close(); } - } catch (final InterruptedException e) { + } catch (final Throwable e) { if (topLevelLog != null) { - topLevelLog.log("~", "Scan interrupted"); + topLevelLog.log("~", + e instanceof InterruptedException || e instanceof CancellationException + ? "Scan interrupted or canceled" + : e instanceof ExecutionException || e instanceof RuntimeException + ? "Uncaught exception during scan" + : e.getMessage(), + InterruptionChecker.getCause(e)); + // Flush the log + topLevelLog.flush(); } - exception = e; + + // Since an exception was thrown, remove temporary files + removeTemporaryFilesAfterScan = true; + + // Stop any running threads (should not be needed, threads should already be quiescent) interruptionChecker.interrupt(); + if (failureHandler == null) { - // Re-throw - throw e; - } - } catch (final CancellationException e) { - if (topLevelLog != null) { - topLevelLog.log("~", "Scan cancelled"); - } - exception = e; - if (failureHandler == null) { - // Re-throw - throw e; - } - } catch (final ExecutionException e) { - if (topLevelLog != null) { - topLevelLog.log("~", "Uncaught exception during scan", InterruptionChecker.getCause(e)); - } - exception = e; - if (failureHandler == null) { - // Re-throw + if (removeTemporaryFilesAfterScan) { + // If removeTemporaryFilesAfterScan was set, remove temp files and close resources, + // zipfiles and modules + nestedJarHandler.close(topLevelLog); + } + // If there is no failure handler set, re-throw the exception throw e; - } - } catch (final RuntimeException e) { - if (topLevelLog != null) { - topLevelLog.log("~", "Uncaught exception during scan", e); - } - exception = e; - if (failureHandler == null) { - // Wrap unchecked exceptions in a new ExecutionException - throw new ExecutionException("Exception while scanning", e); - } - - } finally { - if (exception != null || scanSpec.removeTemporaryFilesAfterScan) { - // If an exception was thrown or removeTemporaryFilesAfterScan was set, remove temporary files - // and close resources, zipfiles, and modules - nestedJarHandler.close(topLevelLog); + } else { + // Otherwise, call the failure handler + try { + failureHandler.onFailure(e); + } catch (final Exception f) { + // The failure handler failed + if (topLevelLog != null) { + topLevelLog.log("~", "The failure handler threw an exception:", f); + topLevelLog.flush(); + } + // Group the two exceptions into one, using the suppressed exception mechanism + // to show the scan exception below the failure handler exception + final ExecutionException failureHandlerException = new ExecutionException( + "Exception while calling failure handler", f); + failureHandlerException.addSuppressed(e); + if (removeTemporaryFilesAfterScan) { + // If removeTemporaryFilesAfterScan was set, remove temp files and close resources, + // zipfiles and modules + nestedJarHandler.close(topLevelLog); + } + // Throw a new ExecutionException (although this will probably be ignored, + // since any job with a FailureHandler was started with ExecutorService::execute + // rather than ExecutorService::submit) + throw failureHandlerException; + } } } - if (exception != null) { - // If an exception was thrown, log the cause, and flush the toplevel log - if (topLevelLog != null) { - final Throwable cause = InterruptionChecker.getCause(exception); - topLevelLog.log("~", "An uncaught exception was thrown:", cause); - topLevelLog.flush(); - } - - // If exception is null, then failureHandler must be non-null at this point - try { - // Call the FailureHandler - failureHandler.onFailure(exception); - } catch (final Exception f) { - // The failure handler failed - if (topLevelLog != null) { - topLevelLog.log("~", "The failure handler threw an exception:", f); - } - // Group the two exceptions into one, using the suppressed exception mechanism - // to show the scan exception below the failure handler exception - final ExecutionException failureHandlerException = new ExecutionException( - "Exception while calling failure handler", f); - failureHandlerException.addSuppressed(exception); - // Throw a new ExecutionException (although this will probably be ignored, - // since any job with a FailureHandler was started with ExecutorService::execute - // rather than ExecutorService::submit) - throw failureHandlerException; - } + if (removeTemporaryFilesAfterScan) { + // If removeTemporaryFilesAfterScan was set, remove temp files and close resources, + // zipfiles and modules + nestedJarHandler.close(topLevelLog); } return scanResult; } diff --git a/src/main/java/io/github/classgraph/TypeArgument.java b/src/main/java/io/github/classgraph/TypeArgument.java index 62c0c2f25..8050cf9eb 100644 --- a/src/main/java/io/github/classgraph/TypeArgument.java +++ b/src/main/java/io/github/classgraph/TypeArgument.java @@ -31,8 +31,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; +import io.github.classgraph.Classfile.TypePathNode; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.Parser; @@ -95,6 +97,26 @@ public ReferenceTypeSignature getTypeSignature() { return typeSignature; } + @Override + protected void addTypeAnnotation(final List typePath, final AnnotationInfo annotationInfo) { + if (typePath.size() == 0 && wildcard != Wildcard.NONE) { + // Annotation before wildcard + addTypeAnnotation(annotationInfo); + } else if (typePath.size() > 0 && typePath.get(0).typePathKind == 2) { + // Annotation is on the bound of a wildcard type argument of a parameterized type. + // TypeSignature can be null in a corrupt classfile (#758). + if (typeSignature != null) { + typeSignature.addTypeAnnotation(typePath.subList(1, typePath.size()), annotationInfo); + } + } else { + // Annotation is on a type argument of a parameterized type. + // TypeSignature can be null in a corrupt classfile (#758). + if (typeSignature != null) { + typeSignature.addTypeAnnotation(typePath, annotationInfo); + } + } + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -197,13 +219,15 @@ void setScanResult(final ScanResult scanResult) { } } - /* (non-Javadoc) - * @see io.github.classgraph.HierarchicalTypeSignature#findReferencedClassNames(java.util.Set) + /** + * Get the names of any classes referenced in the type signature. + * + * @param refdClassNames + * the referenced class names. */ - @Override - void findReferencedClassNames(final Set classNameListOut) { + public void findReferencedClassNames(final Set refdClassNames) { if (typeSignature != null) { - typeSignature.findReferencedClassNames(classNameListOut); + typeSignature.findReferencedClassNames(refdClassNames); } } @@ -214,7 +238,7 @@ void findReferencedClassNames(final Set classNameListOut) { */ @Override public int hashCode() { - return typeSignature.hashCode() + 7 * wildcard.hashCode(); + return (typeSignature != null ? typeSignature.hashCode() : 0) + 7 * wildcard.hashCode(); } /* (non-Javadoc) @@ -222,51 +246,46 @@ public int hashCode() { */ @Override public boolean equals(final Object obj) { - if (!(obj instanceof TypeArgument)) { + if (obj == this) { + return true; + } else if (!(obj instanceof TypeArgument)) { return false; } - final TypeArgument o = (TypeArgument) obj; - return (o.typeSignature.equals(this.typeSignature) && o.wildcard.equals(this.wildcard)); + final TypeArgument other = (TypeArgument) obj; + return Objects.equals(this.typeAnnotationInfo, other.typeAnnotationInfo) + && (Objects.equals(this.typeSignature, other.typeSignature) + && other.wildcard.equals(this.wildcard)); } - /** - * {@link #toString()} internal method. - * - * @param useSimpleNames - * whether to use simple names for classes. - * @return the string - */ - private String toStringInternal(final boolean useSimpleNames) { + // ------------------------------------------------------------------------------------------------------------- + + @Override + protected void toStringInternal(final boolean useSimpleNames, final AnnotationInfoList annotationsToExclude, + final StringBuilder buf) { + if (typeAnnotationInfo != null) { + for (final AnnotationInfo annotationInfo : typeAnnotationInfo) { + if (annotationsToExclude == null || !annotationsToExclude.contains(annotationInfo)) { + annotationInfo.toString(useSimpleNames, buf); + buf.append(' '); + } + } + } switch (wildcard) { case ANY: - return "?"; + buf.append('?'); + break; case EXTENDS: - final String typeSigStr = typeSignature.toString(); - return typeSigStr.equals("java.lang.Object") ? "?" : "? extends " + typeSigStr; + final String typeSigStr = typeSignature.toString(useSimpleNames); + buf.append(typeSigStr.equals("java.lang.Object") ? "?" : "? extends " + typeSigStr); + break; case SUPER: - return "? super " + typeSignature.toString(); + buf.append("? super "); + typeSignature.toString(useSimpleNames, buf); + break; case NONE: - return useSimpleNames ? typeSignature.toStringWithSimpleNames() : typeSignature.toString(); default: - // Should not happen - throw ClassGraphException.newClassGraphException("Unknown wildcard type " + wildcard); + typeSignature.toString(useSimpleNames, buf); + break; } } - - /** - * {@link #toString()} with simple names for classes. - * - * @return the string - */ - public String toStringWithSimpleNames() { - return toStringInternal(true); - } - - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return toStringInternal(false); - } } diff --git a/src/main/java/io/github/classgraph/TypeParameter.java b/src/main/java/io/github/classgraph/TypeParameter.java index e90e4c39a..308e4d039 100644 --- a/src/main/java/io/github/classgraph/TypeParameter.java +++ b/src/main/java/io/github/classgraph/TypeParameter.java @@ -31,8 +31,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; +import io.github.classgraph.Classfile.TypePathNode; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.Parser; import nonapi.io.github.classgraph.types.TypeUtils; @@ -60,7 +62,7 @@ public final class TypeParameter extends HierarchicalTypeSignature { * @param interfaceBounds * The type parameter interface bound. */ - private TypeParameter(final String identifier, final ReferenceTypeSignature classBound, + protected TypeParameter(final String identifier, final ReferenceTypeSignature classBound, final List interfaceBounds) { super(); this.name = identifier; @@ -95,6 +97,15 @@ public List getInterfaceBounds() { return interfaceBounds; } + @Override + protected void addTypeAnnotation(final List typePath, final AnnotationInfo annotationInfo) { + if (typePath.isEmpty()) { + addTypeAnnotation(annotationInfo); + } else { + throw new IllegalArgumentException("Type parameter should have empty typePath"); + } + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -119,7 +130,8 @@ static List parseList(final Parser parser, final String definingC if (!parser.hasMore()) { throw new ParseException(parser, "Missing '>'"); } - if (!TypeUtils.getIdentifierToken(parser)) { + // Scala can contain '$' in type parameter names (#495) + if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ false, /* stopAtDot = */ true)) { throw new ParseException(parser, "Could not parse identifier token"); } final String identifier = parser.currToken(); @@ -182,16 +194,18 @@ void setScanResult(final ScanResult scanResult) { } } - /* (non-Javadoc) - * @see io.github.classgraph.HierarchicalTypeSignature#findReferencedClassNames(java.util.Set) + /** + * Get the names of any classes referenced in the type signature. + * + * @param refdClassNames + * the referenced class names. */ - @Override - void findReferencedClassNames(final Set classNameListOut) { + protected void findReferencedClassNames(final Set refdClassNames) { if (classBound != null) { - classBound.findReferencedClassNames(classNameListOut); + classBound.findReferencedClassNames(refdClassNames); } for (final ReferenceTypeSignature typeSignature : interfaceBounds) { - typeSignature.findReferencedClassNames(classNameListOut); + typeSignature.findReferencedClassNames(refdClassNames); } } @@ -211,29 +225,39 @@ public int hashCode() { */ @Override public boolean equals(final Object obj) { - if (!(obj instanceof TypeParameter)) { + if (obj == this) { + return true; + } else if (!(obj instanceof TypeParameter)) { return false; } - final TypeParameter o = (TypeParameter) obj; - return o.name.equals(this.name) - && ((o.classBound == null && this.classBound == null) - || (o.classBound != null && o.classBound.equals(this.classBound))) - && o.interfaceBounds.equals(this.interfaceBounds); + final TypeParameter other = (TypeParameter) obj; + return other.name.equals(this.name) && Objects.equals(other.typeAnnotationInfo, this.typeAnnotationInfo) + && ((other.classBound == null && this.classBound == null) + || (other.classBound != null && other.classBound.equals(this.classBound))) + && other.interfaceBounds.equals(this.interfaceBounds); } - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ + // ------------------------------------------------------------------------------------------------------------- + @Override - public String toString() { - final StringBuilder buf = new StringBuilder(); - buf.append(name); + protected void toStringInternal(final boolean useSimpleNames, final AnnotationInfoList annotationsToExclude, + final StringBuilder buf) { + if (typeAnnotationInfo != null) { + for (final AnnotationInfo annotationInfo : typeAnnotationInfo) { + if (annotationsToExclude == null || !annotationsToExclude.contains(annotationInfo)) { + annotationInfo.toString(useSimpleNames, buf); + buf.append(' '); + } + } + } + buf.append(useSimpleNames ? ClassInfo.getSimpleName(name) : name); String classBoundStr; if (classBound == null) { classBoundStr = null; } else { - classBoundStr = classBound.toString(); - if (classBoundStr.equals("java.lang.Object")) { + classBoundStr = classBound.toString(useSimpleNames); + if (classBoundStr.equals("java.lang.Object") || (classBoundStr.equals("Object") + && ((ClassRefTypeSignature) classBound).className.equals("java.lang.Object"))) { // Don't add "extends java.lang.Object" classBoundStr = null; } @@ -250,8 +274,7 @@ public String toString() { buf.append(" &"); } buf.append(' '); - buf.append(interfaceBounds.get(i).toString()); + interfaceBounds.get(i).toString(useSimpleNames, buf); } - return buf.toString(); } } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/TypeSignature.java b/src/main/java/io/github/classgraph/TypeSignature.java index 34637a9db..4e8b96b68 100644 --- a/src/main/java/io/github/classgraph/TypeSignature.java +++ b/src/main/java/io/github/classgraph/TypeSignature.java @@ -28,8 +28,15 @@ */ package io.github.classgraph; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.github.classgraph.Classfile.TypePathNode; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.Parser; +import nonapi.io.github.classgraph.utils.LogNode; /** * A type signature for a reference type or base type. Subclasses are {@link ReferenceTypeSignature} (whose own @@ -43,43 +50,55 @@ protected TypeSignature() { } /** - * Compare base types, ignoring generic type parameters. - * - * @param other - * the other {@link TypeSignature} to compare to. - * @return True if the two {@link TypeSignature} objects are equal, ignoring type parameters. + * Get the names of any classes referenced in the type signature. + * + * @param refdClassNames + * the referenced class names. */ - public abstract boolean equalsIgnoringTypeParams(final TypeSignature other); + protected void findReferencedClassNames(final Set refdClassNames) { + final String className = getClassName(); + if (className != null && !className.isEmpty()) { + refdClassNames.add(getClassName()); + } + } /** - * {@link #toString()} method, possibly returning simple names for classes (i.e. if useSimpleNames is true, the - * package names of classes are stripped). + * Get {@link ClassInfo} objects for any classes referenced in the type signature. * - * @param useSimpleNames - * whether or not to use simple names for classes. - * @return the string representation of the type signature, with package names stripped. + * @param classNameToClassInfo + * the map from class name to {@link ClassInfo}. + * @param refdClassInfo + * the referenced class info. */ - protected abstract String toStringInternal(boolean useSimpleNames); + @Override + final protected void findReferencedClassInfo(final Map classNameToClassInfo, + final Set refdClassInfo, final LogNode log) { + final Set refdClassNames = new HashSet<>(); + findReferencedClassNames(refdClassNames); + for (final String refdClassName : refdClassNames) { + final ClassInfo classInfo = ClassInfo.getOrCreateClassInfo(refdClassName, classNameToClassInfo); + classInfo.scanResult = scanResult; + refdClassInfo.add(classInfo); + } + } /** - * {@link #toString()} method, but returning simple names for classes (i.e. the package names of classes are - * stripped). - * - * @return the string representation of the type signature, with package names stripped. + * Get a list of {@link AnnotationInfo} objects for any type annotations on this type, or null if none. + * + * @return a list of {@link AnnotationInfo} objects for any type annotations on this type, or null if none. */ - public String toStringWithSimpleNames() { - return toStringInternal(true); + public AnnotationInfoList getTypeAnnotationInfo() { + return typeAnnotationInfo; } /** - * {@link #toString()} method for type signature. - * - * @return the string representation of the type signature. + * Compare base types, ignoring generic type parameters. + * + * @param other + * the other {@link TypeSignature} to compare to. + * @return True if the two {@link TypeSignature} objects are equal, ignoring type parameters. */ - @Override - public String toString() { - return toStringInternal(false); - } + public abstract boolean equalsIgnoringTypeParams(final TypeSignature other); /** * Parse a type signature. @@ -128,4 +147,15 @@ static TypeSignature parse(final String typeDescriptor, final String definingCla } return typeSignature; } + + /** + * Add a type annotation to this type. + * + * @param typePath + * The type path. + * @param annotationInfo + * The annotation to add. + */ + @Override + protected abstract void addTypeAnnotation(List typePath, AnnotationInfo annotationInfo); } \ No newline at end of file diff --git a/src/main/java/io/github/classgraph/TypeVariableSignature.java b/src/main/java/io/github/classgraph/TypeVariableSignature.java index 9b2367b92..722428c72 100644 --- a/src/main/java/io/github/classgraph/TypeVariableSignature.java +++ b/src/main/java/io/github/classgraph/TypeVariableSignature.java @@ -29,9 +29,12 @@ package io.github.classgraph; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; +import io.github.classgraph.Classfile.TypePathNode; import nonapi.io.github.classgraph.types.ParseException; import nonapi.io.github.classgraph.types.Parser; import nonapi.io.github.classgraph.types.TypeUtils; @@ -47,6 +50,9 @@ public final class TypeVariableSignature extends ClassRefOrTypeVariableSignature /** The method signature that this type variable is part of. */ MethodTypeSignature containingMethodSignature; + /** The resolved type parameter, if any. */ + private TypeParameter typeParameterCached; + // ------------------------------------------------------------------------------------------------------------- /** @@ -85,31 +91,59 @@ public String getName() { * method or the enclosing class. */ public TypeParameter resolve() { + if (typeParameterCached != null) { + return typeParameterCached; + } // Try resolving the type variable against the containing method if (containingMethodSignature != null && containingMethodSignature.typeParameters != null && !containingMethodSignature.typeParameters.isEmpty()) { for (final TypeParameter typeParameter : containingMethodSignature.typeParameters) { if (typeParameter.name.equals(this.name)) { + typeParameterCached = typeParameter; return typeParameter; } } } // If that failed, try resolving the type variable against the containing class - final ClassInfo containingClassInfo = getClassInfo(); - if (containingClassInfo == null) { - throw new IllegalArgumentException("Could not find ClassInfo object for " + definingClassName); - } - final ClassTypeSignature containingClassSignature = containingClassInfo.getTypeSignature(); - if (containingClassSignature != null && containingClassSignature.typeParameters != null - && !containingClassSignature.typeParameters.isEmpty()) { - for (final TypeParameter typeParameter : containingClassSignature.typeParameters) { - if (typeParameter.name.equals(this.name)) { - return typeParameter; + if (getClassName() != null) { + final ClassInfo containingClassInfo = getClassInfo(); + if (containingClassInfo == null) { + throw new IllegalArgumentException("Could not find ClassInfo object for " + definingClassName); + } + ClassTypeSignature containingClassSignature = null; + try { + containingClassSignature = containingClassInfo.getTypeSignature(); + } catch (final Exception e) { + // Ignore + } + if (containingClassSignature != null && containingClassSignature.typeParameters != null + && !containingClassSignature.typeParameters.isEmpty()) { + for (final TypeParameter typeParameter : containingClassSignature.typeParameters) { + if (typeParameter.name.equals(this.name)) { + typeParameterCached = typeParameter; + return typeParameter; + } } } } - throw new IllegalArgumentException( - "Could not resolve " + name + " against parameters of the defining method or enclosing class"); + // If that failed, then this is a type variable that cannot be resolved. + // Return a new TypeParameter that only has the name set, with no class or interface bounds. (#706) + final TypeParameter typeParameter = new TypeParameter(name, null, + Collections. emptyList()); + typeParameter.setScanResult(scanResult); + typeParameterCached = typeParameter; + return typeParameter; + } + + // ------------------------------------------------------------------------------------------------------------- + + @Override + protected void addTypeAnnotation(final List typePath, final AnnotationInfo annotationInfo) { + if (typePath.isEmpty()) { + addTypeAnnotation(annotationInfo); + } else { + throw new IllegalArgumentException("Type variable should have empty typePath"); + } } // ------------------------------------------------------------------------------------------------------------- @@ -129,7 +163,8 @@ static TypeVariableSignature parse(final Parser parser, final String definingCla final char peek = parser.peek(); if (peek == 'T') { parser.next(); - if (!TypeUtils.getIdentifierToken(parser)) { + // Scala can contain '$' in type variable names (#495) + if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ false, /* stopAtDot = */ true)) { throw new ParseException(parser, "Could not parse type variable signature"); } parser.expect(';'); @@ -164,12 +199,24 @@ protected String getClassName() { return definingClassName; } - /* (non-Javadoc) - * @see io.github.classgraph.HierarchicalTypeSignature#findReferencedClassNames(java.util.Set) + /** + * Get the names of any classes referenced in the type signature. + * + * @param refdClassNames + * the referenced class names. */ @Override - void findReferencedClassNames(final Set classNames) { - // No class names present in type variables + protected void findReferencedClassNames(final Set refdClassNames) { + // Any class names present in resolved type variables have to be present in enclosing method or class, + // so there's no need to look up class references in resolved type variables + } + + @Override + void setScanResult(final ScanResult scanResult) { + super.setScanResult(scanResult); + if (typeParameterCached != null) { + typeParameterCached.setScanResult(scanResult); + } } // ------------------------------------------------------------------------------------------------------------- @@ -187,11 +234,13 @@ public int hashCode() { */ @Override public boolean equals(final Object obj) { - if (!(obj instanceof TypeVariableSignature)) { + if (obj == this) { + return true; + } else if (!(obj instanceof TypeVariableSignature)) { return false; } - final TypeVariableSignature o = (TypeVariableSignature) obj; - return o.name.equals(this.name); + final TypeVariableSignature other = (TypeVariableSignature) obj; + return other.name.equals(this.name) && Objects.equals(other.typeAnnotationInfo, this.typeAnnotationInfo); } /* (non-Javadoc) @@ -277,11 +326,17 @@ public String toStringWithTypeBound() { } } - /* (non-Javadoc) - * @see io.github.classgraph.TypeSignature#toStringInternal(boolean) - */ @Override - protected String toStringInternal(final boolean useSimpleNames) { - return name; + protected void toStringInternal(final boolean useSimpleNames, final AnnotationInfoList annotationsToExclude, + final StringBuilder buf) { + if (typeAnnotationInfo != null) { + for (final AnnotationInfo annotationInfo : typeAnnotationInfo) { + if (annotationsToExclude == null || !annotationsToExclude.contains(annotationInfo)) { + annotationInfo.toString(useSimpleNames, buf); + buf.append(' '); + } + } + } + buf.append(name); } } \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/WhiteBlackList.java b/src/main/java/nonapi/io/github/classgraph/WhiteBlackList.java deleted file mode 100644 index 6bc24a807..000000000 --- a/src/main/java/nonapi/io/github/classgraph/WhiteBlackList.java +++ /dev/null @@ -1,640 +0,0 @@ -/* - * This file is part of ClassGraph. - * - * Author: Luke Hutchison - * - * Hosted at: https://github.com/classgraph/classgraph - * - * -- - * - * The MIT License (MIT) - * - * Copyright (c) 2019 Luke Hutchison - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT - * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO - * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE - * OR OTHER DEALINGS IN THE SOFTWARE. - */ -package nonapi.io.github.classgraph; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; - -import nonapi.io.github.classgraph.utils.FileUtils; -import nonapi.io.github.classgraph.utils.JarUtils; - -/** A class storing whitelist or blacklist criteria. */ -public abstract class WhiteBlackList { - /** Whitelisted items (whole-string match). */ - protected Set whitelist; - /** Blacklisted items (whole-string match). */ - protected Set blacklist; - /** Whitelisted items (prefix match). */ - protected List whitelistPrefixes; - /** Blacklisted items (prefix match). */ - protected List blacklistPrefixes; - /** Whitelist glob strings. (Serialized to JSON, for logging purposes.) */ - protected Set whitelistGlobs; - /** Blacklist glob strings. (Serialized to JSON, for logging purposes.) */ - protected Set blacklistGlobs; - /** Whitelist regexp patterns. (Not serialized to JSON.) */ - protected transient List whitelistPatterns; - /** Blacklist regexp patterns. (Not serialized to JSON.) */ - protected transient List blacklistPatterns; - - /** Constructor for deserialization. */ - public WhiteBlackList() { - // Empty - } - - /** Whitelist/blacklist for prefix strings. */ - public static class WhiteBlackListPrefix extends WhiteBlackList { - /** - * Add to the whitelist. - * - * @param str - * the string to whitelist - */ - @Override - public void addToWhitelist(final String str) { - if (str.contains("*")) { - throw new IllegalArgumentException("Cannot use a glob wildcard here: " + str); - } - if (this.whitelistPrefixes == null) { - this.whitelistPrefixes = new ArrayList<>(); - } - this.whitelistPrefixes.add(str); - } - - /** - * Add to the blacklist. - * - * @param str - * the string to blacklist - */ - @Override - public void addToBlacklist(final String str) { - if (str.contains("*")) { - throw new IllegalArgumentException("Cannot use a glob wildcard here: " + str); - } - if (this.blacklistPrefixes == null) { - this.blacklistPrefixes = new ArrayList<>(); - } - this.blacklistPrefixes.add(str); - } - - /** - * Check if the requested string has a whitelisted/non-blacklisted prefix. - * - * @param str - * the string to test - * @return true if string is whitelisted and not blacklisted - */ - @Override - public boolean isWhitelistedAndNotBlacklisted(final String str) { - boolean isWhitelisted = whitelistPrefixes == null; - if (!isWhitelisted) { - for (final String prefix : whitelistPrefixes) { - if (str.startsWith(prefix)) { - isWhitelisted = true; - break; - } - } - } - if (!isWhitelisted) { - return false; - } - if (blacklistPrefixes != null) { - for (final String prefix : blacklistPrefixes) { - if (str.startsWith(prefix)) { - return false; - } - } - } - return true; - } - - /** - * Check if the requested string has a whitelisted prefix. - * - * @param str - * the string to test - * @return true if string is whitelisted - */ - @Override - public boolean isWhitelisted(final String str) { - boolean isWhitelisted = whitelistPrefixes == null; - if (!isWhitelisted) { - for (final String prefix : whitelistPrefixes) { - if (str.startsWith(prefix)) { - isWhitelisted = true; - break; - } - } - } - return isWhitelisted; - } - - /** - * Prefix-of-prefix is invalid -- throws {@link IllegalArgumentException}. - * - * @param str - * the string to test - * @return (does not return, throws exception) - * @throws IllegalArgumentException - * always - */ - @Override - public boolean whitelistHasPrefix(final String str) { - throw new IllegalArgumentException("Can only find prefixes of whole strings"); - } - - /** - * Check if the requested string has a blacklisted prefix. - * - * @param str - * the string to test - * @return true if the string has a blacklisted prefix - */ - @Override - public boolean isBlacklisted(final String str) { - if (blacklistPrefixes != null) { - for (final String prefix : blacklistPrefixes) { - if (str.startsWith(prefix)) { - return true; - } - } - } - return false; - } - } - - /** Whitelist/blacklist for whole-strings matches. */ - public static class WhiteBlackListWholeString extends WhiteBlackList { - /** - * Add to the whitelist. - * - * @param str - * the string to whitelist - */ - @Override - public void addToWhitelist(final String str) { - if (str.contains("*")) { - if (this.whitelistGlobs == null) { - this.whitelistGlobs = new HashSet<>(); - this.whitelistPatterns = new ArrayList<>(); - } - this.whitelistGlobs.add(str); - this.whitelistPatterns.add(globToPattern(str)); - } else { - if (this.whitelist == null) { - this.whitelist = new HashSet<>(); - } - this.whitelist.add(str); - } - } - - /** - * Add to the blacklist. - * - * @param str - * the string to blacklist - */ - @Override - public void addToBlacklist(final String str) { - if (str.contains("*")) { - if (this.blacklistGlobs == null) { - this.blacklistGlobs = new HashSet<>(); - this.blacklistPatterns = new ArrayList<>(); - } - this.blacklistGlobs.add(str); - this.blacklistPatterns.add(globToPattern(str)); - } else { - if (this.blacklist == null) { - this.blacklist = new HashSet<>(); - } - this.blacklist.add(str); - } - } - - /** - * Check if the requested string is whitelisted and not blacklisted. - * - * @param str - * the string to test - * @return true if the string is whitelisted and not blacklisted - */ - @Override - public boolean isWhitelistedAndNotBlacklisted(final String str) { - return isWhitelisted(str) && !isBlacklisted(str); - } - - /** - * Check if the requested string is whitelisted. - * - * @param str - * the string to test - * @return true if the string is whitelisted - */ - @Override - public boolean isWhitelisted(final String str) { - return (whitelist == null && whitelistPatterns == null) - || (whitelist != null && whitelist.contains(str)) || matchesPatternList(str, whitelistPatterns); - } - - /** - * Check if the requested string is a prefix of a whitelisted string. - * - * @param str - * the string to test - * @return true if the string is a prefix of a whitelisted string - */ - @Override - public boolean whitelistHasPrefix(final String str) { - if (whitelist == null) { - return false; - } - for (final String w : whitelist) { - if (w.startsWith(str)) { - return true; - } - } - return false; - } - - /** - * Check if the requested string is blacklisted. - * - * @param str - * the string to test - * @return true if the string is blacklisted - */ - @Override - public boolean isBlacklisted(final String str) { - return (blacklist != null && blacklist.contains(str)) || matchesPatternList(str, blacklistPatterns); - } - } - - /** Whitelist/blacklist for prefix strings. */ - public static class WhiteBlackListLeafname extends WhiteBlackListWholeString { - - /** - * Add to the whitelist. - * - * @param str - * the string to whitelist - */ - @Override - public void addToWhitelist(final String str) { - super.addToWhitelist(JarUtils.leafName(str)); - } - - /** - * Add to the blacklist. - * - * @param str - * the string to blacklist - */ - @Override - public void addToBlacklist(final String str) { - super.addToBlacklist(JarUtils.leafName(str)); - } - - /** - * Check if the requested string is whitelisted and not blacklisted. - * - * @param str - * the string to test - * @return true if the string is whitelisted and not blacklisted - */ - @Override - public boolean isWhitelistedAndNotBlacklisted(final String str) { - return super.isWhitelistedAndNotBlacklisted(JarUtils.leafName(str)); - } - - /** - * Check if the requested string is whitelisted. - * - * @param str - * the string to test - * @return true if the string is whitelisted - */ - @Override - public boolean isWhitelisted(final String str) { - return super.isWhitelisted(JarUtils.leafName(str)); - } - - /** - * Prefix tests are invalid for jar leafnames -- throws {@link IllegalArgumentException}. - * - * @param str - * the string to test - * @return (does not return, throws exception) - * @throws IllegalArgumentException - * always - */ - @Override - public boolean whitelistHasPrefix(final String str) { - throw new IllegalArgumentException("Can only find prefixes of whole strings"); - } - - /** - * Check if the requested string is blacklisted. - * - * @param str - * the string to test - * @return true if the string is blacklisted - */ - @Override - public boolean isBlacklisted(final String str) { - return super.isBlacklisted(JarUtils.leafName(str)); - } - } - - /** - * Add to the whitelist. - * - * @param str - * The string to whitelist. - */ - public abstract void addToWhitelist(final String str); - - /** - * Add to the blacklist. - * - * @param str - * The string to blacklist. - */ - public abstract void addToBlacklist(final String str); - - /** - * Check if a string is whitelisted and not blacklisted. - * - * @param str - * The string to test. - * @return true if the string is whitelisted and not blacklisted. - */ - public abstract boolean isWhitelistedAndNotBlacklisted(final String str); - - /** - * Check if a string is whitelisted. - * - * @param str - * The string to test. - * @return true if the string is whitelisted. - */ - public abstract boolean isWhitelisted(final String str); - - /** - * Check if a string is a prefix of a whitelisted string. - * - * @param str - * The string to test. - * @return true if the string is a prefix of a whitelisted string. - */ - public abstract boolean whitelistHasPrefix(final String str); - - /** - * Check if a string is blacklisted. - * - * @param str - * The string to test. - * @return true if the string is blacklisted. - */ - public abstract boolean isBlacklisted(final String str); - - /** - * Remove initial and final '/' characters, if any. - * - * @param path - * The path to normalize. - * @return The normalized path. - */ - public static String normalizePath(final String path) { - return FileUtils.sanitizeEntryPath(path, /* removeInitialSlash = */ true); - } - - /** - * Remove initial and final '.' characters, if any. - * - * @param packageOrClassName - * The package or class name. - * @return The normalized package or class name. - */ - public static String normalizePackageOrClassName(final String packageOrClassName) { - return normalizePath(packageOrClassName.replace('.', '/')).replace('/', '.'); - } - - /** - * Convert a path to a package name. - * - * @param path - * The path. - * @return The package name. - */ - public static String pathToPackageName(final String path) { - return path.replace('/', '.'); - } - - /** - * Convert a package name to a path. - * - * @param packageName - * The package name. - * @return The path. - */ - public static String packageNameToPath(final String packageName) { - return packageName.replace('.', '/'); - } - - /** - * Convert a class name to a classfile path. - * - * @param className - * The class name. - * @return The classfile path (including a ".class" suffix). - */ - public static String classNameToClassfilePath(final String className) { - return JarUtils.classNameToClassfilePath(className); - } - - /** - * Convert a spec with a '*' glob character into a regular expression. Replaces "." with "\." and "*" with ".*", - * then compiles a regular expression. - * - * @param glob - * The glob string. - * @return The Pattern created from the glob string. - */ - public static Pattern globToPattern(final String glob) { - return Pattern.compile("^" + glob.replace(".", "\\.").replace("*", ".*") + "$"); - } - - /** - * Check if a string matches one of the patterns in the provided list. - * - * @param str - * the string to test - * @param patterns - * the patterns - * @return true, if successful - */ - private static boolean matchesPatternList(final String str, final List patterns) { - if (patterns != null) { - for (final Pattern pattern : patterns) { - if (pattern.matcher(str).matches()) { - return true; - } - } - } - return false; - } - - /** - * Check if the whitelist is empty. - * - * @return true if there were no whitelist criteria added. - */ - public boolean whitelistIsEmpty() { - return whitelist == null && whitelistPrefixes == null && whitelistGlobs == null; - } - - /** - * Check if the blacklist is empty. - * - * @return true if there were no blacklist criteria added. - */ - public boolean blacklistIsEmpty() { - return blacklist == null && blacklistPrefixes == null && blacklistGlobs == null; - } - - /** - * Check if the whitelist and blacklist are empty. - * - * @return true if there were no whitelist or blacklist criteria added. - */ - public boolean whitelistAndBlacklistAreEmpty() { - return whitelistIsEmpty() && blacklistIsEmpty(); - } - - /** - * Check if a string is specifically whitelisted and not blacklisted. - * - * @param str - * The string to test. - * @return true if the requested string is specifically whitelisted and not blacklisted, i.e. will not - * return true if the whitelist is empty, or if the string is blacklisted. - */ - public boolean isSpecificallyWhitelistedAndNotBlacklisted(final String str) { - return !whitelistIsEmpty() && isWhitelistedAndNotBlacklisted(str); - } - - /** - * Check if a string is specifically whitelisted. - * - * @param str - * The string to test. - * @return true if the requested string is specifically whitelisted, i.e. will not return true if the - * whitelist is empty. - */ - public boolean isSpecificallyWhitelisted(final String str) { - return !whitelistIsEmpty() && isWhitelisted(str); - } - - /** Need to sort prefixes to ensure correct whitelist/blacklist evaluation (see Issue #167). */ - void sortPrefixes() { - if (whitelistPrefixes != null) { - Collections.sort(whitelistPrefixes); - } - if (blacklistPrefixes != null) { - Collections.sort(blacklistPrefixes); - } - } - - private static void quoteList(final Collection coll, final StringBuilder buf) { - buf.append('['); - boolean first = true; - for (final String item : coll) { - if (first) { - first = false; - } else { - buf.append(", "); - } - buf.append('"'); - for (int i = 0; i < item.length(); i++) { - final char c = item.charAt(i); - if (c == '"') { - buf.append("\\\""); - } else { - buf.append(c); - } - } - buf.append('"'); - } - buf.append(']'); - } - - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - final StringBuilder buf = new StringBuilder(); - if (whitelist != null) { - buf.append("whitelist: "); - quoteList(whitelist, buf); - } - if (whitelistPrefixes != null) { - if (buf.length() > 0) { - buf.append("; "); - } - buf.append("whitelistPrefixes: "); - quoteList(whitelistPrefixes, buf); - } - if (whitelistGlobs != null) { - if (buf.length() > 0) { - buf.append("; "); - } - buf.append("whitelistGlobs: "); - quoteList(whitelistGlobs, buf); - } - if (blacklist != null) { - if (buf.length() > 0) { - buf.append("; "); - } - buf.append("blacklist: "); - quoteList(blacklist, buf); - } - if (blacklistPrefixes != null) { - if (buf.length() > 0) { - buf.append("; "); - } - buf.append("blacklistPrefixes: "); - quoteList(blacklistPrefixes, buf); - } - if (blacklistGlobs != null) { - if (buf.length() > 0) { - buf.append("; "); - } - buf.append("blacklistGlobs: "); - quoteList(blacklistGlobs, buf); - } - return buf.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/AntClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/AntClassLoaderHandler.java index 578170575..3a3b66a04 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/AntClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/AntClassLoaderHandler.java @@ -28,45 +28,64 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** Extract classpath entries from the Ant ClassLoader. */ class AntClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { "org.apache.tools.ant.AntClassLoader" }; + /** Class cannot be constructed. */ + private AntClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.apache.tools.ant.AntClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { - classpathOrderOut.addClasspathEntries( // - (String) ReflectionUtils.invokeMethod(classLoader, "getClasspath", false), classLoader, log); + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + classpathOrder.addClasspathPathStr( + (String) classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getClasspath"), + classLoader, scanSpec, log); } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassGraphClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassGraphClassLoaderHandler.java new file mode 100644 index 000000000..256157df6 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassGraphClassLoaderHandler.java @@ -0,0 +1,110 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.classloaderhandler; + +import java.net.URL; + +import io.github.classgraph.ClassGraphClassLoader; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; +import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.LogNode; + +/** + * Allow for overrideClassLoaders to be called with a ClassGraphClassLoader as a parameter, so that nested scans can + * share a single classloader (#485). + */ +class ClassGraphClassLoaderHandler implements ClassLoaderHandler { + /** Class cannot be constructed. */ + private ClassGraphClassLoaderHandler() { + } + + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. + */ + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + final boolean matches = ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "io.github.classgraph.ClassGraphClassLoader"); + if (matches && log != null) { + log.log("Sharing a `ClassGraphClassLoader` between multiple nested scans is not advisable, " + + "because scan criteria may differ between scans. " + + "See: https://github.com/classgraph/classgraph/issues/485"); + } + return matches; + } + + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log + */ + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); + } + + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. + */ + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + // ClassGraphClassLoader overrides URLClassLoader, so we can get the basic classpath URLs the same + // way as for URLClassLoader. However, classloading will try to preferentially reuse the older + // ClassGraphClassLoader before loading with the new ClassGraphClassLoader from the current scan, + // so the following URLs will be scanned by the current scan, but classes will only be loaded from + // these URLs if the older classloader fails. + for (final URL url : ((ClassGraphClassLoader) classLoader).getURLs()) { + if (url != null) { + classpathOrder.addClasspathEntry(url, classLoader, scanSpec, log); + } + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandler.java index 837dc310d..b6fff3a71 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandler.java @@ -28,10 +28,6 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import nonapi.io.github.classgraph.ScanSpec; -import nonapi.io.github.classgraph.classpath.ClasspathOrder; -import nonapi.io.github.classgraph.utils.LogNode; - /** * A ClassLoader handler. * @@ -39,67 +35,4 @@ * If you create a custom ClassLoaderHandler, please consider submitting it to the ClassGraph open source project. */ public interface ClassLoaderHandler { - /** - * The fully-qualified names of handled classloader classes. - * - * @return The names of ClassLoaders that this ClassLoaderHandler can handle. - */ - String[] handledClassLoaders(); - - /** - * The delegation order configuration for a given ClassLoader instance (this is usually PARENT_FIRST for most - * ClassLoaders, but this can be overridden by some ClassLoaders, e.g. WebSphere). - */ - enum DelegationOrder { - /** Delegate to parent before handling in child. */ - PARENT_FIRST, - /** Handle classloading in child before delegating to parent. */ - PARENT_LAST - } - - /** - * If this ClassLoader delegates directly to an embedded classloader instance, return it here, otherwise return - * null. - * - * @param outerClassLoaderInstance - * The outer ClassLoader instance to check for an embedded ClassLoader. - * @return The embedded ClassLoader to use instead of the outer ClassLoader, or null to use the outer - * ClassLoader. - */ - ClassLoader getEmbeddedClassLoader(ClassLoader outerClassLoaderInstance); - - /** - * The delegation order configuration for a given ClassLoader instance (this is usually PARENT_FIRST for most - * ClassLoaders, since you don't generally want to be able to override system classes with user classes, but - * this can be overridden by some ClassLoaders, e.g. WebSphere). - * - * @param classLoaderInstance - * The ClassLoader to get the delegation order for. - * @return The delegation order for the given ClassLoader. - */ - DelegationOrder getDelegationOrder(ClassLoader classLoaderInstance); - - /** - * Determine if a given ClassLoader can be handled (meaning that its classpath elements can be extracted from - * it), and if it can, extract the classpath elements from the ClassLoader and register them with the - * ClasspathFinder using classpathFinder.addClasspathElement(pathElement) or - * classpathFinder.addClasspathElements(path). - * - * @param scanSpec - * the scanning specification, in case it is needed, e.g. this could be used to reduce the number of - * classpath elements returned in cases where it is very costly for a given classloader to return the - * entire classpath. (The ScanSpec can be safely ignored, however, and the returned paths will be - * filtered by ClassGraph.) - * @param classLoader - * The ClassLoader class to attempt to handle. If you can't directly use instanceof (because you are - * using introspection so that your ClassLoaderHandler implementation can be added to the upstream - * ClassGraph project), you should iterate through the ClassLoader's superclass lineage to ensure - * subclasses of the target ClassLoader are correctly detected. - * @param classpathOrderOut - * The ClasspathOrder to register any discovered classpath elements with. - * @param log - * A logger instance -- if this is non-null, write debug information using log.log("message"). - */ - void handle(ScanSpec scanSpec, final ClassLoader classLoader, final ClasspathOrder classpathOrderOut, - LogNode log); } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandlerRegistry.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandlerRegistry.java index ff8912423..5da03c36c 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandlerRegistry.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandlerRegistry.java @@ -28,54 +28,62 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import java.util.ArrayList; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.List; -import io.github.classgraph.ClassGraphException; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; +import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; /** The registry for ClassLoaderHandler classes. */ public class ClassLoaderHandlerRegistry { /** - * Default ClassLoaderHandlers. + * Default ClassLoaderHandlers. If a ClassLoaderHandler is added to ClassGraph, it should be added to this list. */ - public static final List CLASS_LOADER_HANDLERS; - - static { - // If a ClassLoaderHandler is added to ClassGraph, it should be added to this list. - final List builtInHandlers = Arrays.asList( - // ClassLoaderHandlers for other ClassLoaders that are handled by ClassGraph - new ClassLoaderHandlerRegistryEntry(AntClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(EquinoxClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(EquinoxContextFinderClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(FelixClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(JBossClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(WeblogicClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(WebsphereLibertyClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(WebsphereTraditionalClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(OSGiDefaultClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(SpringBootRestartClassLoaderHandler.class), - new ClassLoaderHandlerRegistryEntry(TomcatWebappClassLoaderBaseHandler.class), - - // For unit testing of PARENT_LAST delegation order - new ClassLoaderHandlerRegistryEntry(ParentLastDelegationOrderTestClassLoaderHandler.class), - - // JPMS support - new ClassLoaderHandlerRegistryEntry(JPMSClassLoaderHandler.class), - - // Java 7/8 support (list last, as fallback) - new ClassLoaderHandlerRegistryEntry(URLClassLoaderHandler.class)); - - final List registeredHandlers = new ArrayList<>(builtInHandlers); - - CLASS_LOADER_HANDLERS = Collections.unmodifiableList(registeredHandlers); - } - - /** The fallback ClassLoaderHandler. Do not need to add FallbackClassLoaderHandler to the above list. */ - public static final ClassLoaderHandlerRegistryEntry FALLBACK_CLASS_LOADER_HANDLER = // - new ClassLoaderHandlerRegistryEntry(FallbackClassLoaderHandler.class); + @SuppressWarnings("null") + public static final List CLASS_LOADER_HANDLERS = // + Collections.unmodifiableList(Arrays.asList( + // ClassLoaderHandlers for other ClassLoaders that are handled by ClassGraph + new ClassLoaderHandlerRegistryEntry(AntClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(EquinoxClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(EquinoxContextFinderClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(FelixClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(JBossClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(WeblogicClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(WebsphereLibertyClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(WebsphereTraditionalClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(OSGiDefaultClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(SpringBootRestartClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(TomcatWebappClassLoaderBaseHandler.class), + new ClassLoaderHandlerRegistryEntry(CxfContainerClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(PlexusClassWorldsClassRealmClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(QuarkusClassLoaderHandler.class), + new ClassLoaderHandlerRegistryEntry(UnoOneJarClassLoaderHandler.class), + + // For unit testing of PARENT_LAST delegation order + new ClassLoaderHandlerRegistryEntry(ParentLastDelegationOrderTestClassLoaderHandler.class), + + // JPMS support (this handler does nothing, since modules are handled separately) + new ClassLoaderHandlerRegistryEntry(JPMSClassLoaderHandler.class), + + // Java 7/8 URLClassLoader support (should be second-to-last, so that subclasses of + // URLClassLoader are handled by more specific handlers above) + new ClassLoaderHandlerRegistryEntry(URLClassLoaderHandler.class), + + // Placeholder for delegation to a ClassGraphClassLoader instance from an outer nested scan + new ClassLoaderHandlerRegistryEntry(ClassGraphClassLoaderHandler.class) + + // FallbackClassLoaderHandler.class is registered separately below + )); + + /** Fallback ClassLoaderHandler. */ + public static final ClassLoaderHandlerRegistryEntry FALLBACK_HANDLER = new ClassLoaderHandlerRegistryEntry( + FallbackClassLoaderHandler.class); + + // ------------------------------------------------------------------------------------------------------------- /** * Lib dirs whose jars should be added to the classpath automatically (to compensate for some classloaders not @@ -83,13 +91,18 @@ public class ClassLoaderHandlerRegistry { */ public static final String[] AUTOMATIC_LIB_DIR_PREFIXES = { // Spring-Boot - "BOOT-INF/lib/", "BOOT-INF/lib-provided/", + // https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/reference/html/appendix-executable-jar-format.html + "BOOT-INF/lib/", // Tomcat "WEB-INF/lib/", "WEB-INF/lib-provided/", + // OSGi + "META-INF/lib/", // Tomcat and others "lib/", // Extension dir - "lib/ext/" // + "lib/ext/", + // UnoJar and One-Jar + "main/" // }; /** @@ -104,8 +117,9 @@ public class ClassLoaderHandlerRegistry { // Spring-Boot "BOOT-INF/classes/", // Tomcat - "WEB-INF/classes/" // - }; + "WEB-INF/classes/", }; + + // ------------------------------------------------------------------------------------------------------------- /** * Constructor. @@ -118,9 +132,16 @@ private ClassLoaderHandlerRegistry() { * A list of fully-qualified ClassLoader class names paired with the ClassLoaderHandler that can handle them. */ public static class ClassLoaderHandlerRegistryEntry { - /** The names of handled ClassLoaders. */ - public final String[] handledClassLoaderNames; - /** The ClassLoader class.. */ + /** canHandle method. */ + private final Method canHandleMethod; + + /** findClassLoaderOrder method. */ + private final Method findClassLoaderOrderMethod; + + /** findClasspathOrder method. */ + private final Method findClasspathOrderMethod; + + /** The ClassLoaderHandler class. */ public final Class classLoaderHandlerClass; /** @@ -130,34 +151,92 @@ public static class ClassLoaderHandlerRegistryEntry { * The ClassLoaderHandler class. */ private ClassLoaderHandlerRegistryEntry(final Class classLoaderHandlerClass) { + // TODO: replace these with MethodHandles for speed + // TODO: (although MethodHandles are disabled for now, due to Animal Sniffer bug): + // https://github.com/mojohaus/animal-sniffer/issues/67 this.classLoaderHandlerClass = classLoaderHandlerClass; try { - // Instantiate each ClassLoaderHandler in order to call the handledClassLoaders() method (this is - // needed because Java doesn't support inherited static interface methods) - this.handledClassLoaderNames = classLoaderHandlerClass.getDeclaredConstructor().newInstance() - .handledClassLoaders(); - } catch (final ReflectiveOperationException | ExceptionInInitializerError e) { - throw ClassGraphException - .newClassGraphException("Could not instantiate " + classLoaderHandlerClass.getName(), e); + canHandleMethod = classLoaderHandlerClass.getDeclaredMethod("canHandle", Class.class, + LogNode.class); + } catch (final Exception e) { + throw new RuntimeException( + "Could not find canHandle method for " + classLoaderHandlerClass.getName(), e); + } + try { + findClassLoaderOrderMethod = classLoaderHandlerClass.getDeclaredMethod("findClassLoaderOrder", + ClassLoader.class, ClassLoaderOrder.class, LogNode.class); + } catch (final Exception e) { + throw new RuntimeException( + "Could not find findClassLoaderOrder method for " + classLoaderHandlerClass.getName(), e); + } + try { + findClasspathOrderMethod = classLoaderHandlerClass.getDeclaredMethod("findClasspathOrder", + ClassLoader.class, ClasspathOrder.class, ScanSpec.class, LogNode.class); + } catch (final Exception e) { + throw new RuntimeException( + "Could not find findClasspathOrder method for " + classLoaderHandlerClass.getName(), e); + } + } + + /** + * Call the static method canHandle(ClassLoader) for the associated {@link ClassLoaderHandler}. + * + * @param classLoader + * the {@link ClassLoader}. + * @param log + * the log. + * @return true, if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. + */ + public boolean canHandle(final Class classLoader, final LogNode log) { + try { + return (boolean) canHandleMethod.invoke(null, classLoader, log); + } catch (final Throwable e) { + throw new RuntimeException( + "Exception while calling canHandle for " + classLoaderHandlerClass.getName(), e); + } + } + + /** + * Call the static method findClassLoaderOrder(ClassLoader, ClassLoaderOrder) for the associated + * {@link ClassLoaderHandler}. + * + * @param classLoader + * the {@link ClassLoader}. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object. + * @param log + * the log + */ + public void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + try { + findClassLoaderOrderMethod.invoke(null, classLoader, classLoaderOrder, log); + } catch (final Throwable e) { + throw new RuntimeException( + "Exception while calling findClassLoaderOrder for " + classLoaderHandlerClass.getName(), e); } } /** - * Instantiate a ClassLoaderHandler, or return null if the class could not be instantiated. + * Call the static method findClasspathOrder(ClassLoader, ClasspathOrder) for the associated + * {@link ClassLoaderHandler}. * + * @param classLoader + * the {@link ClassLoader}. + * @param classpathOrder + * a {@link ClasspathOrder} object. + * @param scanSpec + * the {@link ScanSpec}. * @param log - * The log. - * @return The ClassLoaderHandler instance. + * the log. */ - public ClassLoaderHandler instantiate(final LogNode log) { + public void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { try { - // Instantiate a ClassLoaderHandler - return classLoaderHandlerClass.getDeclaredConstructor().newInstance(); - } catch (final ReflectiveOperationException | ExceptionInInitializerError e) { - if (log != null) { - log.log("Could not instantiate " + classLoaderHandlerClass.getName(), e); - } - return null; + findClasspathOrderMethod.invoke(null, classLoader, classpathOrder, scanSpec, log); + } catch (final Throwable e) { + throw new RuntimeException( + "Exception while calling findClassLoaderOrder for " + classLoaderHandlerClass.getName(), e); } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/CxfContainerClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/CxfContainerClassLoaderHandler.java new file mode 100644 index 000000000..dbd6be4d4 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/CxfContainerClassLoaderHandler.java @@ -0,0 +1,100 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.classloaderhandler; + +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; +import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.LogNode; + +/** ClassLoaderHandler that is able to extract the URLs from a CxfContainerClassLoader. */ +class CxfContainerClassLoaderHandler implements ClassLoaderHandler { + /** Class cannot be constructed. */ + private CxfContainerClassLoaderHandler() { + } + + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. + */ + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.apache.openejb.server.cxf.transport.util.CxfContainerClassLoader"); + } + + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log + */ + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + try { + classLoaderOrder.delegateTo( + Class.forName("org.apache.openejb.server.cxf.transport.util.CxfUtil").getClassLoader(), + /* isParent = */ true, log); + } catch (LinkageError | ClassNotFoundException e) { + // Ignore + } + // tccl = TomcatClassLoader + classLoaderOrder.delegateTo( + (ClassLoader) classLoaderOrder.reflectionUtils.invokeMethod(false, classLoader, "tccl"), + /* isParent = */ false, log); + // This classloader doesn't actually load any classes, but add it to the order to improve logging + classLoaderOrder.add(classLoader, log); + } + + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. + */ + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + // Classloader doesn't do any classloading of its own, it only delegates to other classloaders + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxClassLoaderHandler.java index cfbb31690..ab601a60b 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxClassLoaderHandler.java @@ -29,51 +29,60 @@ package nonapi.io.github.classgraph.classloaderhandler; import java.lang.reflect.Array; -import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Set; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * Extract classpath entries from the Eclipse Equinox ClassLoader. */ class EquinoxClassLoaderHandler implements ClassLoaderHandler { + /** + * True if system bundles have been read. We assume there is only one system bundle on the classpath, so this is + * static. + */ + private static boolean alreadyReadSystemBundles; /** Field names. */ - private static final List FIELD_NAMES = Collections - .unmodifiableList(Arrays.asList("cp", "nestedDirName")); - - /** True if system bundles have been read. */ - private boolean alreadyReadSystemBundles; + private static final String[] FIELD_NAMES = { "cp", "nestedDirName" }; - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { "org.eclipse.osgi.internal.loader.EquinoxClassLoader" }; + /** Class cannot be constructed. */ + private EquinoxClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.eclipse.osgi.internal.loader.EquinoxClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } /** @@ -87,38 +96,54 @@ public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) * the classloader * @param classpathOrderOut * the classpath order + * @param scanSpec + * the scan spec * @param log * the log */ - private void addBundleFile(final Object bundlefile, final Set path, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + private static void addBundleFile(final Object bundlefile, final Set path, + final ClassLoader classLoader, final ClasspathOrder classpathOrderOut, final ScanSpec scanSpec, + final LogNode log) { // Don't get stuck in infinite loop if (bundlefile != null && path.add(bundlefile)) { // type File - final Object basefile = ReflectionUtils.getFieldVal(bundlefile, "basefile", false); - if (basefile != null) { + final Object baseFile = classpathOrderOut.reflectionUtils.getFieldVal(false, bundlefile, "basefile"); + if (baseFile != null) { boolean foundClassPathElement = false; for (final String fieldName : FIELD_NAMES) { - final Object fieldVal = ReflectionUtils.getFieldVal(bundlefile, fieldName, false); - foundClassPathElement = fieldVal != null; - if (foundClassPathElement) { + final Object fieldVal = classpathOrderOut.reflectionUtils.getFieldVal(false, bundlefile, + fieldName); + if (fieldVal != null) { + foundClassPathElement = true; // We found the base file and a classpath element, e.g. "bin/" - classpathOrderOut.addClasspathEntry(basefile.toString() + "/" + fieldVal.toString(), - classLoader, log); + Object base = baseFile; + String sep = "/"; + if (bundlefile.getClass().getName() + .equals("org.eclipse.osgi.storage.bundlefile.NestedDirBundleFile")) { + // Handle nested ZipBundleFile with "!/" separator + final Object baseBundleFile = classpathOrderOut.reflectionUtils.getFieldVal(false, + bundlefile, "baseBundleFile"); + if (baseBundleFile != null && baseBundleFile.getClass().getName() + .equals("org.eclipse.osgi.storage.bundlefile.ZipBundleFile")) { + base = baseBundleFile; + sep = "!/"; + } + } + final String pathElement = base + sep + fieldVal; + classpathOrderOut.addClasspathEntry(pathElement, classLoader, scanSpec, log); break; } } - if (!foundClassPathElement) { // No classpath element found, just use basefile - classpathOrderOut.addClasspathEntry(basefile.toString(), classLoader, log); + classpathOrderOut.addClasspathEntry(baseFile.toString(), classLoader, scanSpec, log); } } - addBundleFile(ReflectionUtils.getFieldVal(bundlefile, "wrapped", false), path, classLoader, - classpathOrderOut, log); - addBundleFile(ReflectionUtils.getFieldVal(bundlefile, "next", false), path, classLoader, - classpathOrderOut, log); + addBundleFile(classpathOrderOut.reflectionUtils.getFieldVal(false, bundlefile, "wrapped"), path, + classLoader, classpathOrderOut, scanSpec, log); + addBundleFile(classpathOrderOut.reflectionUtils.getFieldVal(false, bundlefile, "next"), path, + classLoader, classpathOrderOut, scanSpec, log); } } @@ -131,80 +156,95 @@ private void addBundleFile(final Object bundlefile, final Set path, fina * the class loader * @param classpathOrderOut * the classpath order out + * @param scanSpec + * the scan spec * @param log * the log */ - private void addClasspathEntries(final Object owner, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + private static void addClasspathEntries(final Object owner, final ClassLoader classLoader, + final ClasspathOrder classpathOrderOut, final ScanSpec scanSpec, final LogNode log) { // type ClasspathEntry[] - final Object entries = ReflectionUtils.getFieldVal(owner, "entries", false); + final Object entries = classpathOrderOut.reflectionUtils.getFieldVal(false, owner, "entries"); if (entries != null) { for (int i = 0, n = Array.getLength(entries); i < n; i++) { // type ClasspathEntry final Object entry = Array.get(entries, i); // type BundleFile - final Object bundlefile = ReflectionUtils.getFieldVal(entry, "bundlefile", false); - addBundleFile(bundlefile, new HashSet<>(), classLoader, classpathOrderOut, log); + final Object bundlefile = classpathOrderOut.reflectionUtils.getFieldVal(false, entry, "bundlefile"); + addBundleFile(bundlefile, new HashSet<>(), classLoader, classpathOrderOut, scanSpec, log); } } } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle( - * nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { // type ClasspathManager - final Object manager = ReflectionUtils.getFieldVal(classLoader, "manager", false); - addClasspathEntries(manager, classLoader, classpathOrderOut, log); + final Object manager = classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "manager"); + addClasspathEntries(manager, classLoader, classpathOrder, scanSpec, log); // type FragmentClasspath[] - final Object fragments = ReflectionUtils.getFieldVal(manager, "fragments", false); + final Object fragments = classpathOrder.reflectionUtils.getFieldVal(false, manager, "fragments"); if (fragments != null) { for (int f = 0, fragLength = Array.getLength(fragments); f < fragLength; f++) { // type FragmentClasspath final Object fragment = Array.get(fragments, f); - addClasspathEntries(fragment, classLoader, classpathOrderOut, log); + addClasspathEntries(fragment, classLoader, classpathOrder, scanSpec, log); } } - // Only read system bundles once (all bundles should give the same results for this). We assume there is - // only one separate Equinox instance on the classpath. + // Only read system bundles once (all bundles should give the same results for this). if (!alreadyReadSystemBundles) { // type BundleLoader - final Object delegate = ReflectionUtils.getFieldVal(classLoader, "delegate", false); + final Object delegate = classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "delegate"); // type EquinoxContainer - final Object container = ReflectionUtils.getFieldVal(delegate, "container", false); + final Object container = classpathOrder.reflectionUtils.getFieldVal(false, delegate, "container"); // type Storage - final Object storage = ReflectionUtils.getFieldVal(container, "storage", false); + final Object storage = classpathOrder.reflectionUtils.getFieldVal(false, container, "storage"); // type ModuleContainer - final Object moduleContainer = ReflectionUtils.getFieldVal(storage, "moduleContainer", false); + final Object moduleContainer = classpathOrder.reflectionUtils.getFieldVal(false, storage, + "moduleContainer"); // type ModuleDatabase - final Object moduleDatabase = ReflectionUtils.getFieldVal(moduleContainer, "moduleDatabase", false); + final Object moduleDatabase = classpathOrder.reflectionUtils.getFieldVal(false, moduleContainer, + "moduleDatabase"); // type HashMap - final Object modulesById = ReflectionUtils.getFieldVal(moduleDatabase, "modulesById", false); + final Object modulesById = classpathOrder.reflectionUtils.getFieldVal(false, moduleDatabase, + "modulesById"); // type EquinoxSystemModule (module 0 is always the system module) - final Object module0 = ReflectionUtils.invokeMethod(modulesById, "get", Object.class, 0L, false); + final Object module0 = classpathOrder.reflectionUtils.invokeMethod(false, modulesById, "get", + Object.class, 0L); // type Bundle - final Object bundle = ReflectionUtils.invokeMethod(module0, "getBundle", false); + final Object bundle = classpathOrder.reflectionUtils.invokeMethod(false, module0, "getBundle"); // type BundleContext - final Object bundleContext = ReflectionUtils.invokeMethod(bundle, "getBundleContext", false); + final Object bundleContext = classpathOrder.reflectionUtils.invokeMethod(false, bundle, + "getBundleContext"); // type Bundle[] - final Object bundles = ReflectionUtils.invokeMethod(bundleContext, "getBundles", false); + final Object bundles = classpathOrder.reflectionUtils.invokeMethod(false, bundleContext, "getBundles"); if (bundles != null) { for (int i = 0, n = Array.getLength(bundles); i < n; i++) { // type EquinoxBundle final Object equinoxBundle = Array.get(bundles, i); // type EquinoxModule - final Object module = ReflectionUtils.getFieldVal(equinoxBundle, "module", false); + final Object module = classpathOrder.reflectionUtils.getFieldVal(false, equinoxBundle, + "module"); // type String - String location = (String) ReflectionUtils.getFieldVal(module, "location", false); + String location = (String) classpathOrder.reflectionUtils.getFieldVal(false, module, + "location"); if (location != null) { final int fileIdx = location.indexOf("file:"); if (fileIdx >= 0) { location = location.substring(fileIdx); - classpathOrderOut.addClasspathEntry(location, classLoader, log); + classpathOrder.addClasspathEntry(location, classLoader, scanSpec, log); } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxContextFinderClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxContextFinderClassLoaderHandler.java index 6ee5c5a82..cc0311455 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxContextFinderClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxContextFinderClassLoaderHandler.java @@ -28,45 +28,64 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** Extract classpath entries from the Eclipse Equinox ContextFinder ClassLoader. */ class EquinoxContextFinderClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { "org.eclipse.osgi.internal.framework.ContextFinder" }; + /** Class cannot be constructed. */ + private EquinoxContextFinderClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return (ClassLoader) ReflectionUtils.getFieldVal(outerClassLoaderInstance, "parentContextClassLoader", - false); + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.eclipse.osgi.internal.framework.ContextFinder"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo((ClassLoader) classLoaderOrder.reflectionUtils.getFieldVal(false, classLoader, + "parentContextClassLoader"), /* isParent = */ true, log); + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { // Nothing to handle -- embedded parentContextClassLoader will be used instead. } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FallbackClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FallbackClassLoaderHandler.java index 7df59037e..646f71ff3 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FallbackClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FallbackClassLoaderHandler.java @@ -28,128 +28,172 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * Fallback ClassLoaderHandler. Tries to get classpath from a range of possible method and field names. */ class FallbackClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - // The actual string "*" is unimportant here, it is ignored - return new String[] { "*" }; + /** Class cannot be constructed. */ + private FallbackClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + // This is the fallback handler, it handles anything + return true; } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { boolean valid = false; - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getClassPath", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getClasspath", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "classpath", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "classPath", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "cp", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.getFieldVal(classLoader, "classpath", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.getFieldVal(classLoader, "classPath", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "cp", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getPath", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getPaths", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "path", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "paths", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "paths", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "paths", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getDir", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getDirs", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "dir", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "dirs", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "dir", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "dirs", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getFile", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getFiles", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "file", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "files", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "file", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "files", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getJar", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getJars", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "jar", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "jars", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "jar", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "jars", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getURL", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getURLs", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getUrl", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getUrls", false), classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "url", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "urls", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "url", false), - classLoader, log); - valid |= classpathOrderOut.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "urls", false), - classLoader, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getClassPath"), classLoader, + scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getClasspath"), classLoader, + scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "classpath"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "classPath"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "cp"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "classpath"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "classPath"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "cp"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getPath"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getPaths"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "path"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "paths"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "paths"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "paths"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getDir"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getDirs"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "dir"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "dirs"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "dir"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "dirs"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getFile"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getFiles"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "file"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "files"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "file"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "files"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getJar"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getJars"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "jar"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "jars"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "jar"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "jars"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getURL"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getURLs"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getUrl"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getUrls"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "url"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "urls"), classLoader, scanSpec, + log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "url"), classLoader, scanSpec, log); + valid |= classpathOrder.addClasspathEntryObject( + classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "urls"), classLoader, scanSpec, log); if (log != null) { log.log("FallbackClassLoaderHandler " + (valid ? "found" : "did not find") + " classpath entries in unknown ClassLoader " + classLoader); diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FelixClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FelixClassLoaderHandler.java index e50161a10..de442aa10 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FelixClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FelixClassLoaderHandler.java @@ -33,10 +33,12 @@ import java.util.List; import java.util.Set; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * Custom Class Loader Handler for OSGi Felix ClassLoader. @@ -47,34 +49,40 @@ * @author elrufaie */ class FelixClassLoaderHandler implements ClassLoaderHandler { - - /** The bundles. */ - private final Set bundles = new HashSet<>(); - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { // - "org.apache.felix.framework.BundleWiringImpl$BundleClassLoaderJava5", - "org.apache.felix.framework.BundleWiringImpl$BundleClassLoader" }; + /** Class cannot be constructed. */ + private FelixClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.apache.felix.framework.BundleWiringImpl$BundleClassLoaderJava5") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.apache.felix.framework.BundleWiringImpl$BundleClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } /** @@ -84,9 +92,8 @@ public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) * the content object * @return the content location */ - private String getContentLocation(final Object content) { - final File file = (File) ReflectionUtils.invokeMethod(content, "getFile", false); - return file != null ? file.toURI().toString() : null; + private static File getContentLocation(final Object content, final ReflectionUtils reflectionUtils) { + return (File) reflectionUtils.invokeMethod(false, content, "getFile"); } /** @@ -98,32 +105,40 @@ private String getContentLocation(final Object content) { * the classloader * @param classpathOrderOut * the classpath order out + * @param bundles + * the bundles + * @param scanSpec + * the scan spec * @param log * the log */ - private void addBundle(final Object bundleWiring, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + private static void addBundle(final Object bundleWiring, final ClassLoader classLoader, + final ClasspathOrder classpathOrderOut, final Set bundles, final ScanSpec scanSpec, + final LogNode log) { // Track the bundles we've processed to prevent loops bundles.add(bundleWiring); // Get the revision for this wiring - final Object revision = ReflectionUtils.invokeMethod(bundleWiring, "getRevision", false); + final Object revision = classpathOrderOut.reflectionUtils.invokeMethod(false, bundleWiring, "getRevision"); // Get the contents - final Object content = ReflectionUtils.invokeMethod(revision, "getContent", false); - final String location = content != null ? getContentLocation(content) : null; + final Object content = classpathOrderOut.reflectionUtils.invokeMethod(false, revision, "getContent"); + final File location = content != null ? getContentLocation(content, classpathOrderOut.reflectionUtils) + : null; if (location != null) { // Add the bundle object - classpathOrderOut.addClasspathEntry(location, classLoader, log); + classpathOrderOut.addClasspathEntry(location, classLoader, scanSpec, log); // And any embedded content - final List embeddedContent = (List) ReflectionUtils.invokeMethod(revision, "getContentPath", - false); + final List embeddedContent = (List) classpathOrderOut.reflectionUtils.invokeMethod(false, + revision, "getContentPath"); if (embeddedContent != null) { for (final Object embedded : embeddedContent) { if (embedded != content) { - final String embeddedLocation = embedded != null ? getContentLocation(embedded) : null; + final File embeddedLocation = embedded != null + ? getContentLocation(embedded, classpathOrderOut.reflectionUtils) + : null; if (embeddedLocation != null) { - classpathOrderOut.addClasspathEntry(embeddedLocation, classLoader, log); + classpathOrderOut.addClasspathEntry(embeddedLocation, classLoader, scanSpec, log); } } } @@ -131,27 +146,36 @@ private void addBundle(final Object bundleWiring, final ClassLoader classLoader, } } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { - + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { // Get the wiring for the ClassLoader's bundle - final Object bundleWiring = ReflectionUtils.getFieldVal(classLoader, "m_wiring", false); - addBundle(bundleWiring, classLoader, classpathOrderOut, log); + final Set bundles = new HashSet<>(); + final Object bundleWiring = classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "m_wiring"); + addBundle(bundleWiring, classLoader, classpathOrder, bundles, scanSpec, log); // Deal with any other bundles we might be wired to. TODO: Use the ScanSpec to narrow down the list of wires // that we follow. - final List requiredWires = (List) ReflectionUtils.invokeMethod(bundleWiring, "getRequiredWires", - String.class, null, false); + final List requiredWires = (List) classpathOrder.reflectionUtils.invokeMethod(false, bundleWiring, + "getRequiredWires", String.class, null); if (requiredWires != null) { for (final Object wire : requiredWires) { - final Object provider = ReflectionUtils.invokeMethod(wire, "getProviderWiring", false); + final Object provider = classpathOrder.reflectionUtils.invokeMethod(false, wire, + "getProviderWiring"); if (!bundles.contains(provider)) { - addBundle(provider, classLoader, classpathOrderOut, log); + addBundle(provider, classLoader, classpathOrder, bundles, scanSpec, log); } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/JBossClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/JBossClassLoaderHandler.java index 7217a511e..99f46aa80 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/JBossClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/JBossClassLoaderHandler.java @@ -31,17 +31,19 @@ import java.io.File; import java.lang.reflect.Array; import java.nio.file.Path; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.FileUtils; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * Extract classpath entries from the JBoss ClassLoader. See: @@ -50,29 +52,38 @@ * https://github.com/jboss-modules/jboss-modules/blob/master/src/main/java/org/jboss/modules/ModuleClassLoader.java */ class JBossClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { "org.jboss.modules.ModuleClassLoader" }; + /** Class cannot be constructed. */ + private JBossClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.jboss.modules.ModuleClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } /** @@ -84,56 +95,151 @@ public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) * the classloader * @param classpathOrderOut * the classpath order + * @param scanSpec + * the scan spec * @param log * the log */ - private void handleResourceLoader(final Object resourceLoader, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + private static void handleResourceLoader(final Object resourceLoader, final ClassLoader classLoader, + final ClasspathOrder classpathOrderOut, final ScanSpec scanSpec, final LogNode log) { if (resourceLoader == null) { return; } // PathResourceLoader has root field, which is a Path object - final Object root = ReflectionUtils.getFieldVal(resourceLoader, "root", false); + final Object root = classpathOrderOut.reflectionUtils.getFieldVal(false, resourceLoader, "root"); + + classpathOrderOut.addClasspathEntry(loadJarPathFromClassicVFS(root, classpathOrderOut), classLoader, + scanSpec, log); + classpathOrderOut.addClasspathEntry(loadJarPathFromNewVFS(root, classpathOrderOut), classLoader, scanSpec, + log); + classpathOrderOut.addClasspathEntry( + classpathOrderOut.reflectionUtils.getFieldVal(false, resourceLoader, "fileOfJar"), classLoader, + scanSpec, log); + } + + /** + * Returns the absolute path of a JAR file from a given root object using the JBoss VFS mechanism. This works + * for Versions of JBoss/Wildfly that contain the following change: + * WFLY-18544 + * JBEAP-25879 + * JBEAP-25677 + * + * @param root + * The root object to get the JAR path from. + * @param classpathOrderOut + * The ClasspathOrder object for updating the classpath order. + * @return The {@link File} of the JAR file, or null if the path couldn't be found. + */ + private static File loadJarPathFromNewVFS(final Object root, final ClasspathOrder classpathOrderOut) { + if (root == null) { + return null; + } + final Class jbossVFS = getJBossVFSAccess(root); + if (jbossVFS == null) { + return null; + } + // try to find the mount of the root. Type is org.jboss.vfs.VFS.Mount + final Object mount = classpathOrderOut.reflectionUtils.invokeStaticMethod(false, jbossVFS, "getMount", + root.getClass(), root); + if (mount == null) { + return null; + } + // try to access the fileSystem of the mount. Type is org.jboss.vfs.spi.FileSystem + final Object fileSystem = classpathOrderOut.reflectionUtils.invokeMethod(false, mount, "getFileSystem"); + if (fileSystem == null) { + return null; + } + // now access the mount source, which is the file that is used to create the mount. + final File mountSource = (File) classpathOrderOut.reflectionUtils.invokeMethod(false, fileSystem, + "getMountSource"); + if (mountSource == null) { + return null; + } + // absolute path of the mountSource should be the 'physical' .jar + return mountSource; + } + + /** + * Get the access to the JBoss VFS class. Tries to load VFS first from the classloader of the provided root + * object if it's an object from org.jboss.vfs. If the root object is not from org.jboss.vfs, VFS will be tried + * to be loaded from the current thread class loader. It might be unnecessary to load VFS from the current + * thread context, because this means that the root object is not from org.jboss.vfs and VFS will not help + * here... but as a defensive approach we really try to get VFS access here. + * + * @param root + * The root VirtualFile of JBoss VFS. Used to load the VFS via the classloader of the root. Can not + * be null. + * @return The Class object representing the JBoss VFS class, or null if it couldn't be found. + */ + private static Class getJBossVFSAccess(final Object root) { + Class jbossVFS = null; + // we need access to the class 'VFS' of org.jboss.vfs + try { + if (root.getClass().getName().contains("org.jboss.vfs")) { + // first, try the classloader of the root object. Since the root object comes from org.jboss.vfs, + // it is likely that we can get access to org.jboss.vfs.VFS from this classloader + final ClassLoader vfsRootClassloader = root.getClass().getClassLoader(); + jbossVFS = loadJBossVFS(vfsRootClassloader); + } else { + // for non org.jboss.vfs objects, use the currentThread + jbossVFS = loadJBossVFS(Thread.currentThread().getContextClassLoader()); + } + } catch (final ClassNotFoundException e) { + try { + // try to load JBoss VFS access from the current threads classloader since the previous method failed + // if the previous method was already the currentThreads classloader, it will fail again... + jbossVFS = loadJBossVFS(Thread.currentThread().getContextClassLoader()); + } catch (final ClassNotFoundException e1) { + // swallow the exception. If there is no VFS present, we can't do anything... + } + } + return jbossVFS; + } + + private static Class loadJBossVFS(final ClassLoader classLoader) throws ClassNotFoundException { + return Class.forName("org.jboss.vfs.VFS", true, classLoader); + } + + /** + * Returns the absolute path of a JAR file from a given root object using the 'classic' VFS read mechanism. This + * works for Versions of JBoss/Wildfly prior to this change: + * WFLY-18544 + * JBEAP-25879 + * JBEAP-25677 + * + * @param root + * The root object to get the JAR path from. + * @param classpathOrderOut + * The ClasspathOrder object for updating the classpath order. + * @return The {@link File} or {@link Path} of the JAR file, or null if the VFS path couldn't be found. + */ + private static Object loadJarPathFromClassicVFS(final Object root, final ClasspathOrder classpathOrderOut) { + if (root == null) { + return null; + } // type VirtualFile - final File physicalFile = (File) ReflectionUtils.invokeMethod(root, "getPhysicalFile", false); - String path = null; + final File physicalFile = (File) classpathOrderOut.reflectionUtils.invokeMethod(false, root, + "getPhysicalFile"); if (physicalFile != null) { - final String name = (String) ReflectionUtils.invokeMethod(root, "getName", false); + final String name = (String) classpathOrderOut.reflectionUtils.invokeMethod(false, root, "getName"); if (name != null) { // getParentFile() removes "contents" directory final File file = new File(physicalFile.getParentFile(), name); if (FileUtils.canRead(file)) { - path = file.getAbsolutePath(); + return file; } else { // This is an exploded jar or classpath directory - path = physicalFile.getAbsolutePath(); + return physicalFile; } } else { - path = physicalFile.getAbsolutePath(); - } - } else { - path = (String) ReflectionUtils.invokeMethod(root, "getPathName", false); - if (path == null) { - // Try Path or File - final File file = root instanceof Path ? ((Path) root).toFile() - : root instanceof File ? (File) root : null; - if (file != null) { - path = file.getAbsolutePath(); - } - } - } - if (path == null) { - final File file = (File) ReflectionUtils.getFieldVal(resourceLoader, "fileOfJar", false); - if (file != null) { - path = file.getAbsolutePath(); + return physicalFile; } - } - if (path != null) { - classpathOrderOut.addClasspathEntry(path, classLoader, log); } else { - if (log != null) { - log.log("Could not determine classpath for ResourceLoader: " + resourceLoader); + final String path = (String) classpathOrderOut.reflectionUtils.invokeMethod(false, root, "getPathName"); + if (path != null) { + return path; } + return root; } } @@ -148,21 +254,26 @@ private void handleResourceLoader(final Object resourceLoader, final ClassLoader * the classloader * @param classpathOrderOut * the classpath order + * @param scanSpec + * the scan spec * @param log * the log */ - private void handleRealModule(final Object module, final Set visitedModules, - final ClassLoader classLoader, final ClasspathOrder classpathOrderOut, final LogNode log) { + private static void handleRealModule(final Object module, final Set visitedModules, + final ClassLoader classLoader, final ClasspathOrder classpathOrderOut, final ScanSpec scanSpec, + final LogNode log) { if (!visitedModules.add(module)) { // Avoid extracting paths from the same module more than once return; } - ClassLoader moduleLoader = (ClassLoader) ReflectionUtils.invokeMethod(module, "getClassLoader", false); + ClassLoader moduleLoader = (ClassLoader) classpathOrderOut.reflectionUtils.invokeMethod(false, module, + "getClassLoader"); if (moduleLoader == null) { moduleLoader = classLoader; } // type VFSResourceLoader[] - final Object vfsResourceLoaders = ReflectionUtils.invokeMethod(moduleLoader, "getResourceLoaders", false); + final Object vfsResourceLoaders = classpathOrderOut.reflectionUtils.invokeMethod(false, moduleLoader, + "getResourceLoaders"); if (vfsResourceLoaders != null) { for (int i = 0, n = Array.getLength(vfsResourceLoaders); i < n; i++) { // type JarFileResourceLoader for jars, VFSResourceLoader for exploded jars, PathResourceLoader @@ -172,42 +283,55 @@ private void handleRealModule(final Object module, final Set visitedModu // Could skip NativeLibraryResourceLoader instances altogether, but testing for their existence // only seems to add about 3% to the total scan time. // if (!resourceLoader.getClass().getSimpleName().equals("NativeLibraryResourceLoader")) { - handleResourceLoader(resourceLoader, moduleLoader, classpathOrderOut, log); + handleResourceLoader(resourceLoader, moduleLoader, classpathOrderOut, scanSpec, log); //} } } } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { - final Object module = ReflectionUtils.invokeMethod(classLoader, "getModule", false); - final Object callerModuleLoader = ReflectionUtils.invokeMethod(module, "getCallerModuleLoader", false); + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + final Object module = classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getModule"); + final Object callerModuleLoader = classpathOrder.reflectionUtils.invokeMethod(false, module, + "getCallerModuleLoader"); final Set visitedModules = new HashSet<>(); @SuppressWarnings("unchecked") - final Map moduleMap = (Map) ReflectionUtils.getFieldVal(callerModuleLoader, - "moduleMap", false); - for (final Entry ent : moduleMap.entrySet()) { + final Map moduleMap = (Map) classpathOrder.reflectionUtils + .getFieldVal(false, callerModuleLoader, "moduleMap"); + final Set> moduleMapEntries = moduleMap != null ? moduleMap.entrySet() + : Collections.> emptySet(); + for (final Entry ent : moduleMapEntries) { // type FutureModule final Object val = ent.getValue(); // type Module - final Object realModule = ReflectionUtils.invokeMethod(val, "getModule", false); - handleRealModule(realModule, visitedModules, classLoader, classpathOrderOut, log); + final Object realModule = classpathOrder.reflectionUtils.invokeMethod(false, val, "getModule"); + handleRealModule(realModule, visitedModules, classLoader, classpathOrder, scanSpec, log); } // type Map> @SuppressWarnings("unchecked") - final Map> pathsMap = (Map>) ReflectionUtils.invokeMethod(module, - "getPaths", false); + final Map> pathsMap = (Map>) classpathOrder.reflectionUtils + .invokeMethod(false, module, "getPaths"); for (final Entry> ent : pathsMap.entrySet()) { for (final Object /* ModuleClassLoader$1 */ localLoader : ent.getValue()) { // type ModuleClassLoader (outer class) - final Object moduleClassLoader = ReflectionUtils.getFieldVal(localLoader, "this$0", false); + final Object moduleClassLoader = classpathOrder.reflectionUtils.getFieldVal(false, localLoader, + "this$0"); // type Module - final Object realModule = ReflectionUtils.getFieldVal(moduleClassLoader, "module", false); - handleRealModule(realModule, visitedModules, classLoader, classpathOrderOut, log); + final Object realModule = classpathOrder.reflectionUtils.getFieldVal(false, moduleClassLoader, + "module"); + handleRealModule(realModule, visitedModules, classLoader, classpathOrder, scanSpec, log); } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/JPMSClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/JPMSClassLoaderHandler.java index 8576943e9..b214ad999 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/JPMSClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/JPMSClassLoaderHandler.java @@ -28,8 +28,12 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import nonapi.io.github.classgraph.ScanSpec; +import java.net.URL; + +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; /** @@ -37,40 +41,65 @@ * them (module scanning uses a different mechanism from classpath scanning). */ class JPMSClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { // - "jdk.internal.loader.ClassLoaders$AppClassLoader", // - "jdk.internal.loader.BuiltinClassLoader" }; + /** Class cannot be constructed. */ + private JPMSClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "jdk.internal.loader.ClassLoaders$AppClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "jdk.internal.loader.BuiltinClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { - // The JDK9 classloaders have a field, URLClassPath ucp, containing URLs for unnamed modules, - // but it is not visible. + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + // The JDK9 classloaders have a field, `URLClassPath ucp`, containing URLs for unnamed modules, + // but it is not visible. Modules therefore have to be scanned using the JPMS API. + // However, it is possible for a Java agent to extend UCP by adding directly to the `ucp` field + // (#537), and there is no way to read this field. Therefore, we need to use Narcissus to break + // Java's encapsulation to read this, for this small corner case. + final Object ucpVal = classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "ucp"); + if (ucpVal != null) { + final URL[] urls = (URL[]) classpathOrder.reflectionUtils.invokeMethod(false, ucpVal, "getURLs"); + classpathOrder.addClasspathEntryObject(urls, classLoader, scanSpec, log); + } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/OSGiDefaultClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/OSGiDefaultClassLoaderHandler.java index d472a79d9..517bc0622 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/OSGiDefaultClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/OSGiDefaultClassLoaderHandler.java @@ -30,10 +30,11 @@ import java.io.File; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * Handle the OSGi DefaultClassLoader. @@ -41,45 +42,66 @@ * @author lukehutch */ class OSGiDefaultClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { "org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader" }; + /** Class cannot be constructed. */ + private OSGiDefaultClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classloader, - final ClasspathOrder classpathOrderOut, final LogNode log) { - final Object classpathManager = ReflectionUtils.invokeMethod(classloader, "getClasspathManager", false); - final Object[] entries = (Object[]) ReflectionUtils.getFieldVal(classpathManager, "entries", false); + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + final Object classpathManager = classpathOrder.reflectionUtils.invokeMethod(false, classLoader, + "getClasspathManager"); + final Object[] entries = (Object[]) classpathOrder.reflectionUtils.getFieldVal(false, classpathManager, + "entries"); if (entries != null) { for (final Object entry : entries) { - final Object bundleFile = ReflectionUtils.invokeMethod(entry, "getBundleFile", false); - final File baseFile = (File) ReflectionUtils.invokeMethod(bundleFile, "getBaseFile", false); + final Object bundleFile = classpathOrder.reflectionUtils.invokeMethod(false, entry, + "getBundleFile"); + final File baseFile = (File) classpathOrder.reflectionUtils.invokeMethod(false, bundleFile, + "getBaseFile"); if (baseFile != null) { - classpathOrderOut.addClasspathEntry(baseFile.getPath(), classloader, log); + classpathOrder.addClasspathEntry(baseFile.getPath(), classLoader, scanSpec, log); } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ParentLastDelegationOrderTestClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ParentLastDelegationOrderTestClassLoaderHandler.java index 9beea6dca..532585748 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ParentLastDelegationOrderTestClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ParentLastDelegationOrderTestClassLoaderHandler.java @@ -28,46 +28,65 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** ClassLoaderHandler that is used to test PARENT_LAST delegation order. */ class ParentLastDelegationOrderTestClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { "io.github.classgraph.issues.issue267.FakeRestartClassLoader" }; + /** Class cannot be constructed. */ + private ParentLastDelegationOrderTestClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "io.github.classgraph.issues.issue267.FakeRestartClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_LAST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + // Add self first, then delegate to parent + classLoaderOrder.add(classLoader, log); + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { - final String classpath = (String) ReflectionUtils.invokeMethod(classLoader, "getClasspath", - /* throwException = */ true); - classpathOrderOut.addClasspathEntry(classpath, classLoader, log); + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + final String classpath = (String) classpathOrder.reflectionUtils.invokeMethod(/* throwException = */ true, + classLoader, "getClasspath"); + classpathOrder.addClasspathEntry(classpath, classLoader, scanSpec, log); } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/PlexusClassWorldsClassRealmClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/PlexusClassWorldsClassRealmClassLoaderHandler.java new file mode 100644 index 000000000..e48ed2a77 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/PlexusClassWorldsClassRealmClassLoaderHandler.java @@ -0,0 +1,153 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.classloaderhandler; + +import java.util.SortedSet; + +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; +import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.LogNode; + +/** + * Handle the Plexus ClassWorlds ClassRealm ClassLoader. + * + * @author lukehutch + */ +class PlexusClassWorldsClassRealmClassLoaderHandler implements ClassLoaderHandler { + /** Class cannot be constructed. */ + private PlexusClassWorldsClassRealmClassLoaderHandler() { + } + + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. + */ + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.codehaus.plexus.classworlds.realm.ClassRealm"); + } + + /** + * Checks if is this classloader uses a parent-first strategy. + * + * @param classRealmInstance + * the ClassRealm instance + * @return true if classloader uses a parent-first strategy + */ + private static boolean isParentFirstStrategy(final ClassLoader classRealmInstance, + final ReflectionUtils reflectionUtils) { + final Object strategy = reflectionUtils.getFieldVal(false, classRealmInstance, "strategy"); + if (strategy != null) { + final String strategyClassName = strategy.getClass().getName(); + if (strategyClassName.equals("org.codehaus.plexus.classworlds.strategy.SelfFirstStrategy") + || strategyClassName.equals("org.codehaus.plexus.classworlds.strategy.OsgiBundleStrategy")) { + // Strategy is self-first + return false; + } + } + // Strategy is org.codehaus.plexus.classworlds.strategy.ParentFirstStrategy (or failed to find strategy) + return true; + } + + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classRealm + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log + */ + public static void findClassLoaderOrder(final ClassLoader classRealm, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + // From ClassRealm#loadClassFromImport(String) -> getImportClassLoader(String) + final Object foreignImports = classLoaderOrder.reflectionUtils.getFieldVal(false, classRealm, + "foreignImports"); + if (foreignImports != null) { + @SuppressWarnings("unchecked") + final SortedSet foreignImportEntries = (SortedSet) foreignImports; + for (final Object entry : foreignImportEntries) { + final ClassLoader foreignImportClassLoader = (ClassLoader) classLoaderOrder.reflectionUtils + .invokeMethod(false, entry, "getClassLoader"); + // Treat foreign import classloader as if it is a parent classloader + classLoaderOrder.delegateTo(foreignImportClassLoader, /* isParent = */ true, log); + } + } + + // Get delegation order -- different strategies have different delegation orders + final boolean isParentFirst = isParentFirstStrategy(classRealm, classLoaderOrder.reflectionUtils); + + // From ClassRealm#loadClassFromSelf(String) -> findLoadedClass(String) for self-first strategy + if (!isParentFirst) { + // Add self before parent + classLoaderOrder.add(classRealm, log); + } + + // From ClassRealm#loadClassFromParent -- N.B. we are ignoring parentImports, which is used to filter + // a class name before deciding whether or not to call the parent classloader (so ClassGraph will be + // able to load classes by name that are not imported from the parent classloader). + final ClassLoader parentClassLoader = (ClassLoader) classLoaderOrder.reflectionUtils.invokeMethod(false, + classRealm, "getParentClassLoader"); + classLoaderOrder.delegateTo(parentClassLoader, /* isParent = */ true, log); + classLoaderOrder.delegateTo(classRealm.getParent(), /* isParent = */ true, log); + + // From ClassRealm#loadClassFromSelf(String) -> findLoadedClass(String) for parent-first strategy + if (isParentFirst) { + // Add self after parent + classLoaderOrder.add(classRealm, log); + } + } + + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. + */ + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + // ClassRealm extends URLClassLoader + URLClassLoaderHandler.findClasspathOrder(classLoader, classpathOrder, scanSpec, log); + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/QuarkusClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/QuarkusClassLoaderHandler.java new file mode 100644 index 000000000..9476d2115 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/QuarkusClassLoaderHandler.java @@ -0,0 +1,205 @@ +/* + * This file is part of ClassGraph. + * + * Author: @mcollovati + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 @mcollovati, contributed to the ClassGraph project + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.classloaderhandler; + +import java.io.IOError; +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; +import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.LogNode; + +/** + * Extract classpath entries from the Quarkus ClassLoader. + */ +class QuarkusClassLoaderHandler implements ClassLoaderHandler { + // Classloader until Quarkus 1.2 + private static final String RUNTIME_CLASSLOADER = "io.quarkus.runner.RuntimeClassLoader"; + + // Classloader since Quarkus 1.3 + private static final String QUARKUS_CLASSLOADER = "io.quarkus.bootstrap.classloading.QuarkusClassLoader"; + + // Classloader since Quarkus 1.13 + private static final String RUNNER_CLASSLOADER = "io.quarkus.bootstrap.runner.RunnerClassLoader"; + + // Class path elements prior to Quarkus 3.11 + private static final Map PRE_311_RESOURCE_BASED_ELEMENTS; + static { + final Map hlp = new HashMap<>(); + hlp.put("io.quarkus.bootstrap.classloading.JarClassPathElement", "file"); + hlp.put("io.quarkus.bootstrap.classloading.DirectoryClassPathElement", "root"); + PRE_311_RESOURCE_BASED_ELEMENTS = Collections.unmodifiableMap(hlp); + } + + /** + * Class cannot be constructed. + */ + private QuarkusClassLoaderHandler() { + } + + /** + * Can handle. + * + * @param classLoaderClass + * the classloader class + * @param log + * the log + * @return true, if classLoaderClass is the Quarkus RuntimeClassloader or QuarkusClassloader + */ + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, RUNTIME_CLASSLOADER) + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, QUARKUS_CLASSLOADER) + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, RUNNER_CLASSLOADER); + } + + /** + * Find classloader order. + * + * @param classLoader + * the class loader + * @param classLoaderOrder + * the classloader order + * @param log + * the log + */ + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); + } + + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. + */ + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + + final String classLoaderName = classLoader.getClass().getName(); + if (RUNTIME_CLASSLOADER.equals(classLoaderName)) { + findClasspathOrderForRuntimeClassloader(classLoader, classpathOrder, scanSpec, log); + } else if (QUARKUS_CLASSLOADER.equals(classLoaderName)) { + findClasspathOrderForQuarkusClassloader(classLoader, classpathOrder, scanSpec, log); + } else if (RUNNER_CLASSLOADER.equals(classLoaderName)) { + findClasspathOrderForRunnerClassloader(classLoader, classpathOrder, scanSpec, log); + } + } + + private static void findClasspathOrderForQuarkusClassloader(final ClassLoader classLoader, + final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { + + final Collection elements = findQuarkusClassLoaderElements(classLoader, classpathOrder); + + for (final Object element : elements) { + final String elementClassName = element.getClass().getName(); + final String fieldName = PRE_311_RESOURCE_BASED_ELEMENTS.get(elementClassName); + if (fieldName != null) { + classpathOrder.addClasspathEntry( + classpathOrder.reflectionUtils.getFieldVal(false, element, fieldName), classLoader, + scanSpec, log); + } else { + final Object rootPath = classpathOrder.reflectionUtils.invokeMethod(false, element, "getRoot"); + if (rootPath instanceof Path) { + classpathOrder.addClasspathEntry(rootPath, classLoader, scanSpec, log); + } + } + } + } + + @SuppressWarnings("unchecked") + private static Collection findQuarkusClassLoaderElements(final ClassLoader classLoader, + final ClasspathOrder classpathOrder) { + Collection elements = (Collection) classpathOrder.reflectionUtils.getFieldVal(false, + classLoader, "elements"); + if (elements == null) { + elements = new ArrayList<>(); + // Since 3.16.x + for (final String fieldName : new String[] { "normalPriorityElements", "lesserPriorityElements" }) { + final Collection fieldVal = (Collection) classpathOrder.reflectionUtils + .getFieldVal(false, classLoader, fieldName); + if (fieldVal == null) { + continue; + } + elements.addAll(fieldVal); + } + } + return elements; + } + + @SuppressWarnings("unchecked") + private static void findClasspathOrderForRuntimeClassloader(final ClassLoader classLoader, + final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { + final Collection applicationClassDirectories = (Collection) classpathOrder.reflectionUtils + .getFieldVal(false, classLoader, "applicationClassDirectories"); + if (applicationClassDirectories != null) { + for (final Path path : applicationClassDirectories) { + try { + final URI uri = path.toUri(); + classpathOrder.addClasspathEntryObject(uri, classLoader, scanSpec, log); + } catch (IOError | SecurityException e) { + if (log != null) { + log.log("Could not convert path to URI: " + path); + } + } + } + } + } + + @SuppressWarnings("unchecked") + private static void findClasspathOrderForRunnerClassloader(final ClassLoader classLoader, + final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { + for (final Object[] elementArray : ((Map) classpathOrder.reflectionUtils + .getFieldVal(false, classLoader, "resourceDirectoryMap")).values()) { + for (final Object element : elementArray) { + final String elementClassName = element.getClass().getName(); + if ("io.quarkus.bootstrap.runner.JarResource".equals(elementClassName)) { + classpathOrder.addClasspathEntry( + classpathOrder.reflectionUtils.getFieldVal(false, element, "jarPath"), classLoader, + scanSpec, log); + } + } + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/SpringBootRestartClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/SpringBootRestartClassLoaderHandler.java index 27b6b0488..73187a8fa 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/SpringBootRestartClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/SpringBootRestartClassLoaderHandler.java @@ -28,8 +28,10 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; /** @@ -40,32 +42,47 @@ * handler for that class loader also has to delegate in PARENT_LAST order. */ class SpringBootRestartClassLoaderHandler implements ClassLoaderHandler { + /** Class cannot be constructed. */ + private SpringBootRestartClassLoaderHandler() { + } /** - * The handler delegate. Spring Boot's devtools class loader is an extension of URLClassLoader, so there's no - * need to use reflection to access the supported URLs, and we can delegate the handling to an internal instance - * of URLClassLoaderHandler. - */ - private final URLClassLoaderHandler handlerDelegate = new URLClassLoaderHandler(); - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public String[] handledClassLoaders() { - return new String[] { // - "org.springframework.boot.devtools.restart.classloader.RestartClassLoader" }; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.springframework.boot.devtools.restart.classloader.RestartClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + // The Restart classloader is a parent-last classloader, so add the Restart classloader itself to the + // classloader order first + classLoaderOrder.add(classLoader, log); + + // Delegate to the parent of the RestartClassLoader + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); } /** + * Find the classpath entries for the associated {@link ClassLoader}. + * * Spring Boot's RestartClassLoader sits in front of the parent class loader and watches a given set of * directories for changes. While those classes are reachable from the parent class loader directly, they should * always be loaded through direct access from the RestartClassLoader until it's completely turned of by means @@ -77,21 +94,17 @@ public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInst * See: #267, * #268 * - * @param classLoaderInstance - * the class loader instance - * @return the delegation order - */ - @Override - public ClassLoaderHandler.DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_LAST; - } - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { - handlerDelegate.handle(scanSpec, classLoader, classpathOrderOut, log); + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + // The Restart classloader doesn't itself store any URLs } } \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/TomcatWebappClassLoaderBaseHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/TomcatWebappClassLoaderBaseHandler.java index c56ac2dc9..aa5a9f2c1 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/TomcatWebappClassLoaderBaseHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/TomcatWebappClassLoaderBaseHandler.java @@ -31,104 +31,164 @@ import java.io.File; import java.util.List; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** Extract classpath entries from the Tomcat/Catalina WebappClassLoaderBase. */ class TomcatWebappClassLoaderBaseHandler implements ClassLoaderHandler { + /** Class cannot be constructed. */ + private TomcatWebappClassLoaderBaseHandler() { + } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public String[] handledClassLoaders() { - return new String[] { // - "org.apache.catalina.loader.WebappClassLoaderBase", // - }; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.apache.catalina.loader.WebappClassLoaderBase"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Return true if this classloader delegates to its parent. + * + * @param classLoader + * the {@link ClassLoader}. + * @return true if this classloader delegates to its parent. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + private static boolean isParentFirst(final ClassLoader classLoader, final ReflectionUtils reflectionUtils) { + final Object delegateObject = reflectionUtils.getFieldVal(false, classLoader, "delegate"); + if (delegateObject != null) { + return (boolean) delegateObject; + } + // Assume parent-first delegation order + return true; } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + final boolean isParentFirst = isParentFirst(classLoader, classLoaderOrder.reflectionUtils); + if (isParentFirst) { + // Use parent-first delegation order + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + } + if ("org.apache.tomee.catalina.TomEEWebappClassLoader".equals(classLoader.getClass().getName())) { + // TomEEWebappClassLoader has a lot of complex delegation rules, including classname-specific + // delegation, which is not supported by the current ClassGraph model, so we just try to approximate + // the delegation order with a fixed order. + try { + classLoaderOrder.delegateTo(Class.forName("org.apache.openejb.OpenEJB").getClassLoader(), + /* isParent = */ true, log); + } catch (LinkageError | ClassNotFoundException e) { + // Ignore + } + } + classLoaderOrder.add(classLoader, log); + if (!isParentFirst) { + // Use parent-last delegation order + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + } } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { // type StandardRoot (implements WebResourceRoot) - final Object resources = ReflectionUtils.invokeMethod(classLoader, "getResources", false); + final Object resources = classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getResources"); // type List - final Object baseURLs = ReflectionUtils.invokeMethod(resources, "getBaseUrls", false); - classpathOrderOut.addClasspathEntryObject(baseURLs, classLoader, log); + final Object baseURLs = classpathOrder.reflectionUtils.invokeMethod(false, resources, "getBaseUrls"); + classpathOrder.addClasspathEntryObject(baseURLs, classLoader, scanSpec, log); // type List> - // members: preResources, mainResources, classResources, jarResources, postResources + // members: preResources, mainResources, classResources, jarResources, + // postResources @SuppressWarnings("unchecked") - final List> allResources = (List>) ReflectionUtils.getFieldVal(resources, "allResources", - false); + final List> allResources = (List>) classpathOrder.reflectionUtils.getFieldVal(false, + resources, "allResources"); if (allResources != null) { - // type List + // type List for (final List webResourceSetList : allResources) { // type WebResourceSet - // {DirResourceSet, FileResourceSet, JarResourceSet, JarWarResourceSet, EmptyResourceSet} + // {DirResourceSet, FileResourceSet, JarResourceSet, JarWarResourceSet, + // EmptyResourceSet} for (final Object webResourceSet : webResourceSetList) { - // For DirResourceSet - final File file = (File) ReflectionUtils.invokeMethod(webResourceSet, "getFileBase", false); - String base = file == null ? null : file.getPath(); - if (base == null) { - // For FileResourceSet - base = (String) ReflectionUtils.invokeMethod(webResourceSet, "getBase", false); - } - if (base == null) { - // For JarResourceSet and JarWarResourceSet - // The absolute path to the WAR file on the file system in which the JAR is located - base = (String) ReflectionUtils.invokeMethod(webResourceSet, "getBaseUrlString", false); - } - if (base != null) { - // For JarWarResourceSet: the path within the WAR file where the JAR file is located - final String archivePath = (String) ReflectionUtils.getFieldVal(webResourceSet, - "archivePath", false); - if (archivePath != null && !archivePath.isEmpty()) { - // If archivePath is non-null, this is a jar within a war - base += "!" + (archivePath.startsWith("/") ? archivePath : "/" + archivePath); + if (webResourceSet != null) { + // For DirResourceSet + final File file = (File) classpathOrder.reflectionUtils.invokeMethod(false, webResourceSet, + "getFileBase"); + String base = file == null ? null : file.getPath(); + if (base == null) { + // For FileResourceSet + base = (String) classpathOrder.reflectionUtils.invokeMethod(false, webResourceSet, + "getBase"); + } + if (base == null) { + // For JarResourceSet and JarWarResourceSet + // The absolute path to the WAR file on the file system in which the JAR is + // located + base = (String) classpathOrder.reflectionUtils.invokeMethod(false, webResourceSet, + "getBaseUrlString"); } - final String className = webResourceSet.getClass().getName(); - final boolean isJar = className - .equals("java.org.apache.catalina.webresources.JarResourceSet") - || className.equals("java.org.apache.catalina.webresources.JarWarResourceSet"); - // The path within this WebResourceSet where resources will be served from, - // e.g. for a resource JAR, this would be "META-INF/resources" - final String internalPath = (String) ReflectionUtils.invokeMethod(webResourceSet, - "getInternalPath", false); - if (internalPath != null && !internalPath.isEmpty() && !internalPath.equals("/")) { - classpathOrderOut.addClasspathEntryObject( - base + (isJar ? "!" : "") - + (internalPath.startsWith("/") ? internalPath : "/" + internalPath), - classLoader, log); - } else { - classpathOrderOut.addClasspathEntryObject(base, classLoader, log); + if (base != null) { + // For JarWarResourceSet: the path within the WAR file where the JAR file is + // located + final String archivePath = (String) classpathOrder.reflectionUtils.getFieldVal(false, + webResourceSet, "archivePath"); + if (archivePath != null && !archivePath.isEmpty()) { + // If archivePath is non-null, this is a jar within a war + base += "!" + (archivePath.startsWith("/") ? archivePath : "/" + archivePath); + } + final String className = webResourceSet.getClass().getName(); + final boolean isJar = className + .equals("java.org.apache.catalina.webresources.JarResourceSet") + || className.equals("java.org.apache.catalina.webresources.JarWarResourceSet"); + // The path within this WebResourceSet where resources will be served from, + // e.g. for a resource JAR, this would be "META-INF/resources" + final String internalPath = (String) classpathOrder.reflectionUtils.invokeMethod(false, + webResourceSet, "getInternalPath"); + if (internalPath != null && !internalPath.isEmpty() && !internalPath.equals("/")) { + classpathOrder.addClasspathEntryObject(base + (isJar ? "!" : "") + + (internalPath.startsWith("/") ? internalPath : "/" + internalPath), + classLoader, scanSpec, log); + } else { + classpathOrder.addClasspathEntryObject(base, classLoader, scanSpec, log); + } } } } } } // This may or may not duplicate the above - final Object urls = ReflectionUtils.invokeMethod(classLoader, "getURLs", false); - classpathOrderOut.addClasspathEntryObject(urls, classLoader, log); + final Object urls = classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getURLs"); + classpathOrder.addClasspathEntryObject(urls, classLoader, scanSpec, log); } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/URLClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/URLClassLoaderHandler.java index 25436dd0f..9b2e9c4d9 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/URLClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/URLClassLoaderHandler.java @@ -31,48 +31,66 @@ import java.net.URL; import java.net.URLClassLoader; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; /** ClassLoaderHandler that is able to extract the URLs from a URLClassLoader. */ class URLClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { "java.net.URLClassLoader" }; + /** Class cannot be constructed. */ + private URLClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, "java.net.URLClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { final URL[] urls = ((URLClassLoader) classLoader).getURLs(); if (urls != null) { for (final URL url : urls) { if (url != null) { - classpathOrderOut.addClasspathEntry(url.toString(), classLoader, log); + classpathOrder.addClasspathEntry(url, classLoader, scanSpec, log); } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/UnoOneJarClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/UnoOneJarClassLoaderHandler.java new file mode 100644 index 000000000..570705e0b --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/UnoOneJarClassLoaderHandler.java @@ -0,0 +1,121 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.classloaderhandler; + +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; +import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.LogNode; + +/** Extract classpath entries from the Uno-Jar's JarClassLoader and One-Jar's JarClassLoader. */ +class UnoOneJarClassLoaderHandler implements ClassLoaderHandler { + /** Class cannot be constructed. */ + private UnoOneJarClassLoaderHandler() { + } + + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. + */ + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "com.needhamsoftware.unojar.JarClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "com.simontuffs.onejar.JarClassLoader"); + } + + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log + */ + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); + } + + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. + */ + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + + // For Uno-Jar: + + final String unoJarOneJarPath = (String) classpathOrder.reflectionUtils.invokeMethod(false, classLoader, + "getOneJarPath"); + classpathOrder.addClasspathEntry(unoJarOneJarPath, classLoader, scanSpec, log); + + // If this property is defined, Uno-Jar jar path was specified on commandline. Otherwise, jar path + // should be contained in java.class.path (which will be separately picked up by ClassGraph, as + // long as classloaders/classpath are not overloaded and parent classloaders are not ignored). + final String unoJarJarPath = System.getProperty("uno-jar.jar.path"); + classpathOrder.addClasspathEntry(unoJarJarPath, classLoader, scanSpec, log); + + // For One-Jar: + + // If this property is defined, One-Jar jar path was specified on commandline. Otherwise, jar path + // should be contained in java.class.path (which will be separately picked up by ClassGraph, as + // long as classloaders/classpath are not overloaded and parent classloaders are not ignored). + final String oneJarJarPath = System.getProperty("one-jar.jar.path"); + classpathOrder.addClasspathEntry(oneJarJarPath, classLoader, scanSpec, log); + + // If this property is defined, additional classpath entries were specified in OneJar format + // on the commandline, with '|' as a separator + final String oneJarClassPath = System.getProperty("one-jar.class.path"); + if (oneJarClassPath != null) { + classpathOrder.addClasspathEntryObject(oneJarClassPath.split("\\|"), classLoader, scanSpec, log); + } + + // For both UnoJar and OneJar, "libs/" and "main/" will be automatically picked up as library roots + // for nested jars, based on ClassLoaderHandlerRegistry.AUTOMATIC_LIB_DIR_PREFIXES. + // ("main/" contains "main.jar".) + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WeblogicClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WeblogicClassLoaderHandler.java index 79ac54dba..dbd257951 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WeblogicClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WeblogicClassLoaderHandler.java @@ -28,54 +28,77 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** Extract classpath entries from the Weblogic ClassLoaders. */ class WeblogicClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { // - "weblogic.utils.classloaders.ChangeAwareClassLoader", // - "weblogic.utils.classloaders.GenericClassLoader", // - "weblogic.utils.classloaders.FilteringClassLoader", // - // TODO: other known classloader names: - // weblogic.servlet.jsp.JspClassLoader - // weblogic.servlet.jsp.TagFileClassLoader - }; + /** Class cannot be constructed. */ + private WeblogicClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "weblogic.utils.classloaders.ChangeAwareClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "weblogic.utils.classloaders.GenericClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "weblogic.utils.classloaders.FilteringClassLoader") + // TODO: The following two known classloader names have not been tested, and the fields/methods + // may not match those of the above classloaders. + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "weblogic.servlet.jsp.JspClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "weblogic.servlet.jsp.TagFileClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { - classpathOrderOut.addClasspathEntries( // - (String) ReflectionUtils.invokeMethod(classLoader, "getFinderClassPath", false), classLoader, log); - classpathOrderOut.addClasspathEntries( // - (String) ReflectionUtils.invokeMethod(classLoader, "getClassPath", false), classLoader, log); + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + classpathOrder.addClasspathPathStr( // + (String) classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getFinderClassPath"), + classLoader, scanSpec, log); + classpathOrder.addClasspathPathStr( // + (String) classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getClassPath"), + classLoader, scanSpec, log); } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereLibertyClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereLibertyClassLoaderHandler.java index 2bd72920b..d47958beb 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereLibertyClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereLibertyClassLoaderHandler.java @@ -3,13 +3,15 @@ * * Author: R. Kempees * + * With contributions from @cpierceworld (#414) + * * Hosted at: https://github.com/classgraph/classgraph * * -- * * The MIT License (MIT) * - * Copyright (c) 2017 R. Kempees + * Copyright (c) 2017 R. Kempees (contributed to the ClassGraph project) * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated * documentation files (the "Software"), to deal in the Software without restriction, including without @@ -29,12 +31,18 @@ package nonapi.io.github.classgraph.classloaderhandler; import java.io.File; +import java.net.URL; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * WebsphereLibertyClassLoaderHandler. @@ -45,7 +53,6 @@ * @author R. Kempees */ class WebsphereLibertyClassLoaderHandler implements ClassLoaderHandler { - /** {@code "com.ibm.ws.classloading.internal."} */ private static final String PKG_PREFIX = "com.ibm.ws.classloading.internal."; @@ -55,89 +62,188 @@ class WebsphereLibertyClassLoaderHandler implements ClassLoaderHandler { /** {@code "com.ibm.ws.classloading.internal.ThreadContextClassLoader"} */ private static final String IBM_THREAD_CONTEXT_CLASS_LOADER = PKG_PREFIX + "ThreadContextClassLoader"; - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - return new String[] { IBM_APP_CLASS_LOADER, IBM_THREAD_CONTEXT_CLASS_LOADER }; + /** Class cannot be constructed. */ + private WebsphereLibertyClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, IBM_APP_CLASS_LOADER) + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + IBM_THREAD_CONTEXT_CLASS_LOADER); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - // TODO: Read correct delegation order from ClassLoader - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } /** - * Get the path from a classpath object. + * Get the paths from a containerClassLoader object. * - * @param classpath - * the classpath object - * @return the path object as a {@link File} or {@link String}. + *

+ * The passed in object should be an instance of "com.ibm.ws.classloading.internal.ContainerClassLoader". + *

+ * Will attempt to use "getContainerURLs" methods to recap the classpath. + * + * @param containerClassLoader + * the containerClassLoader object + * @return Collection of path objects as a {@link URL} or {@link String}. */ - private String getPath(final Object classpath) { - final Object container = ReflectionUtils.getFieldVal(classpath, "container", false); + private static Collection getPaths(final Object containerClassLoader, + final ReflectionUtils reflectionUtils) { + if (containerClassLoader == null) { + return Collections.emptyList(); + } + + // Expecting this to be an instance of + // "com.ibm.ws.classloading.internal.ContainerClassLoader$UniversalContainer". + // Call "getContainerURLs" to get its container's classpath. + Collection urls = callGetUrls(containerClassLoader, "getContainerURLs", reflectionUtils); + if (urls != null && !urls.isEmpty()) { + return urls; + } + + // "getContainerURLs" didn't work, try getting the container object... + final Object container = reflectionUtils.getFieldVal(false, containerClassLoader, "container"); if (container == null) { - return ""; + return Collections.emptyList(); + } + + // Should be an instance of "com.ibm.wsspi.adaptable.module.Container". + // Call "getURLs" to get its classpath. + urls = callGetUrls(container, "getURLs", reflectionUtils); + if (urls != null && !urls.isEmpty()) { + return urls; } - final Object delegate = ReflectionUtils.getFieldVal(container, "delegate", false); + // "getURLs" did not work, reverting to previous logic of introspection of the "delegate". + final Object delegate = reflectionUtils.getFieldVal(false, container, "delegate"); if (delegate == null) { - return ""; + return Collections.emptyList(); } - final String path = (String) ReflectionUtils.getFieldVal(delegate, "path", false); + final String path = (String) reflectionUtils.getFieldVal(false, delegate, "path"); if (path != null && path.length() > 0) { - return path; + return Collections.singletonList((Object) path); } - final Object base = ReflectionUtils.getFieldVal(delegate, "base", false); + final Object base = reflectionUtils.getFieldVal(false, delegate, "base"); if (base == null) { // giving up. - return ""; + return Collections.emptyList(); } - final Object archiveFile = ReflectionUtils.getFieldVal(base, "archiveFile", false); + final Object archiveFile = reflectionUtils.getFieldVal(false, base, "archiveFile"); if (archiveFile != null) { final File file = (File) archiveFile; - return file.getAbsolutePath(); + return Collections.singletonList((Object) file.getAbsolutePath()); + } + return Collections.emptyList(); + } + + /** + * Utility to call a "getURLs" method, flattening "collections of collections" and ignoring + * "UnsupportedOperationException". + * + * All of the "getURLs" methods eventually call "com.ibm.wsspi.adaptable.module.Container#getURLs()". + * + * https://www.ibm.com/support/knowledgecenter/SSEQTP_liberty/com.ibm.websphere.javadoc.liberty.doc + * /com.ibm.websphere.appserver.spi.artifact_1.2-javadoc + * /com/ibm/wsspi/adaptable/module/Container.html?view=embed#getURLs() "A collection of URLs that represent all + * of the locations on disk that contribute to this container" + */ + @SuppressWarnings("unchecked") + private static Collection callGetUrls(final Object container, final String methodName, + final ReflectionUtils reflectionUtils) { + if (container != null) { + try { + final Collection results = (Collection) reflectionUtils.invokeMethod(false, + container, methodName); + if (results != null && !results.isEmpty()) { + final Collection allUrls = new HashSet<>(); + for (final Object result : results) { + if (result instanceof Collection) { + // SmartClassPath returns collection of collection of URLs. + for (final Object url : ((Collection) result)) { + if (url != null) { + allUrls.add(url); + } + } + } else if (result != null) { + allUrls.add(result); + } + } + return allUrls; + } + } catch (final UnsupportedOperationException e) { + /* ignore */ + } } - return ""; + return Collections.emptyList(); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClasspathOrder classpathOrderOut, final LogNode log) { + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { Object smartClassPath; - final Object appLoader = ReflectionUtils.getFieldVal(classLoader, "appLoader", false); + final Object appLoader = classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "appLoader"); if (appLoader != null) { - smartClassPath = ReflectionUtils.getFieldVal(appLoader, "smartClassPath", false); + smartClassPath = classpathOrder.reflectionUtils.getFieldVal(false, appLoader, "smartClassPath"); } else { - smartClassPath = ReflectionUtils.getFieldVal(classLoader, "smartClassPath", false); + smartClassPath = classpathOrder.reflectionUtils.getFieldVal(false, classLoader, "smartClassPath"); } if (smartClassPath != null) { - final List classPathElements = (List) ReflectionUtils.getFieldVal(smartClassPath, "classPath", - false); - if (classPathElements != null) { - for (final Object classpath : classPathElements) { - final String path = getPath(classpath); - if (path != null && path.length() > 0) { - classpathOrderOut.addClasspathEntry(path, classLoader, log); + // "com.ibm.ws.classloading.internal.ContainerClassLoader$SmartClassPath" + // interface specifies a "getClassPath" to return all urls that makeup its path. + final Collection paths = callGetUrls(smartClassPath, "getClassPath", + classpathOrder.reflectionUtils); + if (!paths.isEmpty()) { + for (final Object path : paths) { + classpathOrder.addClasspathEntry(path, classLoader, scanSpec, log); + } + } else { + // "getClassPath" didn't work... reverting to looping over "classPath" elements. + @SuppressWarnings("unchecked") + final List classPathElements = (List) classpathOrder.reflectionUtils + .getFieldVal(false, smartClassPath, "classPath"); + if (classPathElements != null && !classPathElements.isEmpty()) { + for (final Object classPathElement : classPathElements) { + final Collection subPaths = getPaths(classPathElement, + classpathOrder.reflectionUtils); + for (final Object path : subPaths) { + classpathOrder.addClasspathEntry(path, classLoader, scanSpec, log); + } } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereTraditionalClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereTraditionalClassLoaderHandler.java index 88572aabe..05d6918eb 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereTraditionalClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereTraditionalClassLoaderHandler.java @@ -28,10 +28,11 @@ */ package nonapi.io.github.classgraph.classloaderhandler; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.classpath.ClassLoaderFinder; +import nonapi.io.github.classgraph.classpath.ClassLoaderOrder; import nonapi.io.github.classgraph.classpath.ClasspathOrder; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * Handle the WebSphere traditonal ClassLoaders. @@ -39,43 +40,60 @@ * @author lukehutch */ class WebsphereTraditionalClassLoaderHandler implements ClassLoaderHandler { - - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handledClassLoaders() - */ - @Override - public String[] handledClassLoaders() { - // All three class loaders implement the getClassPath method call. - return new String[] { // - "com.ibm.ws.classloader.CompoundClassLoader", // - "com.ibm.ws.classloader.ProtectionClassLoader", // - "com.ibm.ws.bootstrap.ExtClassLoader" }; + /** Class cannot be constructed. */ + private WebsphereTraditionalClassLoaderHandler() { } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getEmbeddedClassLoader(java.lang.ClassLoader) + /** + * Check whether this {@link ClassLoaderHandler} can handle a given {@link ClassLoader}. + * + * @param classLoaderClass + * the {@link ClassLoader} class or one of its superclasses. + * @param log + * the log + * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ - @Override - public ClassLoader getEmbeddedClassLoader(final ClassLoader outerClassLoaderInstance) { - return null; + public static boolean canHandle(final Class classLoaderClass, final LogNode log) { + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "com.ibm.ws.classloader.CompoundClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "com.ibm.ws.classloader.ProtectionClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "com.ibm.ws.bootstrap.ExtClassLoader"); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#getDelegationOrder(java.lang.ClassLoader) + /** + * Find the {@link ClassLoader} delegation order for a {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the order for. + * @param classLoaderOrder + * a {@link ClassLoaderOrder} object to update. + * @param log + * the log */ - @Override - public DelegationOrder getDelegationOrder(final ClassLoader classLoaderInstance) { - // TODO: Read correct delegation order from ClassLoader - return DelegationOrder.PARENT_FIRST; + public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, + final LogNode log) { + classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); + classLoaderOrder.add(classLoader, log); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler#handle(nonapi.io.github.classgraph.ScanSpec, java.lang.ClassLoader, nonapi.io.github.classgraph.classpath.ClasspathOrder, nonapi.io.github.classgraph.utils.LogNode) + /** + * Find the classpath entries for the associated {@link ClassLoader}. + * + * @param classLoader + * the {@link ClassLoader} to find the classpath entries order for. + * @param classpathOrder + * a {@link ClasspathOrder} object to update. + * @param scanSpec + * the {@link ScanSpec}. + * @param log + * the log. */ - @Override - public void handle(final ScanSpec scanSpec, final ClassLoader classloader, - final ClasspathOrder classpathOrderOut, final LogNode log) { - final String classpath = (String) ReflectionUtils.invokeMethod(classloader, "getClassPath", false); - classpathOrderOut.addClasspathEntries(classpath, classloader, log); + public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, + final ScanSpec scanSpec, final LogNode log) { + final String classpath = (String) classpathOrder.reflectionUtils.invokeMethod(false, classLoader, + "getClassPath"); + classpathOrder.addClasspathPathStr(classpath, classLoader, scanSpec, log); } } diff --git a/src/main/java/nonapi/io/github/classgraph/classpath/CallStackReader.java b/src/main/java/nonapi/io/github/classgraph/classpath/CallStackReader.java index 204fbf695..499996fa7 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/CallStackReader.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/CallStackReader.java @@ -28,25 +28,27 @@ */ package nonapi.io.github.classgraph.classpath; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Callable; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; import nonapi.io.github.classgraph.utils.LogNode; import nonapi.io.github.classgraph.utils.VersionFinder; /** A class to find the unique ordered classpath elements. */ class CallStackReader { + ReflectionUtils reflectionUtils; /** * Constructor. */ - private CallStackReader() { - // Cannot be constructed + public CallStackReader(final ReflectionUtils reflectionUtils) { + this.reflectionUtils = reflectionUtils; } /** @@ -74,7 +76,7 @@ private static Class[] getCallStackViaStackWalker() { .getMethod("getDeclaringClass"); stackWalkerClass.getMethod("forEach", consumerClass).invoke(stackWalkerInstance, // // InvocationHandler proxy for Consumer - Proxy.newProxyInstance(consumerClass.getClassLoader(), new Class[] { consumerClass }, + Proxy.newProxyInstance(consumerClass.getClassLoader(), new Class[] { consumerClass }, new InvocationHandler() { @Override public Object invoke(final Object proxy, final Method method, final Object[] args) @@ -95,24 +97,7 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg // ------------------------------------------------------------------------------------------------------------- /** - * Using a SecurityManager gets around the fact that Oracle removed sun.reflect.Reflection.getCallerClass, see: - * - * https://www.infoq.com/news/2013/07/Oracle-Removes-getCallerClass - * - * http://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html - */ - private static final class CallerResolver extends SecurityManager { - /* (non-Javadoc) - * @see java.lang.SecurityManager#getClassContext() - */ - @Override - protected Class[] getClassContext() { - return super.getClassContext(); - } - } - - /** - * Get the call stack via the SecurityManager API. + * Get the call stack via the SecurityManager.getClassContext() native method. * * @param log * the log @@ -120,12 +105,27 @@ protected Class[] getClassContext() { */ private static Class[] getCallStackViaSecurityManager(final LogNode log) { try { - return new CallerResolver().getClassContext(); - } catch (final SecurityException e) { + // Call method via reflection, since SecurityManager is deprecated in JDK 17. + final Class securityManagerClass = Class.forName("java.lang.SecurityManager"); + Object securityManager = null; + for (final Constructor constructor : securityManagerClass.getDeclaredConstructors()) { + if (constructor.getParameterTypes().length == 0) { + securityManager = constructor.newInstance(); + break; + } + } + if (securityManager != null) { + final Method getClassContext = securityManager.getClass().getDeclaredMethod("getClassContext"); + getClassContext.setAccessible(true); + return (Class[]) getClassContext.invoke(securityManager); + } else { + return null; + } + } catch (final Throwable t) { // Creating a SecurityManager can fail if the current SecurityManager does not allow // RuntimePermission("createSecurityManager") if (log != null) { - log.log("Exception while trying to obtain call stack via SecurityManager", e); + log.log("Exception while trying to obtain call stack via SecurityManager", t); } return null; } @@ -140,51 +140,90 @@ private static Class[] getCallStackViaSecurityManager(final LogNode log) { * the log * @return The classes in the call stack. */ - static Class[] getClassContext(final LogNode log) { - // For JRE 9+, use StackWalker to get call stack - Class[] stack = null; - if (VersionFinder.JAVA_MAJOR_VERSION >= 9) { + Class[] getClassContext(final LogNode log) { + Class[] callStack = null; + + // For JRE 9+, use StackWalker to get call stack. + if (VersionFinder.JAVA_MAJOR_VERSION == 9 // + || VersionFinder.JAVA_MAJOR_VERSION == 10 // + || (VersionFinder.JAVA_MAJOR_VERSION == 11 // + && VersionFinder.JAVA_MINOR_VERSION == 0 + && (VersionFinder.JAVA_SUB_VERSION < 4 + || (VersionFinder.JAVA_SUB_VERSION == 4 && VersionFinder.JAVA_IS_EA_VERSION))) + || (VersionFinder.JAVA_MAJOR_VERSION == 12 && VersionFinder.JAVA_MINOR_VERSION == 0 + && (VersionFinder.JAVA_SUB_VERSION < 2 + || (VersionFinder.JAVA_SUB_VERSION == 2 && VersionFinder.JAVA_IS_EA_VERSION)))) { + // Don't trigger the StackWalker bug that crashed the JVM, which was fixed in JDK 13, + // and backported to 12.0.2 and 11.0.4 (probably introduced in JDK 9, when StackWalker + // was introduced): + // https://github.com/classgraph/classgraph/issues/341 + // https://bugs.openjdk.java.net/browse/JDK-8210457 + // -- fall through + } else { + // Get the stack via StackWalker. // Invoke with doPrivileged -- see: // http://mail.openjdk.java.net/pipermail/jigsaw-dev/2018-October/013974.html - stack = AccessController.doPrivileged(new PrivilegedAction[]>() { - @Override - public Class[] run() { - return getCallStackViaStackWalker(); - } - }); + try { + callStack = reflectionUtils.doPrivileged(new Callable[]>() { + @Override + public Class[] call() throws Exception { + return getCallStackViaStackWalker(); + } + }); + } catch (final Throwable e) { + // Fall through + } } - // For JRE 7 and 8, use SecurityManager to get call stack - if (stack == null) { - stack = AccessController.doPrivileged(new PrivilegedAction[]>() { - @Override - public Class[] run() { - return getCallStackViaSecurityManager(log); - } - }); + // For JRE 7 and 8, use SecurityManager to get call stack (don't use this method on JDK 9+, + // because it will result in a reflective illegal access warning, see #663) + if (VersionFinder.JAVA_MAJOR_VERSION < 9 && (callStack == null || callStack.length == 0)) { + try { + callStack = reflectionUtils.doPrivileged(new Callable[]>() { + @Override + public Class[] call() throws Exception { + return getCallStackViaSecurityManager(log); + } + }); + } catch (final Throwable e) { + // Fall through + } } // As a fallback, use getStackTrace() to try to get the call stack - if (stack == null) { + if (callStack == null || callStack.length == 0) { + StackTraceElement[] stackTrace = null; try { - throw new Exception(); - } catch (final Exception e) { - final List> classes = new ArrayList<>(); - for (final StackTraceElement elt : e.getStackTrace()) { - try { - classes.add(Class.forName(elt.getClassName())); - } catch (final ClassNotFoundException | LinkageError ignored) { - // Ignored - } + stackTrace = Thread.currentThread().getStackTrace(); + } catch (final SecurityException e) { + // Fall through + } + if (stackTrace == null || stackTrace.length == 0) { + try { + // Try getting stacktrace by throwing an exception + throw new Exception(); + } catch (final Exception e) { + stackTrace = e.getStackTrace(); } - if (!classes.isEmpty()) { - stack = classes.toArray(new Class[0]); - } else { - // Last-ditch effort -- include just this class in the call stack - stack = new Class[] { CallStackReader.class }; + } + final List> stackClassesList = new ArrayList<>(); + for (final StackTraceElement elt : stackTrace) { + try { + stackClassesList.add(Class.forName(elt.getClassName())); + } catch (final ClassNotFoundException | LinkageError ignored) { + // Ignored } } + if (!stackClassesList.isEmpty()) { + callStack = stackClassesList.toArray(new Class[0]); + } } - return stack; + + // Last-ditch effort -- include just this class in the call stack + if (callStack == null || callStack.length == 0) { + callStack = new Class[] { CallStackReader.class }; + } + + return callStack; } } diff --git a/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderFinder.java b/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderFinder.java new file mode 100644 index 000000000..95a958a06 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderFinder.java @@ -0,0 +1,161 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.classpath; + +import java.util.LinkedHashSet; + +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.LogNode; + +/** A class to find the unique ordered classpath elements. */ +public class ClassLoaderFinder { + /** The context class loaders. */ + private final ClassLoader[] contextClassLoaders; + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get the context class loaders. + * + * @return The context classloader, and any other classloader that is not an ancestor of context classloader. + */ + public ClassLoader[] getContextClassLoaders() { + return contextClassLoaders; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** Return true if the class is, extends, or implements a given named class or interface. */ + // TODO: make this a default method of the ClassLoaderHandler interface in ClassGraph 5.x + public static boolean classIsOrExtendsOrImplements(Class cls, String className) { + if (cls == null) { + return false; + } + if (cls.getName().equals(className)) { + return true; + } + if (classIsOrExtendsOrImplements(cls.getSuperclass(), className)) { + return true; + } + for (Class iface : cls.getInterfaces()) { + if (classIsOrExtendsOrImplements(iface, className)) { + return true; + } + } + return false; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * A class to find the unique ordered classpath elements. + * + * @param scanSpec + * The scan spec, or null if none available. + * @param log + * The log. + */ + ClassLoaderFinder(final ScanSpec scanSpec, final ReflectionUtils reflectionUtils, final LogNode log) { + LinkedHashSet classLoadersUnique; + LogNode classLoadersFoundLog; + if (scanSpec.overrideClassLoaders == null) { + // ClassLoaders were not overridden + + // There's some advice here about choosing the best or the right classloader, but it is not complete + // (e.g. it doesn't cover parent delegation modes): + // http://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html?page=2 + + // Get thread context classloader (this is the first classloader to try, since a context classloader + // can be set as an override on a per-thread basis) + classLoadersUnique = new LinkedHashSet<>(); + final ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader(); + if (threadClassLoader != null) { + classLoadersUnique.add(threadClassLoader); + } + + // Get classloader for this class, which will generally be the classloader of the class that + // called ClassGraph (the classloader of the caller is used by Class.forName(className), when + // no classloader is provided) + final ClassLoader currClassClassLoader = getClass().getClassLoader(); + if (currClassClassLoader != null) { + classLoadersUnique.add(currClassClassLoader); + } + + // Get system classloader (this is a fallback if one of the above do not work) + final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + if (systemClassLoader != null) { + classLoadersUnique.add(systemClassLoader); + } + + // There is one more classloader in JDK9+, the platform classloader (used for handling extensions), + // see: http://openjdk.java.net/jeps/261#Class-loaders + // The method call to get it is ClassLoader.getPlatformClassLoader() + // However, since it's not possible to get URLs from this classloader, and it is the parent of + // the application classloader returned by ClassLoader.getSystemClassLoader() (so is delegated to + // by the application classloader), there is no point adding it here. Modules are scanned + // directly anyway, so we don't need to get module path entries from the platform classloader. + + // Find classloaders for classes on callstack, in case any were missed + try { + final Class[] callStack = new CallStackReader(reflectionUtils).getClassContext(log); + for (int i = callStack.length - 1; i >= 0; --i) { + final ClassLoader callerClassLoader = callStack[i].getClassLoader(); + if (callerClassLoader != null) { + classLoadersUnique.add(callerClassLoader); + } + } + } catch (final IllegalArgumentException e) { + if (log != null) { + log.log("Could not get call stack", e); + } + } + + // Add any custom-added classloaders after system/context/module classloaders + if (scanSpec.addedClassLoaders != null) { + classLoadersUnique.addAll(scanSpec.addedClassLoaders); + } + classLoadersFoundLog = log == null ? null : log.log("Found ClassLoaders:"); + + } else { + // ClassLoaders were overridden + classLoadersUnique = new LinkedHashSet<>(scanSpec.overrideClassLoaders); + classLoadersFoundLog = log == null ? null : log.log("Override ClassLoaders:"); + } + + // Log all identified ClassLoaders + if (classLoadersFoundLog != null) { + for (final ClassLoader classLoader : classLoadersUnique) { + classLoadersFoundLog.log(classLoader.getClass().getName()); + } + } + + this.contextClassLoaders = classLoadersUnique.toArray(new ClassLoader[0]); + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderOrder.java b/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderOrder.java new file mode 100644 index 000000000..2394178a5 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderOrder.java @@ -0,0 +1,167 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.classpath; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import io.github.classgraph.ClassGraph; +import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandlerRegistry; +import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandlerRegistry.ClassLoaderHandlerRegistryEntry; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.utils.LogNode; + +/** A class to find all unique classloaders. */ +public class ClassLoaderOrder { + /** The {@link ClassLoader} order. */ + private final Map> classLoaderOrder = new LinkedHashMap<>(); + + public ReflectionUtils reflectionUtils; + + /** + * The set of all {@link ClassLoader} instances that have been added to the order so far, so that classloaders + * don't get added twice. + */ + // Need to use IdentityHashMap for maps and sets here, because TomEE weirdly makes instances of + // CxfContainerClassLoader equal to (via .equals()) the instance of TomEEWebappClassLoader that it + // delegates to (#515) + private final Set added = Collections.newSetFromMap(new IdentityHashMap()); + + /** + * The set of all {@link ClassLoader} instances that have been delegated to so far, to prevent an infinite loop + * in delegation. + */ + private final Set delegatedTo = Collections + .newSetFromMap(new IdentityHashMap()); + + /** + * The set of all parent {@link ClassLoader} instances that have been delegated to so far, to enable + * {@link ClassGraph#ignoreParentClassLoaders()}. + */ + private final Set allParentClassLoaders = Collections + .newSetFromMap(new IdentityHashMap()); + + // ------------------------------------------------------------------------------------------------------------- + + public ClassLoaderOrder(final ReflectionUtils reflectionUtils) { + this.reflectionUtils = reflectionUtils; + } + + /** + * Get the {@link ClassLoader} order. + * + * @return the {@link ClassLoader} order, as a pair: {@link ClassLoader}, + * {@link ClassLoaderHandlerRegistryEntry}. + */ + public List>> getClassLoaderOrder() { + return new ArrayList<>(classLoaderOrder.entrySet()); + } + + /** + * Get the all parent classloaders. + * + * @return all parent classloaders + */ + public Set getAllParentClassLoaders() { + return allParentClassLoaders; + } + + /** Get the ClassLoaderHandler(s) that can handle a given ClassLoader. */ + private static List getClassLoaderHandlerRegistryEntries( + final ClassLoader classLoader, final LogNode log) { + List ents = new ArrayList<>(); + boolean matched = false; + for (final ClassLoaderHandlerRegistryEntry ent : ClassLoaderHandlerRegistry.CLASS_LOADER_HANDLERS) { + if (ent.canHandle(classLoader.getClass(), log)) { + // This ClassLoaderHandler can handle the ClassLoader class, or one of its superclasses + ents.add(ent); + matched = true; + } + } + if (!matched) { + ents.add(ClassLoaderHandlerRegistry.FALLBACK_HANDLER); + } + return ents; + } + + /** + * Add a {@link ClassLoader} to the ClassLoader order at the current position. + * + * @param classLoader + * the class loader + * @param log + * the log + */ + public void add(final ClassLoader classLoader, final LogNode log) { + if (classLoader == null) { + return; + } + if (added.add(classLoader)) { + classLoaderOrder.put(classLoader, getClassLoaderHandlerRegistryEntries(classLoader, log)); + } + } + + /** + * Recursively delegate to another {@link ClassLoader}. + * + * @param classLoader + * the class loader + * @param isParent + * true if this is a parent of another classloader + * @param log + * the log + */ + public void delegateTo(final ClassLoader classLoader, final boolean isParent, final LogNode log) { + if (classLoader == null) { + return; + } + // Check if this is a parent before checking if the classloader is already in the delegatedTo set, + // so that if the classloader is a context classloader but also a parent, it still gets marked as + // a parent classloader. + if (isParent) { + allParentClassLoaders.add(classLoader); + } + // Don't delegate to a classloader twice + if (delegatedTo.add(classLoader)) { + add(classLoader, log); + // Recurse to get delegation order + // (note: may be wrong if multiple ClassLoaderHandlers can handle this classloader) + for (final ClassLoaderHandlerRegistryEntry entry : getClassLoaderHandlerRegistryEntries(classLoader, + /* Don't log twice -- also logged by add method above */ null)) { + entry.findClassLoaderOrder(classLoader, this, log); + } + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathFinder.java b/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathFinder.java index f758c5a78..09e3581bb 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathFinder.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathFinder.java @@ -28,19 +28,16 @@ */ package nonapi.io.github.classgraph.classpath; -import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; +import java.util.Map.Entry; import java.util.Set; -import io.github.classgraph.ClassGraphException; -import nonapi.io.github.classgraph.ScanSpec; -import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler; -import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler.DelegationOrder; +import io.github.classgraph.ClassGraphClassLoader; import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandlerRegistry; import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandlerRegistry.ClassLoaderHandlerRegistryEntry; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.FastPathResolver; import nonapi.io.github.classgraph.utils.FileUtils; import nonapi.io.github.classgraph.utils.JarUtils; @@ -48,176 +45,64 @@ /** A class to find the unique ordered classpath elements. */ public class ClasspathFinder { - /** The classpath order. */ private final ClasspathOrder classpathOrder; - /** The classloader and module finder. */ - private final ClassLoaderAndModuleFinder classLoaderAndModuleFinder; + /** The {@link ModuleFinder}, if modules are to be scanned. */ + private final ModuleFinder moduleFinder; + + /** + * The default order in which ClassLoaders are called to load classes, respecting parent-first/parent-last + * delegation order. + */ + private ClassLoader[] classLoaderOrderRespectingParentDelegation; + + /** + * If one of the classloaders that was found was an existing instance of {@link ClassGraphClassLoader}, then + * delegate to that classloader first rather than trying to load from the {@link ClassGraphClassLoader} of the + * current scan, so that classes are compatible between nested scans (#485). + */ + private ClassGraphClassLoader delegateClassGraphClassLoader; // ------------------------------------------------------------------------------------------------------------- /** - * Add a ClassLoaderHandler, and recurse to parent classloader. + * Get the classpath order. * - * @param scanSpec - * the scan spec - * @param classLoader - * the classloader - * @param classLoaderHandlerRegistryEntry - * the classloader handler registry entry - * @param foundClassLoaders - * the found classloaders - * @param allClassLoaderHandlerRegistryEntries - * the all classloader handler registry entries - * @param classLoaderAndHandlerOrderOut - * the classloader and handler order - * @param ignoredClassLoaderAndHandlerOrderOut - * the ignored classloader and handler order - * @param visited - * visited - * @param log - * the log - * @return true, if successful + * @return The order of raw classpath elements obtained from ClassLoaders. */ - private boolean addClassLoaderHandler(final ScanSpec scanSpec, final ClassLoader classLoader, - final ClassLoaderHandlerRegistryEntry classLoaderHandlerRegistryEntry, - final Set foundClassLoaders, - final List allClassLoaderHandlerRegistryEntries, - final List> classLoaderAndHandlerOrderOut, - final List> ignoredClassLoaderAndHandlerOrderOut, - final Set visited, final LogNode log) { - // Instantiate a ClassLoaderHandler for each ClassLoader, in case the ClassLoaderHandler has state - final ClassLoaderHandler classLoaderHandler = classLoaderHandlerRegistryEntry.instantiate(log); - if (classLoaderHandler != null) { - if (log != null) { - log.log("ClassLoader " + classLoader.getClass().getName() + " will be handled by " - + classLoaderHandler); - } - final ClassLoader embeddedClassLoader = classLoaderHandler.getEmbeddedClassLoader(classLoader); - if (embeddedClassLoader != null) { - if (visited.add(embeddedClassLoader)) { - if (log != null) { - log.log("Delegating from " + classLoader.getClass().getName() + " to embedded ClassLoader " - + embeddedClassLoader.getClass().getName()); - } - return addClassLoaderHandler(scanSpec, embeddedClassLoader, classLoaderHandlerRegistryEntry, - foundClassLoaders, allClassLoaderHandlerRegistryEntries, classLoaderAndHandlerOrderOut, - ignoredClassLoaderAndHandlerOrderOut, visited, log); - } else { - if (log != null) { - log.log("Hit infinite loop when delegating from " + classLoader.getClass().getName() - + " to embedded ClassLoader " + embeddedClassLoader.getClass().getName()); - } - return false; - } - } else { - final DelegationOrder delegationOrder = classLoaderHandler.getDelegationOrder(classLoader); - final ClassLoader parent = classLoader.getParent(); - if (log != null && parent != null) { - log.log(classLoader.getClass().getName() + " delegates to parent " + parent.getClass().getName() - + " with order " + delegationOrder); - } - switch (delegationOrder) { - case PARENT_FIRST: - // Recurse to parent first, then add this ClassLoader to order - if (parent != null) { - findClassLoaderHandlerForClassLoaderAndParents(scanSpec, parent, foundClassLoaders, - allClassLoaderHandlerRegistryEntries, - scanSpec.ignoreParentClassLoaders ? ignoredClassLoaderAndHandlerOrderOut - : classLoaderAndHandlerOrderOut, - ignoredClassLoaderAndHandlerOrderOut, log); - } - classLoaderAndHandlerOrderOut.add(new SimpleEntry<>(classLoader, classLoaderHandler)); - return true; - case PARENT_LAST: - // Add this ClassLoader to order, then recurse to parent - classLoaderAndHandlerOrderOut.add(new SimpleEntry<>(classLoader, classLoaderHandler)); - if (parent != null) { - findClassLoaderHandlerForClassLoaderAndParents(scanSpec, parent, foundClassLoaders, - allClassLoaderHandlerRegistryEntries, - scanSpec.ignoreParentClassLoaders ? ignoredClassLoaderAndHandlerOrderOut - : classLoaderAndHandlerOrderOut, - ignoredClassLoaderAndHandlerOrderOut, log); - } - return true; - default: - throw ClassGraphException.newClassGraphException("Unknown delegation order"); - } - } - } - return false; + public ClasspathOrder getClasspathOrder() { + return classpathOrder; } /** - * Recursively find the ClassLoaderHandler that can handle each ClassLoader and its parent(s), correctly - * observing parent delegation order (PARENT_FIRST or PARENT_LAST). + * Get the {@link ModuleFinder}. * - * @param scanSpec - * the scan spec - * @param classLoader - * the classloader - * @param foundClassLoaders - * the found classloaders - * @param allClassLoaderHandlerRegistryEntries - * the all classloader handler registry entries - * @param classLoaderAndHandlerOrderOut - * the classloader and handler order out - * @param ignoredClassLoaderAndHandlerOrderOut - * the ignored classloader and handler order out - * @param log - * the log + * @return The {@link ModuleFinder}. */ - private void findClassLoaderHandlerForClassLoaderAndParents(final ScanSpec scanSpec, - final ClassLoader classLoader, final Set foundClassLoaders, - final List allClassLoaderHandlerRegistryEntries, - final List> classLoaderAndHandlerOrderOut, - final List> ignoredClassLoaderAndHandlerOrderOut, - final LogNode log) { - // Don't handle ClassLoaders twice (so that any shared parent ClassLoaders get handled only once) - if (foundClassLoaders.add(classLoader)) { - boolean foundMatch = false; - // Iterate through each ClassLoader superclass name - for (Class c = classLoader.getClass(); c != null; c = c.getSuperclass()) { - // Compare against the class names handled by each ClassLoaderHandler - for (final ClassLoaderHandlerRegistryEntry classLoaderHandlerRegistryEntry : // - allClassLoaderHandlerRegistryEntries) { - for (final String handledClassLoaderName : // - classLoaderHandlerRegistryEntry.handledClassLoaderNames) { - if (handledClassLoaderName.equals(c.getName())) { - // This ClassLoaderHandler can handle this class -- instantiate it - if (addClassLoaderHandler(scanSpec, classLoader, classLoaderHandlerRegistryEntry, - foundClassLoaders, allClassLoaderHandlerRegistryEntries, - classLoaderAndHandlerOrderOut, ignoredClassLoaderAndHandlerOrderOut, - new HashSet(), log)) { - foundMatch = true; - } - break; - } - } - if (foundMatch) { - break; - } - } - if (foundMatch) { - break; - } - } - if (!foundMatch) { - if (log != null) { - log.log("Could not find a ClassLoaderHandler that can handle " - + classLoader.getClass().getName() + " , trying " - + ClassLoaderHandlerRegistry.FALLBACK_CLASS_LOADER_HANDLER.classLoaderHandlerClass - .getName() - + " instead. Please report this at: " - + "https://github.com/classgraph/classgraph/issues"); - } - addClassLoaderHandler(scanSpec, classLoader, - ClassLoaderHandlerRegistry.FALLBACK_CLASS_LOADER_HANDLER, foundClassLoaders, - allClassLoaderHandlerRegistryEntries, classLoaderAndHandlerOrderOut, - ignoredClassLoaderAndHandlerOrderOut, new HashSet(), log); - } - } + public ModuleFinder getModuleFinder() { + return moduleFinder; + } + + /** + * Get the ClassLoader order, respecting parent-first/parent-last delegation order. + * + * @return the class loader order. + */ + public ClassLoader[] getClassLoaderOrderRespectingParentDelegation() { + return classLoaderOrderRespectingParentDelegation; + } + + /** + * If one of the classloaders that was found was an existing instance of {@link ClassGraphClassLoader}, then + * delegate to that classloader first rather than trying to load from the {@link ClassGraphClassLoader} of the + * current scan, so that classes are compatible between nested scans (#485). + * + * @return the {@link ClassGraphClassLoader} to delegate to before loading classes with this scan's own + * {@link ClassGraphClassLoader} (or null if none). + */ + public ClassGraphClassLoader getDelegateClassGraphClassLoader() { + return delegateClassGraphClassLoader; } // ------------------------------------------------------------------------------------------------------------- @@ -230,40 +115,70 @@ private void findClassLoaderHandlerForClassLoaderAndParents(final ScanSpec scanS * @param log * The log. */ - public ClasspathFinder(final ScanSpec scanSpec, final LogNode log) { + public ClasspathFinder(final ScanSpec scanSpec, final ReflectionUtils reflectionUtils, final LogNode log) { final LogNode classpathFinderLog = log == null ? null : log.log("Finding classpath and modules"); - // If system jars are not blacklisted, add JRE rt.jar to the beginning of the classpath - final String jreRtJar = SystemJarFinder.getJreRtJarPath(); - final boolean scanAllLibOrExtJars = !scanSpec.libOrExtJarWhiteBlackList.whitelistAndBlacklistAreEmpty(); - final Set libOrExtJars = SystemJarFinder.getJreLibOrExtJars(); - if (classpathFinderLog != null && (jreRtJar != null || !libOrExtJars.isEmpty())) { - final LogNode systemJarsLog = classpathFinderLog.log("System jars:"); - if (jreRtJar != null) { - systemJarsLog.log( - (scanSpec.enableSystemJarsAndModules ? "" : "Scanning disabled for rt.jar: ") + jreRtJar); - } - // If the lib/ext jar whitelist is non-empty, then zero or more lib/ext jars were whitelisted - // (calling ClassGraph#whitelistLibOrExtJars() with no parameters manually whitelists all - // jars found in lib/ext dirs, by iterating through all jarfiles in lib/ext dirs and adding - // them to the whitelist). - for (final String libOrExtJarPath : libOrExtJars) { - systemJarsLog.log((scanAllLibOrExtJars || scanSpec.libOrExtJarWhiteBlackList - .isSpecificallyWhitelistedAndNotBlacklisted(libOrExtJarPath) ? "" - : "Scanning disabled for lib or ext jar: ") - + libOrExtJarPath); + // Require scanning traditional classpath if an override classloader is AppClassLoader (#639) + boolean forceScanJavaClassPath = false; + + // If classloaders are overridden, check if the override classloader(s) is/are JPMS classloaders. + // If so, need to enable non-system module scanning. + boolean scanNonSystemModules; + if (scanSpec.overrideClasspath != null) { + // Don't scan non-system modules if classpath is overridden + scanNonSystemModules = false; + } else if (scanSpec.overrideClassLoaders != null) { + // If classloaders are overridden, only scan non-system modules if an override classloader is a JPMS + // AppClassLoader or PlatformClassLoader + scanNonSystemModules = false; + for (final ClassLoader classLoader : scanSpec.overrideClassLoaders) { + final String classLoaderClassName = classLoader.getClass().getName(); + // It's not possible to instantiate AppClassLoader or PlatformClassLoader, so if these are + // passed in as override classloaders, they must have been obtained using + // Thread.currentThread().getContextClassLoader() [.getParent()] or similar + if (!scanSpec.enableSystemJarsAndModules + && classLoaderClassName.equals("jdk.internal.loader.ClassLoaders$PlatformClassLoader")) { + if (classpathFinderLog != null) { + classpathFinderLog + .log("overrideClassLoaders() was called with an instance of " + classLoaderClassName + + ", so enableSystemJarsAndModules() was called automatically"); + } + scanSpec.enableSystemJarsAndModules = true; + } + if (classLoaderClassName.equals("jdk.internal.loader.ClassLoaders$AppClassLoader") + || classLoaderClassName.equals("jdk.internal.loader.ClassLoaders$PlatformClassLoader")) { + if (classpathFinderLog != null) { + classpathFinderLog + .log("overrideClassLoaders() was called with an instance of " + classLoaderClassName + + ", so the `java.class.path` classpath will also be scanned"); + } + forceScanJavaClassPath = true; + } } + } else { + // If classloaders are not overridden and classpath is not overridden, only scan non-system modules + // if module scanning is enabled + scanNonSystemModules = scanSpec.scanModules; } - classLoaderAndModuleFinder = new ClassLoaderAndModuleFinder(scanSpec, classpathFinderLog); + // Only instantiate a module finder if requested + moduleFinder = scanNonSystemModules || scanSpec.enableSystemJarsAndModules + ? new ModuleFinder(new CallStackReader(reflectionUtils).getClassContext(classpathFinderLog), + scanSpec, scanNonSystemModules, + /* scanSystemModules = */ scanSpec.enableSystemJarsAndModules, reflectionUtils, + classpathFinderLog) + : null; - classpathOrder = new ClasspathOrder(scanSpec); - final ClasspathOrder ignoredClasspathOrder = new ClasspathOrder(scanSpec); + classpathOrder = new ClasspathOrder(scanSpec, reflectionUtils); - final ClassLoader[] contextClassLoaders = classLoaderAndModuleFinder.getContextClassLoaders(); - final ClassLoader defaultClassLoader = contextClassLoaders != null && contextClassLoaders.length > 0 - ? contextClassLoaders[0] - : null; + // Only look for environment classloaders if classpath and classloaders are not overridden + final ClassLoaderFinder classLoaderFinder = scanSpec.overrideClasspath == null + && scanSpec.overrideClassLoaders == null + ? new ClassLoaderFinder(scanSpec, reflectionUtils, classpathFinderLog) + : null; + final ClassLoader[] contextClassLoaders = classLoaderFinder == null ? new ClassLoader[0] + : classLoaderFinder.getContextClassLoaders(); + final ClassLoader defaultClassLoader = contextClassLoaders.length > 0 ? contextClassLoaders[0] : null; if (scanSpec.overrideClasspath != null) { // Manual classpath override if (scanSpec.overrideClassLoaders != null && classpathFinderLog != null) { @@ -272,24 +187,52 @@ public ClasspathFinder(final ScanSpec scanSpec, final LogNode log) { } final LogNode overrideLog = classpathFinderLog == null ? null : classpathFinderLog.log("Overriding classpath with: " + scanSpec.overrideClasspath); - classpathOrder.addClasspathEntries(scanSpec.overrideClasspath, defaultClassLoader, overrideLog); + classpathOrder.addClasspathEntries(scanSpec.overrideClasspath, + // If the classpath is overridden, the classloader used to load classes is overridden in + // ClassGraphClassLoader by a custom URLClassLoader that loads from the override classpath. + // Just use defaultClassLoader as a placeholder here. + defaultClassLoader, scanSpec, overrideLog); if (overrideLog != null) { overrideLog.log("WARNING: when the classpath is overridden, there is no guarantee that the classes " + "found by classpath scanning will be the same as the classes loaded by the " + "context classloader"); } - } else { + classLoaderOrderRespectingParentDelegation = contextClassLoaders; + } + + // If system jars and modules are enabled, add JRE rt.jar to the beginning of the classpath + if (scanSpec.enableSystemJarsAndModules) { + final String jreRtJar = SystemJarFinder.getJreRtJarPath(); + // Add rt.jar and/or lib/ext jars to beginning of classpath, if enabled - if (jreRtJar != null && scanSpec.enableSystemJarsAndModules) { - classpathOrder.addSystemClasspathEntry(jreRtJar, defaultClassLoader); + final LogNode systemJarsLog = classpathFinderLog == null ? null + : classpathFinderLog.log("System jars:"); + if (jreRtJar != null) { + if (scanSpec.enableSystemJarsAndModules) { + classpathOrder.addSystemClasspathEntry(jreRtJar, defaultClassLoader); + if (systemJarsLog != null) { + systemJarsLog.log("Found rt.jar: " + jreRtJar); + } + } else if (systemJarsLog != null) { + systemJarsLog.log((scanSpec.enableSystemJarsAndModules ? "" : "Scanning disabled for rt.jar: ") + + jreRtJar); + } } + final boolean scanAllLibOrExtJars = !scanSpec.libOrExtJarAcceptReject.acceptAndRejectAreEmpty(); for (final String libOrExtJarPath : SystemJarFinder.getJreLibOrExtJars()) { - if (scanAllLibOrExtJars || scanSpec.libOrExtJarWhiteBlackList - .isSpecificallyWhitelistedAndNotBlacklisted(libOrExtJarPath)) { + if (scanAllLibOrExtJars + || scanSpec.libOrExtJarAcceptReject.isSpecificallyAcceptedAndNotRejected(libOrExtJarPath)) { classpathOrder.addSystemClasspathEntry(libOrExtJarPath, defaultClassLoader); + if (systemJarsLog != null) { + systemJarsLog.log("Found lib or ext jar: " + libOrExtJarPath); + } + } else if (systemJarsLog != null) { + systemJarsLog.log("Scanning disabled for lib or ext jar: " + libOrExtJarPath); } } + } + if (scanSpec.overrideClasspath == null) { // List ClassLoaderHandlers if (classpathFinderLog != null) { final LogNode classLoaderHandlerLog = classpathFinderLog.log("ClassLoaderHandlers:"); @@ -299,101 +242,77 @@ public ClasspathFinder(final ScanSpec scanSpec, final LogNode log) { } } - // Find all unique parent ClassLoaders, and put all ClassLoaders into a single order, according to the - // delegation order (PARENT_FIRST or PARENT_LAST) - final List> classLoaderAndHandlerOrder = new ArrayList<>(); - final List> ignoredClassLoaderAndHandlerOrder = // - new ArrayList<>(); - if (contextClassLoaders != null) { - for (final ClassLoader envClassLoader : contextClassLoaders) { - findClassLoaderHandlerForClassLoaderAndParents(scanSpec, envClassLoader, - /* foundClassLoaders = */ new LinkedHashSet(), - ClassLoaderHandlerRegistry.CLASS_LOADER_HANDLERS, classLoaderAndHandlerOrder, - ignoredClassLoaderAndHandlerOrder, classpathFinderLog); + // Find all unique classloaders, in delegation order + final LogNode classloaderOrderLog = classpathFinderLog == null ? null + : classpathFinderLog.log("Finding unique classloaders in delegation order"); + final ClassLoaderOrder classLoaderOrder = new ClassLoaderOrder(reflectionUtils); + final ClassLoader[] origClassLoaderOrder = scanSpec.overrideClassLoaders != null + ? scanSpec.overrideClassLoaders.toArray(new ClassLoader[0]) + : contextClassLoaders; + if (origClassLoaderOrder != null) { + for (final ClassLoader classLoader : origClassLoaderOrder) { + classLoaderOrder.delegateTo(classLoader, /* isParent = */ false, classloaderOrderLog); } } - // Call each ClassLoaderHandler on its corresponding ClassLoader to get the classpath URLs or paths - final LogNode classLoaderClasspathLoopLog = classpathFinderLog == null ? null - : classpathFinderLog.log("Finding classpath elements in ClassLoaders"); - for (final SimpleEntry classLoaderAndHandler : // - classLoaderAndHandlerOrder) { - final ClassLoader classLoader = classLoaderAndHandler.getKey(); - final ClassLoaderHandler classLoaderHandler = classLoaderAndHandler.getValue(); - final LogNode classLoaderClasspathLog = classLoaderClasspathLoopLog == null ? null - : classLoaderClasspathLoopLog.log( - "Finding classpath elements in ClassLoader " + classLoader.getClass().getName()); - try { - classLoaderHandler.handle(scanSpec, classLoader, classpathOrder, classLoaderClasspathLog); - } catch (final RuntimeException | LinkageError e) { - if (classLoaderClasspathLog != null) { - classLoaderClasspathLog.log("Exception in ClassLoaderHandler", e); + // Get all parent classloaders + final Set allParentClassLoaders = classLoaderOrder.getAllParentClassLoaders(); + + // Get the classpath URLs from each ClassLoader + final LogNode classloaderURLLog = classpathFinderLog == null ? null + : classpathFinderLog.log("Obtaining URLs from classloaders in delegation order"); + final List finalClassLoaderOrder = new ArrayList<>(); + for (final Entry> ent : classLoaderOrder + .getClassLoaderOrder()) { + final ClassLoader classLoader = ent.getKey(); + for (final ClassLoaderHandlerRegistryEntry classLoaderHandlerRegistryEntry : ent.getValue()) { + // Add classpath entries to ignoredClasspathOrder or classpathOrder + if (!scanSpec.ignoreParentClassLoaders || !allParentClassLoaders.contains(classLoader)) { + // Otherwise add classpath entries to classpathOrder, and add the classloader to the + // final classloader ordering + final LogNode classloaderHandlerLog = classloaderURLLog == null ? null + : classloaderURLLog.log("Classloader " + classLoader.getClass().getName() + + " is handled by " + + classLoaderHandlerRegistryEntry.classLoaderHandlerClass.getName()); + classLoaderHandlerRegistryEntry.findClasspathOrder(classLoader, classpathOrder, scanSpec, + classloaderHandlerLog); + finalClassLoaderOrder.add(classLoader); + } else if (classloaderURLLog != null) { + classloaderURLLog + .log("Ignoring parent classloader " + classLoader + ", normally handled by " + + classLoaderHandlerRegistryEntry.classLoaderHandlerClass.getName()); } - } - } - // Repeat the process for ignored parent ClassLoaders - for (final SimpleEntry classLoaderAndHandler : // - ignoredClassLoaderAndHandlerOrder) { - final ClassLoader classLoader = classLoaderAndHandler.getKey(); - final ClassLoaderHandler classLoaderHandler = classLoaderAndHandler.getValue(); - final LogNode classLoaderClasspathLog = classpathFinderLog == null ? null - : classpathFinderLog - .log("Will not scan the following classpath elements from ignored ClassLoader " - + classLoader.getClass().getName()); - try { - classLoaderHandler.handle(scanSpec, classLoader, ignoredClasspathOrder, - classLoaderClasspathLog); - } catch (final RuntimeException | LinkageError e) { - if (classLoaderClasspathLog != null) { - classLoaderClasspathLog.log("Exception in ClassLoaderHandler", e); + // See if a previous scan's ClassGraphClassLoader should be delegated to first + if (classLoader instanceof ClassGraphClassLoader) { + delegateClassGraphClassLoader = (ClassGraphClassLoader) classLoader; } } } - // Get classpath elements from java.class.path, but don't add them if the element is in an ignored - // parent classloader and not in a child classloader (and don't use java.class.path at all if - // overrideClassLoaders is true or overrideClasspath is set) - if (scanSpec.overrideClassLoaders == null && scanSpec.overrideClasspath == null) { - final String[] pathElements = JarUtils.smartPathSplit(System.getProperty("java.class.path")); - if (pathElements.length > 0) { - final LogNode sysPropLog = classpathFinderLog == null ? null - : classpathFinderLog.log("Getting classpath entries from java.class.path"); - for (final String pathElement : pathElements) { - final String pathElementResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, - pathElement); - if (!ignoredClasspathOrder.getClasspathEntryUniqueResolvedPaths() - .contains(pathElementResolved)) { - // pathElement is not also listed in an ignored parent classloader - classpathOrder.addClasspathEntry(pathElement, defaultClassLoader, sysPropLog); - } else { - // pathElement is also listed in an ignored parent classloader, ignore it (Issue #169) - if (sysPropLog != null) { - sysPropLog.log("Found classpath element in java.class.path that will be ignored, " - + "since it is also found in an ignored parent classloader: " - + pathElement); - } - } - } + // Need to record the classloader delegation order, in particular to respect parent-last delegation + // order, since this is not the default (issue #267). + classLoaderOrderRespectingParentDelegation = finalClassLoaderOrder.toArray(new ClassLoader[0]); + } + + // Only scan java.class.path if parent classloaders are not ignored, classloaders are not overridden, + // and the classpath is not overridden, unless only module scanning was enabled, and an unnamed module + // layer was encountered -- in this case, have to forcibly scan java.class.path, since the ModuleLayer + // API doesn't allow for the opening of unnamed modules. + if (forceScanJavaClassPath + || (!scanSpec.ignoreParentClassLoaders && scanSpec.overrideClassLoaders == null + && scanSpec.overrideClasspath == null) + || (moduleFinder != null && moduleFinder.forceScanJavaClassPath())) { + final String[] pathElements = JarUtils.smartPathSplit(System.getProperty("java.class.path"), scanSpec); + if (pathElements.length > 0) { + final LogNode sysPropLog = classpathFinderLog == null ? null + : classpathFinderLog.log("Getting classpath entries from java.class.path"); + for (final String pathElement : pathElements) { + // pathElement is not also listed in an ignored parent classloader + final String pathElementResolved = FastPathResolver.resolve(FileUtils.currDirPath(), + pathElement); + classpathOrder.addClasspathEntry(pathElementResolved, defaultClassLoader, scanSpec, sysPropLog); } } } } - - /** - * Get the classpath order. - * - * @return The order of raw classpath elements obtained from ClassLoaders. - */ - public ClasspathOrder getClasspathOrder() { - return classpathOrder; - } - - /** - * Get the classloader and module finder. - * - * @return The {@link ClassLoaderAndModuleFinder}. - */ - public ClassLoaderAndModuleFinder getClassLoaderAndModuleFinder() { - return classLoaderAndModuleFinder; - } } diff --git a/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathOrder.java b/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathOrder.java index e5a131b46..7b7c354e5 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathOrder.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathOrder.java @@ -29,16 +29,28 @@ package nonapi.io.github.classgraph.classpath; import java.io.File; +import java.io.IOError; import java.lang.reflect.Array; -import java.util.AbstractMap.SimpleEntry; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import io.github.classgraph.ClassGraph.ClasspathElementFilter; -import nonapi.io.github.classgraph.ScanSpec; +import io.github.classgraph.ClassGraph.ClasspathElementURLFilter; +import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandlerRegistry; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.FastPathResolver; import nonapi.io.github.classgraph.utils.FileUtils; import nonapi.io.github.classgraph.utils.JarUtils; @@ -46,15 +58,73 @@ /** A class to find the unique ordered classpath elements. */ public class ClasspathOrder { - /** The scan spec. */ private final ScanSpec scanSpec; + public ReflectionUtils reflectionUtils; + /** Unique classpath entries. */ private final Set classpathEntryUniqueResolvedPaths = new HashSet<>(); - /** The classpath order. */ - private final List> order = new ArrayList<>(); + /** The classpath order. Keys are instances of {@link String} or {@link URL}. */ + private final List order = new ArrayList<>(); + + /** Suffixes for automatic package roots, e.g. "!/BOOT-INF/classes". */ + private static final List AUTOMATIC_PACKAGE_ROOT_SUFFIXES = new ArrayList<>(); + + /** Match URL schemes (must consist of at least two chars, otherwise this is Windows drive letter). */ + private static final Pattern schemeMatcher = Pattern.compile("^[a-zA-Z][a-zA-Z+\\-.]+:"); + + static { + for (final String prefix : ClassLoaderHandlerRegistry.AUTOMATIC_PACKAGE_ROOT_PREFIXES) { + AUTOMATIC_PACKAGE_ROOT_SUFFIXES.add("!/" + prefix.substring(0, prefix.length() - 1)); + } + } + + /** + * A classpath element and the {@link ClassLoader} it was obtained from. + */ + public static class ClasspathEntry { + /** The classpath entry object (a {@link String} path, {@link Path}, {@link URL} or {@link URI}). */ + public final Object classpathEntryObj; + + /** The classloader the classpath element was obtained from. */ + public final ClassLoader classLoader; + + /** + * Constructor. + * + * @param classpathEntryObj + * the classpath entry object (a {@link String} or {@link URL} or {@link Path}). + * @param classLoader + * the classloader the classpath element was obtained from. + */ + public ClasspathEntry(final Object classpathEntryObj, final ClassLoader classLoader) { + this.classpathEntryObj = classpathEntryObj; + this.classLoader = classLoader; + } + + @Override + public int hashCode() { + return Objects.hash(classpathEntryObj); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof ClasspathEntry)) { + return false; + } + final ClasspathEntry other = (ClasspathEntry) obj; + return Objects.equals(this.classpathEntryObj, other.classpathEntryObj); + } + + @Override + public String toString() { + return classpathEntryObj + " [" + classLoader + "]"; + } + } /** * Constructor. @@ -62,16 +132,17 @@ public class ClasspathOrder { * @param scanSpec * the scan spec */ - ClasspathOrder(final ScanSpec scanSpec) { + ClasspathOrder(final ScanSpec scanSpec, final ReflectionUtils reflectionUtils) { this.scanSpec = scanSpec; + this.reflectionUtils = reflectionUtils; } /** - * Get the order of classpath elements, as an ordered set. + * Get the order of classpath elements, uniquified and in order. * - * @return the classpath order, as (path/URL, ClassLoader) tuples. + * @return the classpath order. */ - public List> getOrder() { + public List getOrder() { return order; } @@ -85,16 +156,22 @@ public Set getClasspathEntryUniqueResolvedPaths() { } /** - * Test to see if a RelativePath has been filtered out by the user. - * + * Test to see if a classpath element has been filtered out by the user. + * + * @param classpathElementURL + * the classpath element URL * @param classpathElementPath * the classpath element path * @return true, if not filtered out */ - private boolean filter(final String classpathElementPath) { + private boolean filter(final URL classpathElementURL, final String classpathElementPath) { if (scanSpec.classpathElementFilters != null) { - for (final ClasspathElementFilter filter : scanSpec.classpathElementFilters) { - if (!filter.includeClasspathElement(classpathElementPath)) { + for (final Object filterObj : scanSpec.classpathElementFilters) { + if ((classpathElementURL != null && filterObj instanceof ClasspathElementURLFilter + && !((ClasspathElementURLFilter) filterObj).includeClasspathElement(classpathElementURL)) + || (classpathElementPath != null && filterObj instanceof ClasspathElementFilter + && !((ClasspathElementFilter) filterObj) + .includeClasspathElement(classpathElementPath))) { return false; } } @@ -107,14 +184,14 @@ private boolean filter(final String classpathElementPath) { * * @param pathEntry * the system classpath entry -- the path string should already have been run through - * FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, path + * FastPathResolver.resolve(FileUtils.currDirPath(), path) * @param classLoader * the classloader * @return true, if added and unique */ boolean addSystemClasspathEntry(final String pathEntry, final ClassLoader classLoader) { if (classpathEntryUniqueResolvedPaths.add(pathEntry)) { - order.add(new SimpleEntry<>(pathEntry, classLoader)); + order.add(new ClasspathEntry(pathEntry, classLoader)); return true; } return false; @@ -123,23 +200,73 @@ boolean addSystemClasspathEntry(final String pathEntry, final ClassLoader classL /** * Add a classpath entry. * - * @param pathEntry - * the classpath entry -- the path string should already have been run through - * FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, path) + * @param pathElement + * the {@link String} path, {@link File}, {@link Path}, {@link URL} or {@link URI} of the classpath + * element. + * @param pathElementStr + * the path element in string format * @param classLoader * the classloader + * @param scanSpec + * the scan spec * @return true, if added and unique */ - private boolean addClasspathEntry(final String pathEntry, final ClassLoader classLoader) { - if (SystemJarFinder.getJreLibOrExtJars().contains(pathEntry) - || pathEntry.equals(SystemJarFinder.getJreRtJarPath())) { - // JRE lib and ext jars are handled separately, so reject them as duplicates if they are - // returned by a system classloader - return false; + private boolean addClasspathEntry(final Object pathElement, final String pathElementStr, + final ClassLoader classLoader, final ScanSpec scanSpec) { + // Check if classpath element path ends with an automatic package root. If so, strip it off to + // eliminate duplication, since automatic package roots are detected automatically (#435) + String pathElementStrWithoutSuffix = pathElementStr; + boolean hasSuffix = false; + for (final String suffix : AUTOMATIC_PACKAGE_ROOT_SUFFIXES) { + if (pathElementStr.endsWith(suffix)) { + // Strip off automatic package root suffix + pathElementStrWithoutSuffix = pathElementStr.substring(0, + pathElementStr.length() - suffix.length()); + hasSuffix = true; + break; + } } - if (classpathEntryUniqueResolvedPaths.add(pathEntry)) { - order.add(new SimpleEntry<>(pathEntry, classLoader)); - return true; + if (pathElement instanceof URL || pathElement instanceof URI || pathElement instanceof Path + || pathElement instanceof File) { + Object pathElementWithoutSuffix = pathElement; + if (hasSuffix) { + try { + pathElementWithoutSuffix = pathElement instanceof URL ? new URL(pathElementStrWithoutSuffix) + : pathElement instanceof URI ? new URI(pathElementStrWithoutSuffix) + : pathElement instanceof Path ? Paths.get(pathElementStrWithoutSuffix) + // For File, just use path string + : pathElementStrWithoutSuffix; + } catch (MalformedURLException | URISyntaxException | InvalidPathException e) { + try { + pathElementWithoutSuffix = pathElement instanceof URL + ? new URL("file:" + pathElementStrWithoutSuffix) + : pathElement instanceof URI ? new URI("file:" + pathElementStrWithoutSuffix) + : pathElementStrWithoutSuffix; + } catch (MalformedURLException | URISyntaxException | InvalidPathException e2) { + return false; + } + } + } + // Deduplicate classpath elements + if (classpathEntryUniqueResolvedPaths.add(pathElementStrWithoutSuffix)) { + // Record classpath element in classpath order + order.add(new ClasspathEntry(pathElementWithoutSuffix, classLoader)); + return true; + } + } else { + final String pathElementStrResolved = FastPathResolver.resolve(FileUtils.currDirPath(), + pathElementStrWithoutSuffix); + if (scanSpec.overrideClasspath == null // + && (SystemJarFinder.getJreLibOrExtJars().contains(pathElementStrResolved) + || pathElementStrResolved.equals(SystemJarFinder.getJreRtJarPath()))) { + // JRE lib and ext jars are handled separately, so reject them as duplicates if they are + // returned by a system classloader + return false; + } + if (classpathEntryUniqueResolvedPaths.add(pathElementStrResolved)) { + order.add(new ClasspathEntry(pathElementStrResolved, classLoader)); + return true; + } } return false; } @@ -149,123 +276,268 @@ private boolean addClasspathEntry(final String pathEntry, final ClassLoader clas * elements that it knows about. ClassLoaders will be called in order. * * @param pathElement - * the URL or path of the classpath element. + * the {@link String} path, {@link URL} or {@link URI} of the classpath element, or some object whose + * {@link Object#toString()} method can be called to obtain the classpath element. * @param classLoader * the ClassLoader that this classpath element was obtained from. + * @param scanSpec + * the scan spec * @param log * the LogNode instance to use if logging in verbose mode. * @return true (and add the classpath element) if pathElement is not null, empty, nonexistent, or filtered out * by user-specified criteria, otherwise return false. */ - public boolean addClasspathEntry(final String pathElement, final ClassLoader classLoader, final LogNode log) { - if (pathElement == null || pathElement.isEmpty()) { + public boolean addClasspathEntry(final Object pathElement, final ClassLoader classLoader, + final ScanSpec scanSpec, final LogNode log) { + if (pathElement == null) { return false; } - // Check for wildcard path element (allowable for local classpaths as of JDK 6) - if (pathElement.endsWith("*")) { - if (pathElement.length() == 1 || // - (pathElement.length() > 2 && pathElement.charAt(pathElement.length() - 1) == '*' - && (pathElement.charAt(pathElement.length() - 2) == File.separatorChar - || (File.separatorChar != '/' - && pathElement.charAt(pathElement.length() - 2) == '/')))) { - // Apply classpath element filters, if any - final String baseDirPath = pathElement.length() == 1 ? "" - : pathElement.substring(0, pathElement.length() - 2); - final String baseDirPathResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, baseDirPath); - if (!filter(baseDirPath) - || (!baseDirPathResolved.equals(baseDirPath) && !filter(baseDirPathResolved))) { - if (log != null) { - log.log("Classpath element did not match filter criterion, skipping: " + pathElement); - } - return false; + String pathElementStr; + if (pathElement instanceof Path) { + try { + // Path objects have to be converted to URIs before calling .toString(), otherwise scheme is dropped + pathElementStr = ((Path) pathElement).toUri().toString(); + // Windows paths ("C:\x\y") are encoded as "file:///C:/x/y" by Path.toUri().toString(), + // but then Paths.get() can't handle paths of the form "///C:/x/y" + if (pathElementStr.startsWith("file:///")) { + pathElementStr = ((Path) pathElement).toFile().toString(); } - - // Check the path before the "/*" suffix is a directory - final File baseDir = new File(baseDirPathResolved); - if (!baseDir.exists()) { - if (log != null) { - log.log("Directory does not exist for wildcard classpath element: " + pathElement); - } - return false; + } catch (IOError | SecurityException e) { + pathElementStr = pathElement.toString(); + } + } else { + pathElementStr = pathElement.toString(); + } + pathElementStr = FastPathResolver.resolve(FileUtils.currDirPath(), pathElementStr); + if (pathElementStr.isEmpty()) { + return false; + } + URL pathElementURL = null; + boolean hasWildcardSuffix = false; + // Fallback -- call toString() on the path element, then try converting to a URL + if (pathElementStr.endsWith("/*") || pathElementStr.endsWith("\\*")) { + hasWildcardSuffix = true; + pathElementStr = pathElementStr.substring(0, pathElementStr.length() - 2); + // Leave pathElementURL null, so that wildcards can be handled below + } else if (pathElementStr.equals("*")) { + hasWildcardSuffix = true; + pathElementStr = ""; + // Leave pathElementURL null, so that wildcards can be handled below + } else { + final Matcher m1 = schemeMatcher.matcher(pathElementStr); + if (m1.find()) { + // Path element string is URL with scheme other than `[jar:]file:`, so need to actually + // parse URL, since the scheme may be a custom scheme + try { + pathElementURL = pathElement instanceof URL ? (URL) pathElement + : pathElement instanceof URI ? ((URI) pathElement).toURL() + : pathElement instanceof Path ? ((Path) pathElement).toUri().toURL() + : pathElement instanceof File ? ((File) pathElement).toURI().toURL() + : null; + } catch (final MalformedURLException | IllegalArgumentException | IOError | SecurityException e2) { + // Fall through } - if (!FileUtils.canRead(baseDir)) { - if (log != null) { - log.log("Cannot read directory for wildcard classpath element: " + pathElement); + if (pathElementURL == null) { + // Escape percentage characters in URLs (#255) + final String urlStr = pathElementStr.replace("%", "%25"); + try { + pathElementURL = new URL(urlStr); + } catch (final MalformedURLException e) { + try { + pathElementURL = new File(urlStr).toURI().toURL(); + } catch (final MalformedURLException | IllegalArgumentException | IOError + | SecurityException e1) { + // Final fallback -- try just using the raw string as a URL + try { + pathElementURL = new URL(pathElementStr); + } catch (final MalformedURLException e2) { + // Fall through + } + } } - return false; } - if (!baseDir.isDirectory()) { + if (pathElementURL == null) { if (log != null) { - log.log("Wildcard is appended to something other than a directory: " + pathElement); + log.log("Failed to convert classpath element to URL: " + pathElement); } - return false; } + } + } + if (pathElementURL != null || pathElement instanceof URI || pathElement instanceof File + || pathElement instanceof Path) { + if (!filter(pathElementURL, pathElementStr)) { + if (log != null) { + log.log("Classpath element did not match filter criterion, skipping: " + pathElementStr); + } + return false; + } + // For URL objects, use the object itself (so that URL scheme handling can be undertaken later); + // for URI and Path objects, convert to URL; for File objects, use the toString result (the path) + final Object classpathElementObj; + classpathElementObj = pathElement instanceof File ? pathElementStr + : pathElementURL != null ? pathElementURL : pathElement; + if (addClasspathEntry(classpathElementObj, pathElementStr, classLoader, scanSpec)) { + if (log != null) { + log.log("Found classpath element: " + pathElementStr); + } + return true; + } else { + if (log != null) { + log.log("Ignoring duplicate classpath element: " + pathElementStr); + } + return false; + } + } + if (hasWildcardSuffix) { + // Has wildcard path element (allowable for local classpaths as of JDK 6) + // Apply classpath element filters, if any + final String baseDirPath = pathElementStr; + final String baseDirPathResolved = FastPathResolver.resolve(FileUtils.currDirPath(), baseDirPath); + if (!filter(pathElementURL, baseDirPath) + || (!baseDirPathResolved.equals(baseDirPath) && !filter(pathElementURL, baseDirPathResolved))) { + if (log != null) { + log.log("Classpath element did not match filter criterion, skipping: " + pathElementStr); + } + return false; + } + + // Check the path before the "/*" suffix is a directory + final File baseDir = new File(baseDirPathResolved); + if (!baseDir.exists()) { + if (log != null) { + log.log("Directory does not exist for wildcard classpath element: " + pathElementStr); + } + return false; + } + if (!FileUtils.canRead(baseDir)) { + if (log != null) { + log.log("Cannot read directory for wildcard classpath element: " + pathElementStr); + } + return false; + } + if (!baseDir.isDirectory()) { + if (log != null) { + log.log("Wildcard is appended to something other than a directory: " + pathElementStr); + } + return false; + } - // Add all elements in the requested directory to the classpath - final LogNode dirLog = log == null ? null - : log.log("Adding classpath elements from wildcarded directory: " + pathElement); - final File[] baseDirFiles = baseDir.listFiles(); - if (baseDirFiles != null) { - for (final File fileInDir : baseDirFiles) { - final String name = fileInDir.getName(); - if (!name.equals(".") && !name.equals("..")) { - // Add each directory entry as a classpath element - final String fileInDirPath = fileInDir.getPath(); - final String fileInDirPathResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, - fileInDirPath); - if (addClasspathEntry(fileInDirPathResolved, classLoader)) { - if (dirLog != null) { - dirLog.log("Found classpath element: " + fileInDirPath - + (fileInDirPath.equals(fileInDirPathResolved) ? "" - : " -> " + fileInDirPathResolved)); - } - } else { - if (dirLog != null) { - dirLog.log("Ignoring duplicate classpath element: " + fileInDirPath - + (fileInDirPath.equals(fileInDirPathResolved) ? "" - : " -> " + fileInDirPathResolved)); - } + // Add all elements in the requested directory to the classpath + final LogNode dirLog = log == null ? null + : log.log("Adding classpath elements from wildcarded directory: " + pathElementStr); + final File[] baseDirFiles = baseDir.listFiles(); + if (baseDirFiles != null) { + for (final File fileInDir : baseDirFiles) { + final String name = fileInDir.getName(); + if (!name.equals(".") && !name.equals("..")) { + // Add each directory entry as a classpath element + final String fileInDirPath = fileInDir.getPath(); + final String fileInDirPathResolved = FastPathResolver.resolve(FileUtils.currDirPath(), + fileInDirPath); + if (addClasspathEntry(fileInDirPathResolved, fileInDirPathResolved, classLoader, + scanSpec)) { + if (dirLog != null) { + dirLog.log("Found classpath element: " + fileInDirPath + + (fileInDirPath.equals(fileInDirPathResolved) ? "" + : " -> " + fileInDirPathResolved)); + } + } else { + if (dirLog != null) { + dirLog.log("Ignoring duplicate classpath element: " + fileInDirPath + + (fileInDirPath.equals(fileInDirPathResolved) ? "" + : " -> " + fileInDirPathResolved)); } } } - return true; - } else { - return false; } + return true; } else { - if (log != null) { - log.log("Wildcard classpath elements can only end with a leaf of \"*\", " - + "can't have a partial name and then a wildcard: " + pathElement); - } return false; } } else { // Non-wildcarded (standard) classpath element - final String pathElementResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, pathElement); - if (!filter(pathElement) - || (!pathElementResolved.equals(pathElement) && !filter(pathElementResolved))) { + if (pathElementStr.indexOf('*') >= 0) { if (log != null) { - log.log("Classpath element did not match filter criterion, skipping: " + pathElement - + (pathElement.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); + log.log("Wildcard classpath elements can only end with a suffix of \"/*\", " + + "can't use globs elsewhere in the path: " + pathElementStr); } return false; } - if (addClasspathEntry(pathElementResolved, classLoader)) { + final String pathElementResolved = FastPathResolver.resolve(FileUtils.currDirPath(), pathElementStr); + if (!filter(pathElementURL, pathElementStr) || (!pathElementResolved.equals(pathElementStr) + && !filter(pathElementURL, pathElementResolved))) { if (log != null) { - log.log("Found classpath element: " + pathElement - + (pathElement.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); + log.log("Classpath element did not match filter criterion, skipping: " + pathElementStr + + (pathElementStr.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); + } + return false; + } + if (pathElementResolved.startsWith("//")) { + // Handle Windows UNC paths (#705). + // File supports UNC paths directly: + // https://wiki.eclipse.org/Eclipse/UNC_Paths#Programming_with_UNC_paths + try { + final File file = new File(pathElementResolved); + if (addClasspathEntry(file, pathElementResolved, classLoader, scanSpec)) { + if (log != null) { + log.log("Found classpath element: " + file + + (pathElementStr.equals(pathElementResolved) ? "" + : " -> " + pathElementResolved)); + } + return true; + } else { + if (log != null) { + log.log("Ignoring duplicate classpath element: " + pathElementStr + + (pathElementStr.equals(pathElementResolved) ? "" + : " -> " + pathElementResolved)); + } + return false; + } + } catch (final Exception e) { + // Fall through + } + } + if (addClasspathEntry(pathElementResolved, pathElementResolved, classLoader, scanSpec)) { + if (log != null) { + log.log("Found classpath element: " + pathElementStr + + (pathElementStr.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); } return true; } else { if (log != null) { - log.log("Ignoring duplicate classpath element: " + pathElement - + (pathElement.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); + log.log("Ignoring duplicate classpath element: " + pathElementStr + + (pathElementStr.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); } return false; } } } + /** + * Add classpath entries, separated by the system path separator character. + * + * @param overrideClasspath + * a list of delimited path {@link String}, {@link URL}, {@link URI} or {@link File} objects. + * @param classLoader + * the ClassLoader that this classpath was obtained from. + * @param scanSpec + * the scan spec + * @param log + * the LogNode instance to use if logging in verbose mode. + * @return true (and add the classpath element) if pathElement is not null or empty, otherwise return false. + */ + public boolean addClasspathEntries(final List overrideClasspath, final ClassLoader classLoader, + final ScanSpec scanSpec, final LogNode log) { + if (overrideClasspath == null || overrideClasspath.isEmpty()) { + return false; + } else { + for (final Object pathElement : overrideClasspath) { + addClasspathEntry(pathElement, classLoader, scanSpec, log); + } + return true; + } + } + /** * Add classpath entries, separated by the system path separator character. * @@ -273,20 +545,23 @@ public boolean addClasspathEntry(final String pathElement, final ClassLoader cla * the delimited string of URLs or paths of the classpath. * @param classLoader * the ClassLoader that this classpath was obtained from. + * @param scanSpec + * the scan spec * @param log * the LogNode instance to use if logging in verbose mode. * @return true (and add the classpath element) if pathElement is not null or empty, otherwise return false. */ - public boolean addClasspathEntries(final String pathStr, final ClassLoader classLoader, final LogNode log) { + public boolean addClasspathPathStr(final String pathStr, final ClassLoader classLoader, final ScanSpec scanSpec, + final LogNode log) { if (pathStr == null || pathStr.isEmpty()) { return false; } else { - final String[] parts = JarUtils.smartPathSplit(pathStr); + final String[] parts = JarUtils.smartPathSplit(pathStr, scanSpec); if (parts.length == 0) { return false; } else { for (final String pathElement : parts) { - addClasspathEntry(pathElement, classLoader, log); + addClasspathEntry(pathElement, classLoader, scanSpec, log); } return true; } @@ -294,43 +569,44 @@ public boolean addClasspathEntries(final String pathStr, final ClassLoader class } /** - * Add classpath entries from an object obtained from reflection. The object may be a String (containing a - * single path, or several paths separated with File.pathSeparator), a List or other Iterable, or an array - * object. In the case of Iterables and arrays, the elements may be any type whose {@code toString()} method - * returns a path or URL string (including the {@code URL} and {@code Path} types). + * Add classpath entries from an object obtained from reflection. The object may be a {@link URL}, a + * {@link URI}, a {@link File}, a {@link Path} or a {@link String} (containing a single classpath element path, + * or several paths separated with File.pathSeparator), a List or other Iterable, or an array object. In the + * case of Iterables and arrays, the elements may be any type whose {@code toString()} method returns a path or + * URL string (including the {@code URL} and {@code Path} types). * * @param pathObject * the object containing a classpath string or strings. * @param classLoader * the ClassLoader that this classpath was obtained from. + * @param scanSpec + * the scan spec * @param log * the LogNode instance to use if logging in verbose mode. * @return true (and add the classpath element) if pathEl)ement is not null or empty, otherwise return false. */ public boolean addClasspathEntryObject(final Object pathObject, final ClassLoader classLoader, - final LogNode log) { + final ScanSpec scanSpec, final LogNode log) { boolean valid = false; if (pathObject != null) { - if (pathObject instanceof String) { - valid |= addClasspathEntries((String) pathObject, classLoader, log); + if (pathObject instanceof URL || pathObject instanceof URI || pathObject instanceof Path + || pathObject instanceof File) { + valid |= addClasspathEntry(pathObject, classLoader, scanSpec, log); } else if (pathObject instanceof Iterable) { - for (final Object p : (Iterable) pathObject) { - if (p != null) { - valid |= addClasspathEntries(p.toString(), classLoader, log); - } + for (final Object elt : (Iterable) pathObject) { + valid |= addClasspathEntryObject(elt, classLoader, scanSpec, log); } } else { final Class valClass = pathObject.getClass(); if (valClass.isArray()) { for (int j = 0, n = Array.getLength(pathObject); j < n; j++) { final Object elt = Array.get(pathObject, j); - if (elt != null) { - valid |= addClasspathEntryObject(elt, classLoader, log); - } + valid |= addClasspathEntryObject(elt, classLoader, scanSpec, log); } } else { - // Try simply calling toString() as a final fallback, in case this returns something sensible - valid |= addClasspathEntries(pathObject.toString(), classLoader, log); + // Try simply calling toString() as a final fallback, to handle String objects, or to + // try to handle anything else + valid |= addClasspathPathStr(pathObject.toString(), classLoader, scanSpec, log); } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderAndModuleFinder.java b/src/main/java/nonapi/io/github/classgraph/classpath/ModuleFinder.java similarity index 50% rename from src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderAndModuleFinder.java rename to src/main/java/nonapi/io/github/classgraph/classpath/ModuleFinder.java index e5b8116f8..8edc7599d 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderAndModuleFinder.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/ModuleFinder.java @@ -38,30 +38,25 @@ import java.util.Set; import io.github.classgraph.ModuleRef; -import nonapi.io.github.classgraph.ScanSpec; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.CollectionUtils; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; - -/** A class to find the unique ordered classpath elements. */ -public class ClassLoaderAndModuleFinder { - - /** The context class loaders. */ - private final ClassLoader[] contextClassLoaders; +/** A class to find the visible modules. */ +public class ModuleFinder { /** The system module refs. */ private List systemModuleRefs; /** The non system module refs. */ private List nonSystemModuleRefs; - /** - * Get the context class loaders. - * - * @return The context classloader, and any other classloader that is not an ancestor of context classloader. - */ - public ClassLoader[] getContextClassLoaders() { - return contextClassLoaders; - } + /** If true, must forcibly scan {@code java.class.path}, since there was an anonymous module layer. */ + private boolean forceScanJavaClassPath; + + private final ReflectionUtils reflectionUtils; + + // ------------------------------------------------------------------------------------------------------------- /** * Get the system modules as {@link ModuleRef} wrappers. @@ -83,6 +78,15 @@ public List getNonSystemModuleRefs() { return nonSystemModuleRefs; } + /** + * Force scan java class path. + * + * @return If true, must forcibly scan {@code java.class.path}, since there was an anonymous module layer. + */ + public boolean forceScanJavaClassPath() { + return forceScanJavaClassPath; + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -102,14 +106,14 @@ public List getNonSystemModuleRefs() { * @param layerOrderOut * the layer order */ - private static void findLayerOrder(final Object /* ModuleLayer */ layer, + private void findLayerOrder(final Object /* ModuleLayer */ layer, final Set /* Set */ layerVisited, final Set /* Set */ parentLayers, final Deque /* Deque */ layerOrderOut) { if (layerVisited.add(layer)) { @SuppressWarnings("unchecked") - final List /* List */ parents = (List) ReflectionUtils.invokeMethod(layer, - "parents", /* throwException = */ true); + final List /* List */ parents = (List) reflectionUtils + .invokeMethod(/* throwException = */ true, layer, "parents"); if (parents != null) { parentLayers.addAll(parents); for (final Object parent : parents) { @@ -131,7 +135,7 @@ private static void findLayerOrder(final Object /* ModuleLayer */ layer, * the log * @return the list */ - private static List findModuleRefs(final List layers, final ScanSpec scanSpec, + private List findModuleRefs(final LinkedHashSet layers, final ScanSpec scanSpec, final LogNode log) { if (layers.isEmpty()) { return Collections.emptyList(); @@ -141,11 +145,15 @@ private static List findModuleRefs(final List layers, final S final Deque /* Deque */ layerOrder = new ArrayDeque<>(); final Set /* Set(); for (final Object layer : layers) { - findLayerOrder(layer, /* layerVisited = */ new HashSet<>(), parentLayers, layerOrder); + if (layer != null) { + findLayerOrder(layer, /* layerVisited = */ new HashSet<>(), parentLayers, layerOrder); + } } if (scanSpec.addedModuleLayers != null) { for (final Object layer : scanSpec.addedModuleLayers) { - findLayerOrder(layer, /* layerVisited = */ new HashSet<>(), parentLayers, layerOrder); + if (layer != null) { + findLayerOrder(layer, /* layerVisited = */ new HashSet<>(), parentLayers, layerOrder); + } } } @@ -166,21 +174,21 @@ private static List findModuleRefs(final List layers, final S final Set /* Set */ addedModules = new HashSet<>(); final LinkedHashSet moduleRefOrder = new LinkedHashSet<>(); for (final Object /* ModuleLayer */ layer : layerOrderFinal) { - final Object /* Configuration */ configuration = ReflectionUtils.invokeMethod(layer, "configuration", - /* throwException = */ true); + final Object /* Configuration */ configuration = reflectionUtils + .invokeMethod(/* throwException = */ true, layer, "configuration"); if (configuration != null) { // Get ModuleReferences from layer configuration @SuppressWarnings("unchecked") - final Set /* Set */ modules = (Set) ReflectionUtils - .invokeMethod(configuration, "modules", /* throwException = */ true); + final Set /* Set */ modules = (Set) reflectionUtils + .invokeMethod(/* throwException = */ true, configuration, "modules"); if (modules != null) { final List modulesInLayer = new ArrayList<>(); for (final Object /* ResolvedModule */ module : modules) { - final Object /* ModuleReference */ moduleReference = ReflectionUtils.invokeMethod(module, - "reference", /* throwException = */ true); + final Object /* ModuleReference */ moduleReference = reflectionUtils + .invokeMethod(/* throwException = */ true, module, "reference"); if (moduleReference != null && addedModules.add(moduleReference)) { try { - modulesInLayer.add(new ModuleRef(moduleReference, layer)); + modulesInLayer.add(new ModuleRef(moduleReference, layer, reflectionUtils)); } catch (final IllegalArgumentException e) { if (log != null) { log.log("Exception while creating ModuleRef for module " + moduleReference, e); @@ -189,7 +197,7 @@ private static List findModuleRefs(final List layers, final S } } // Sort modules in layer by name - Collections.sort(modulesInLayer); + CollectionUtils.sortIfNotEmpty(modulesInLayer); moduleRefOrder.addAll(modulesInLayer); } } @@ -204,22 +212,29 @@ private static List findModuleRefs(final List layers, final S * the call stack * @param scanSpec * the scan spec + * @param scanNonSystemModules + * whether to include unnamed and non-system modules * @param log * the log * @return the list */ - private static List findModuleRefs(final Class[] callStack, final ScanSpec scanSpec, - final LogNode log) { - final List layers = new ArrayList<>(); - for (final Class stackFrameClass : callStack) { - final Object /* Module */ module = ReflectionUtils.invokeMethod(stackFrameClass, "getModule", - /* throwException = */ false); - if (module != null) { - final Object /* ModuleLayer */ layer = ReflectionUtils.invokeMethod(module, "getLayer", - /* throwException = */ true); - // getLayer() returns null for unnamed modules -- we have to get their classes from java.class.path - if (layer != null) { - layers.add(layer); + private List findModuleRefsFromCallstack(final Class[] callStack, final ScanSpec scanSpec, + final boolean scanNonSystemModules, final LogNode log) { + final LinkedHashSet layers = new LinkedHashSet<>(); + if (callStack != null) { + for (final Class stackFrameClass : callStack) { + final Object /* Module */ module = reflectionUtils.invokeMethod(/* throwException = */ false, + stackFrameClass, "getModule"); + if (module != null) { + final Object /* ModuleLayer */ layer = reflectionUtils.invokeMethod(/* throwException = */ true, + module, "getLayer"); + if (layer != null) { + layers.add(layer); + } else if (scanNonSystemModules) { + // getLayer() returns null for unnamed modules -- still add null to list if it is returned, + // so we can get classes from java.class.path + forceScanJavaClassPath = true; + } } } } @@ -231,10 +246,15 @@ private static List findModuleRefs(final Class[] callStack, final // Ignored } if (moduleLayerClass != null) { - final Object /* ModuleLayer */ bootLayer = ReflectionUtils.invokeStaticMethod(moduleLayerClass, "boot", - /* throwException = */ false); + final Object /* ModuleLayer */ bootLayer = reflectionUtils + .invokeStaticMethod(/* throwException = */ false, moduleLayerClass, "boot"); if (bootLayer != null) { layers.add(bootLayer); + } else if (scanNonSystemModules) { + // getLayer() returns null for unnamed modules -- still add null to list if it is returned, + // so we can get classes from java.class.path. (I'm not sure if the boot layer can ever + // actually be null, but this is here for completeness.) + forceScanJavaClassPath = true; } } return findModuleRefs(layers, scanSpec, log); @@ -243,140 +263,80 @@ private static List findModuleRefs(final Class[] callStack, final // ------------------------------------------------------------------------------------------------------------- /** - * A class to find the unique ordered classpath elements. - * + * A class to find the visible modules. + * + * @param callStack + * the callstack. * @param scanSpec - * The scan spec, or null if none available. + * The scan spec. + * @param scanNonSystemModules + * whether to scan unnamed and non-system modules + * @param scanSystemModules + * whether to scan system modules * @param log * The log. */ - ClassLoaderAndModuleFinder(final ScanSpec scanSpec, final LogNode log) { - LinkedHashSet classLoadersUnique; - LogNode classLoadersFoundLog; - if (scanSpec.overrideClassLoaders == null) { - // ClassLoaders were not overridden + public ModuleFinder(final Class[] callStack, final ScanSpec scanSpec, final boolean scanNonSystemModules, + final boolean scanSystemModules, final ReflectionUtils reflectionUtils, final LogNode log) { + this.reflectionUtils = reflectionUtils; - // Add the ClassLoaders in the order system, caller, context; then remove any of them that are - // parents/ancestors of one or more other classloaders (performed below). There will generally only be - // one class left after this. In rare cases, you may have a separate callerLoader and contextLoader, but - // those cases are ill-defined -- see: - // http://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html?page=2 - - // Get system classloader - classLoadersUnique = new LinkedHashSet<>(); - final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); - if (systemClassLoader != null) { - classLoadersUnique.add(systemClassLoader); + // Get the module resolution order + List allModuleRefsList = null; + if (scanSpec.overrideModuleLayers == null) { + // Find module references for classes on callstack, and from system (for JDK9+) + if (callStack != null && callStack.length > 0) { + allModuleRefsList = findModuleRefsFromCallstack(callStack, scanSpec, scanNonSystemModules, log); } - - // There is one more classloader in JDK9+, the platform classloader (used for handling extensions), - // see: http://openjdk.java.net/jeps/261#Class-loaders - // The method call to get it is ClassLoader.getPlatformClassLoader() - // However, since it's not possible to get URLs from this classloader, and it is the parent of - // the application classloader returned by ClassLoader.getSystemClassLoader() (so is delegated to - // by the application classloader), there is no point adding it here. - - List allModuleRefsList = null; - if (scanSpec.overrideModuleLayers == null) { - try { - // Find classloaders for classes on callstack - final Class[] callStack = CallStackReader.getClassContext(log); - for (int i = callStack.length - 1; i >= 0; --i) { - final ClassLoader callerClassLoader = callStack[i].getClassLoader(); - if (callerClassLoader != null) { - classLoadersUnique.add(callerClassLoader); - } - } - // Find module references for classes on callstack (for JDK9+) - allModuleRefsList = findModuleRefs(callStack, scanSpec, log); - } catch (final IllegalArgumentException e) { - if (log != null) { - log.log("Could not get call stack", e); - } - } - } else { - if (log != null) { - final LogNode subLog = log.log("Overriding module layers"); - for (final Object moduleLayer : scanSpec.overrideModuleLayers) { - subLog.log(moduleLayer.toString()); - } + } else { + if (log != null) { + final LogNode subLog = log.log("Overriding module layers"); + for (final Object moduleLayer : scanSpec.overrideModuleLayers) { + subLog.log(moduleLayer.toString()); } - allModuleRefsList = findModuleRefs(scanSpec.overrideModuleLayers, scanSpec, log); } - - if (allModuleRefsList != null) { - // Split modules into system modules and non-system modules - systemModuleRefs = new ArrayList<>(); - nonSystemModuleRefs = new ArrayList<>(); - for (final ModuleRef moduleRef : allModuleRefsList) { - if (moduleRef.isSystemModule()) { + allModuleRefsList = findModuleRefs(new LinkedHashSet<>(scanSpec.overrideModuleLayers), scanSpec, log); + } + if (allModuleRefsList != null) { + // Split modules into system modules and non-system modules + systemModuleRefs = new ArrayList<>(); + nonSystemModuleRefs = new ArrayList<>(); + for (final ModuleRef moduleRef : allModuleRefsList) { + if (moduleRef != null) { + final boolean isSystemModule = moduleRef.isSystemModule(); + if (isSystemModule && scanSystemModules) { systemModuleRefs.add(moduleRef); - } else { + } else if (!isSystemModule && scanNonSystemModules) { nonSystemModuleRefs.add(moduleRef); } } } - - // Get context classloader - final ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader(); - if (threadClassLoader != null) { - classLoadersUnique.add(threadClassLoader); - } - - // Add any custom-added classloaders after system/context classloaders - if (scanSpec.addedClassLoaders != null) { - classLoadersUnique.addAll(scanSpec.addedClassLoaders); - } - classLoadersFoundLog = log == null ? null : log.log("Found ClassLoaders:"); - - } else { - // ClassLoaders were overridden - classLoadersUnique = new LinkedHashSet<>(scanSpec.overrideClassLoaders); - classLoadersFoundLog = log == null ? null : log.log("Override ClassLoaders:"); - } - - // Remove all ancestral classloaders (they are called automatically during class load) - final Set ancestralClassLoaders = new HashSet<>(classLoadersUnique.size()); - for (final ClassLoader classLoader : classLoadersUnique) { - for (ClassLoader cl = classLoader.getParent(); cl != null; cl = cl.getParent()) { - ancestralClassLoaders.add(cl); - } } - final List classLoaderFinalOrder = new ArrayList<>(classLoadersUnique.size()); - for (final ClassLoader classLoader : classLoadersUnique) { - // Build final ClassLoader order, with ancestral classloaders removed - if (!ancestralClassLoaders.contains(classLoader)) { - classLoaderFinalOrder.add(classLoader); - } - } - - // Log all identified ClassLoaders - if (classLoadersFoundLog != null) { - for (final ClassLoader classLoader : classLoaderFinalOrder) { - classLoadersFoundLog.log(classLoader.getClass().getName()); - } - } - // Log any identified modules if (log != null) { - final LogNode sysSubLog = log.log("Found system modules:"); - if (systemModuleRefs != null && !systemModuleRefs.isEmpty()) { - for (final ModuleRef moduleRef : systemModuleRefs) { - sysSubLog.log(moduleRef.toString()); + if (scanSystemModules) { + final LogNode sysSubLog = log.log("System modules found:"); + if (systemModuleRefs != null && !systemModuleRefs.isEmpty()) { + for (final ModuleRef moduleRef : systemModuleRefs) { + sysSubLog.log(moduleRef.toString()); + } + } else { + sysSubLog.log("[None]"); } } else { - sysSubLog.log("[None]"); + log.log("Scanning of system modules is not enabled"); } - final LogNode nonSysSubLog = log.log("Found non-system modules:"); - if (nonSystemModuleRefs != null && !nonSystemModuleRefs.isEmpty()) { - for (final ModuleRef moduleRef : nonSystemModuleRefs) { - nonSysSubLog.log(moduleRef.toString()); + if (scanNonSystemModules) { + final LogNode nonSysSubLog = log.log("Non-system modules found:"); + if (nonSystemModuleRefs != null && !nonSystemModuleRefs.isEmpty()) { + for (final ModuleRef moduleRef : nonSystemModuleRefs) { + nonSysSubLog.log(moduleRef.toString()); + } + } else { + nonSysSubLog.log("[None]"); } } else { - nonSysSubLog.log("[None]"); + log.log("Scanning of non-system modules is not enabled"); } } - - this.contextClassLoaders = classLoaderFinalOrder.toArray(new ClassLoader[0]); } } diff --git a/src/main/java/nonapi/io/github/classgraph/classpath/SystemJarFinder.java b/src/main/java/nonapi/io/github/classgraph/classpath/SystemJarFinder.java index c86c5b5a0..3e51b6478 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/SystemJarFinder.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/SystemJarFinder.java @@ -43,6 +43,9 @@ public final class SystemJarFinder { /** The paths of any "rt.jar" files found in the JRE. */ private static final Set RT_JARS = new LinkedHashSet<>(); + /** The path of the first "rt.jar" found. */ + private static final String RT_JAR; + /** The paths of any "lib/" or "ext/" jars found in the JRE. */ private static final Set JRE_LIB_OR_EXT_JARS = new LinkedHashSet<>(); @@ -61,14 +64,14 @@ private SystemJarFinder() { * @return true if the directory was readable. */ private static boolean addJREPath(final File dir) { - if (dir != null && !dir.getPath().isEmpty() && FileUtils.canRead(dir) && dir.isDirectory()) { + if (dir != null && !dir.getPath().isEmpty() && FileUtils.canReadAndIsDir(dir)) { final File[] dirFiles = dir.listFiles(); if (dirFiles != null) { for (final File file : dirFiles) { final String filePath = file.getPath(); if (filePath.endsWith(".jar")) { - final String jarPathResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, filePath); - if (filePath.endsWith("/rt.jar")) { + final String jarPathResolved = FastPathResolver.resolve(FileUtils.currDirPath(), filePath); + if (jarPathResolved.endsWith("/rt.jar")) { RT_JARS.add(jarPathResolved); } else { JRE_LIB_OR_EXT_JARS.add(jarPathResolved); @@ -78,7 +81,7 @@ private static boolean addJREPath(final File dir) { final String canonicalFilePath = canonicalFile.getPath(); if (!canonicalFilePath.equals(filePath)) { final String canonicalJarPathResolved = FastPathResolver - .resolve(FileUtils.CURR_DIR_PATH, filePath); + .resolve(FileUtils.currDirPath(), filePath); JRE_LIB_OR_EXT_JARS.add(canonicalJarPathResolved); } } catch (IOException | SecurityException e) { @@ -121,7 +124,7 @@ private static boolean addJREPath(final File dir) { } final String javaExtDirs = VersionFinder.getProperty("java.ext.dirs"); if (javaExtDirs != null && !javaExtDirs.isEmpty()) { - for (final String javaExtDir : JarUtils.smartPathSplit(javaExtDirs)) { + for (final String javaExtDir : JarUtils.smartPathSplit(javaExtDirs, /* scanSpec = */ null)) { if (!javaExtDir.isEmpty()) { addJREPath(new File(javaExtDir)); } @@ -163,6 +166,8 @@ private static boolean addJREPath(final File dir) { default: break; } + + RT_JAR = RT_JARS.isEmpty() ? null : FastPathResolver.resolve(RT_JARS.iterator().next()); } /** @@ -172,7 +177,7 @@ private static boolean addJREPath(final File dir) { */ public static String getJreRtJarPath() { // Only include the first rt.jar -- if there is a copy in both the JDK and JRE, no need to scan both - return !RT_JARS.isEmpty() ? FastPathResolver.resolve(RT_JARS.iterator().next()) : null; + return RT_JAR; } /** diff --git a/src/main/java/nonapi/io/github/classgraph/concurrency/AutoCloseableExecutorService.java b/src/main/java/nonapi/io/github/classgraph/concurrency/AutoCloseableExecutorService.java index d04119f94..6a6821a56 100644 --- a/src/main/java/nonapi/io/github/classgraph/concurrency/AutoCloseableExecutorService.java +++ b/src/main/java/nonapi/io/github/classgraph/concurrency/AutoCloseableExecutorService.java @@ -35,8 +35,6 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import io.github.classgraph.ClassGraphException; - /** A ThreadPoolExecutor that can be used in a try-with-resources block. */ public class AutoCloseableExecutorService extends ThreadPoolExecutor implements AutoCloseable { /** The {@link InterruptionChecker}. */ @@ -108,7 +106,7 @@ public void close() { // Interrupt all the threads to terminate them, if awaitTermination() timed out shutdownNow(); } catch (final SecurityException e) { - throw ClassGraphException.newClassGraphException("Could not shut down ExecutorService -- need " + throw new RuntimeException("Could not shut down ExecutorService -- need " + "java.lang.RuntimePermission(\"modifyThread\"), " + "or the security manager's checkAccess method denies access", e); } diff --git a/src/main/java/nonapi/io/github/classgraph/concurrency/InterruptionChecker.java b/src/main/java/nonapi/io/github/classgraph/concurrency/InterruptionChecker.java index 9d74c12e2..83b500caa 100644 --- a/src/main/java/nonapi/io/github/classgraph/concurrency/InterruptionChecker.java +++ b/src/main/java/nonapi/io/github/classgraph/concurrency/InterruptionChecker.java @@ -42,7 +42,7 @@ public class InterruptionChecker { /** The first {@link ExecutionException} that was thrown. */ private final AtomicReference thrownExecutionException = // - new AtomicReference(); + new AtomicReference<>(); /** Interrupt all threads that share this InterruptionChecker. */ public void interrupt() { diff --git a/src/main/java/nonapi/io/github/classgraph/concurrency/SimpleThreadFactory.java b/src/main/java/nonapi/io/github/classgraph/concurrency/SimpleThreadFactory.java index 2ffd67466..4b2cfea98 100644 --- a/src/main/java/nonapi/io/github/classgraph/concurrency/SimpleThreadFactory.java +++ b/src/main/java/nonapi/io/github/classgraph/concurrency/SimpleThreadFactory.java @@ -28,6 +28,7 @@ */ package nonapi.io.github.classgraph.concurrency; +import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicInteger; /** @@ -35,8 +36,7 @@ * * @author Johno Crawford (johno@sulake.com) */ -class SimpleThreadFactory implements java.util.concurrent.ThreadFactory { - +public class SimpleThreadFactory implements java.util.concurrent.ThreadFactory { /** The thread name prefix. */ private final String threadNamePrefix; @@ -59,13 +59,32 @@ class SimpleThreadFactory implements java.util.concurrent.ThreadFactory { this.daemon = daemon; } - /* (non-Javadoc) - * @see java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable) + /** + * New thread. + * + * @param runnable + * the runnable + * @return the thread */ @Override - public Thread newThread(final Runnable r) { - final Thread t = new Thread(r, threadNamePrefix + threadIdx.getAndIncrement()); - t.setDaemon(daemon); - return t; + public Thread newThread(final Runnable runnable) { + // Call System.getSecurityManager().getThreadGroup() via reflection, since it is deprecated in JDK 17 + ThreadGroup securityManagerThreadGroup = null; + try { + final Method getSecurityManager = System.class.getDeclaredMethod("getSecurityManager"); + final Object securityManager = getSecurityManager.invoke(null); + if (securityManager != null) { + final Method getThreadGroup = securityManager.getClass().getDeclaredMethod("getThreadGroup"); + securityManagerThreadGroup = (ThreadGroup) getThreadGroup.invoke(securityManager); + } + } catch (final Throwable t) { + // Fall through + } + final Thread thread = new Thread( + securityManagerThreadGroup != null ? securityManagerThreadGroup + : new ThreadGroup("ClassGraph-thread-group"), + runnable, threadNamePrefix + threadIdx.getAndIncrement()); + thread.setDaemon(daemon); + return thread; } } diff --git a/src/main/java/nonapi/io/github/classgraph/concurrency/SingletonMap.java b/src/main/java/nonapi/io/github/classgraph/concurrency/SingletonMap.java index 85b5e811a..61fdb4014 100644 --- a/src/main/java/nonapi/io/github/classgraph/concurrency/SingletonMap.java +++ b/src/main/java/nonapi/io/github/classgraph/concurrency/SingletonMap.java @@ -28,6 +28,7 @@ */ package nonapi.io.github.classgraph.concurrency; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; @@ -41,11 +42,13 @@ * A map from keys to singleton instances. Allows you to create object instance singletons and add them to a * {@link ConcurrentMap} on demand, based on a key value. Works the same as * {@code concurrentMap.computeIfAbsent(key, key -> newInstance(key))}, except that it also works on JDK 7. - * + * * @param * The key type. * @param * The value type. + * @param + * the element type */ public abstract class SingletonMap { /** The map. */ @@ -71,6 +74,26 @@ public NullSingletonException(final K key) { } } + /** Thrown when {@link SingletonMap#newInstance(Object, LogNode)} throws an exception. */ + public static class NewInstanceException extends Exception { + /** serialVersionUID. */ + static final long serialVersionUID = 1L; + + /** + * Constructor. + * + * @param + * the key type + * @param key + * the key + * @param t + * the Throwable that was thrown + */ + public NewInstanceException(final K key, final Throwable t) { + super("newInstance threw an exception for key " + key + " : " + t, t); + } + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -84,6 +107,7 @@ public NullSingletonException(final K key) { */ private static class SingletonHolder { /** The singleton. */ + @SuppressWarnings("null") private volatile V singleton; /** Whether or not the singleton has been initialized (the count will have reached 0 if so). */ @@ -139,6 +163,22 @@ V get() throws InterruptedException { */ public abstract V newInstance(K key, LogNode log) throws E, InterruptedException; + /** + * Create a new instance. + * + * @param + * The instance type. + */ + @FunctionalInterface + public interface NewInstanceFactory { + /** + * Create a new instance. + * + * @return The new instance. + */ + public V newInstance() throws E, InterruptedException; + } + /** * Check if the given key is in the map, and if so, return the value of {@link #newInstance(Object, LogNode)} * for that key, or block on the result of {@link #newInstance(Object, LogNode)} if another thread is currently @@ -150,6 +190,10 @@ V get() throws InterruptedException { * * @param key * The key for the singleton. + * @param newInstanceFactory + * if non-null, a factory for creating new instances, otherwise if null, then + * {@link #newInstance(Object, LogNode)} is called instead (this allows new instance creation to be + * overridden on a per-instance basis). * @param log * The log. * @return The non-null singleton instance, if {@link #newInstance(Object, LogNode)} returned a non-null @@ -162,9 +206,13 @@ V get() throws InterruptedException { * thread. * @throws NullSingletonException * if {@link #newInstance(Object, LogNode)} returned null. + * @throws NewInstanceException + * if {@link #newInstance(Object, LogNode)} threw an exception. */ - public V get(final K key, final LogNode log) throws E, InterruptedException, NullSingletonException { + public V get(final K key, final LogNode log, final NewInstanceFactory newInstanceFactory) + throws E, InterruptedException, NullSingletonException, NewInstanceException { final SingletonHolder singletonHolder = map.get(key); + @SuppressWarnings("null") V instance = null; if (singletonHolder != null) { // There is already a SingletonHolder in the map for this key -- get the value @@ -181,15 +229,23 @@ public V get(final K key, final LogNode log) throws E, InterruptedException, Nul } else { try { // Create a new instance - instance = newInstance(key, log); + if (newInstanceFactory != null) { + // Call NewInstanceFactory + instance = newInstanceFactory.newInstance(); + } else { + // Call overridden newInstance method + instance = newInstance(key, log); + } - } finally { + } catch (final Throwable t) { // Initialize newSingletonHolder with the new instance. // Always need to call .set() even if an exception is thrown by newInstance() // or newInstance() returns null, since .set() calls initialized.countDown(). // Otherwise threads that call .get() may end up waiting forever. newSingletonHolder.set(instance); + throw new NewInstanceException(key, t); } + newSingletonHolder.set(instance); } } if (instance == null) { @@ -199,6 +255,37 @@ public V get(final K key, final LogNode log) throws E, InterruptedException, Nul } } + /** + * Check if the given key is in the map, and if so, return the value of {@link #newInstance(Object, LogNode)} + * for that key, or block on the result of {@link #newInstance(Object, LogNode)} if another thread is currently + * creating the new instance. + * + * If the given key is not currently in the map, store a placeholder in the map for this key, then run + * {@link #newInstance(Object, LogNode)} for the key, store the result in the placeholder (which unblocks any + * other threads waiting for the value), and then return the new instance. + * + * @param key + * The key for the singleton. + * @param log + * The log. + * @return The non-null singleton instance, if {@link #newInstance(Object, LogNode)} returned a non-null + * instance on this call or a previous call, otherwise throws {@link NullPointerException} if this call + * or a previous call to {@link #newInstance(Object, LogNode)} returned null. + * @throws E + * If {@link #newInstance(Object, LogNode)} threw an exception. + * @throws InterruptedException + * if the thread was interrupted while waiting for the singleton to be instantiated by another + * thread. + * @throws NullSingletonException + * if {@link #newInstance(Object, LogNode)} returned null. + * @throws NewInstanceException + * if {@link #newInstance(Object, LogNode)} threw an exception. + */ + public V get(final K key, final LogNode log) + throws E, InterruptedException, NullSingletonException, NewInstanceException { + return get(key, log, null); + } + /** * Get all valid singleton values in the map. * @@ -218,6 +305,45 @@ public List values() throws InterruptedException { return entries; } + /** + * Returns true if the map is empty. + * + * @return true, if the map is empty + */ + public boolean isEmpty() { + return map.isEmpty(); + } + + /** + * Get the map entries. + * + * @return the map entries. + * @throws InterruptedException + * if interrupted. + */ + public List> entries() throws InterruptedException { + final List> entries = new ArrayList<>(map.size()); + for (final Entry> ent : map.entrySet()) { + entries.add(new SimpleEntry<>(ent.getKey(), ent.getValue().get())); + } + return entries; + } + + /** + * Remove the singleton for a given key. + * + * @param key + * the key + * @return the old singleton from the map, if one was present, otherwise null. + * @throws InterruptedException + * if interrupted. + */ + @SuppressWarnings("null") + public V remove(final K key) throws InterruptedException { + final SingletonHolder val = map.remove(key); + return val == null ? null : val.get(); + } + /** Clear the map. */ public void clear() { map.clear(); diff --git a/src/main/java/nonapi/io/github/classgraph/concurrency/WorkQueue.java b/src/main/java/nonapi/io/github/classgraph/concurrency/WorkQueue.java index 9d70802c0..60de7e1e6 100644 --- a/src/main/java/nonapi/io/github/classgraph/concurrency/WorkQueue.java +++ b/src/main/java/nonapi/io/github/classgraph/concurrency/WorkQueue.java @@ -170,7 +170,7 @@ public static void runWorkQueue(final Collection elements, final Executor * @param workUnitProcessor * the work unit processor * @param numWorkers - * the num workers + * the number of workers * @param interruptionChecker * the interruption checker * @param log @@ -208,6 +208,7 @@ public Void call() throws Exception { /** * Send poison pills to workers. */ + @SuppressWarnings("null") private void sendPoisonPills() { for (int i = 0; i < numWorkers; i++) { workUnits.add(new WorkUnitWrapper(null)); @@ -228,39 +229,40 @@ private void sendPoisonPills() { private void runWorkLoop() throws InterruptedException, ExecutionException { // Get next work unit from queue for (;;) { - // Check for interruption - interruptionChecker.check(); + // Process the work unit + try { + // Check for interruption + interruptionChecker.check(); - // Get next work unit - final WorkUnitWrapper workUnitWrapper = workUnits.take(); + // Get next work unit + final WorkUnitWrapper workUnitWrapper = workUnits.take(); - if (workUnitWrapper.workUnit == null) { - // Received poison pill - break; - } + if (workUnitWrapper.workUnit == null) { + // Received poison pill + break; + } - // Process the work unit - try { // Process the work unit (may throw InterruptedException) workUnitProcessor.processWorkUnit(workUnitWrapper.workUnit, this, log); - } catch (InterruptedException | OutOfMemoryError e) { + } catch (InterruptedException | Error e) { // On InterruptedException or OutOfMemoryError, drain work queue, send poison pills, and re-throw workUnits.clear(); + numIncompleteWorkUnits.set(0); sendPoisonPills(); throw e; } catch (final RuntimeException e) { // On unchecked exception, drain work queue, send poison pills, and throw ExecutionException workUnits.clear(); + numIncompleteWorkUnits.set(0); sendPoisonPills(); throw new ExecutionException("Worker thread threw unchecked exception", e); - } finally { - if (numIncompleteWorkUnits.decrementAndGet() == 0) { - // No more work units -- send poison pills - sendPoisonPills(); - } + } + if (numIncompleteWorkUnits.decrementAndGet() == 0) { + // No more work units -- send poison pills + sendPoisonPills(); } } } diff --git a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/FastZipEntry.java b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/FastZipEntry.java index 4660f9aa0..11afcfbfd 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/FastZipEntry.java +++ b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/FastZipEntry.java @@ -28,19 +28,12 @@ */ package nonapi.io.github.classgraph.fastzipfilereader; -import java.io.EOFException; import java.io.IOException; -import java.io.InputStream; -import java.nio.Buffer; -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.zip.DataFormatException; -import java.util.zip.Inflater; -import java.util.zip.ZipException; - -import nonapi.io.github.classgraph.recycler.RecycleOnClose; -import nonapi.io.github.classgraph.utils.FileUtils; +import java.util.Calendar; +import java.util.TimeZone; + +import nonapi.io.github.classgraph.fileslice.Slice; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessReader; import nonapi.io.github.classgraph.utils.VersionFinder; /** A zip entry within a {@link LogicalZipFile}. */ @@ -51,9 +44,6 @@ public class FastZipEntry implements Comparable { /** The offset of the entry's local header, as an offset relative to the parent logical zipfile. */ private final long locHeaderPos; - /** The start offset of the entry's compressed data, as an absolute offset within the physical zipfile. */ - private long entryDataStartOffsetWithinPhysicalZipFile = -1L; - /** The zip entry path. */ public final String entryName; @@ -66,6 +56,21 @@ public class FastZipEntry implements Comparable { /** The uncompressed size of the zip entry, in bytes. */ public final long uncompressedSize; + /** The last modified millis since the epoch, or 0L if it is unknown */ + private long lastModifiedTimeMillis; + + /** The last modified time in MSDOS format, if {@link FastZipEntry#lastModifiedTimeMillis} is 0L. */ + private final int lastModifiedTimeMSDOS; + + /** The last modified date in MSDOS format, if {@link FastZipEntry#lastModifiedTimeMillis} is 0L. */ + private final int lastModifiedDateMSDOS; + + /** The file attributes for this resource, or 0 if unknown. */ + public final int fileAttributes; + + /** The {@link Slice} for the zip entry's raw data (which can be either stored or deflated). */ + private Slice slice; + /** * The version code (>= 9), or 8 for the base layer or a non-versioned jar (whether JDK 7 or 8 compatible). */ @@ -76,17 +81,11 @@ public class FastZipEntry implements Comparable { */ public final String entryNameUnversioned; - /** The nested jar handler. */ - private final NestedJarHandler nestedJarHandler; - - /** The {@link RecyclableInflater} instance wrapping recyclable {@link Inflater} instances. */ - private RecyclableInflater recyclableInflaterInstance; - // ------------------------------------------------------------------------------------------------------------- /** * Constructor. - * + * * @param parentLogicalZipFile * The parent logical zipfile containing this entry. * @param locHeaderPos @@ -99,19 +98,30 @@ public class FastZipEntry implements Comparable { * The compressed size of the entry. * @param uncompressedSize * The uncompressed size of the entry. - * @param nestedJarHandler - * The {@link NestedJarHandler}. + * @param lastModifiedTimeMillis + * The last modified date/time in millis since the epoch, or 0L if unknown (in which case, the MSDOS + * time and date fields will be provided). + * @param lastModifiedTimeMSDOS + * The last modified date, in MSDOS format, if lastModifiedMillis is 0L. + * @param lastModifiedDateMSDOS + * The last modified date, in MSDOS format, if lastModifiedMillis is 0L. + * @param fileAttributes + * The POSIX file attribute bits from the zip entry. */ FastZipEntry(final LogicalZipFile parentLogicalZipFile, final long locHeaderPos, final String entryName, final boolean isDeflated, final long compressedSize, final long uncompressedSize, - final NestedJarHandler nestedJarHandler) { + final long lastModifiedTimeMillis, final int lastModifiedTimeMSDOS, final int lastModifiedDateMSDOS, + final int fileAttributes, final boolean enableMultiReleaseVersions) { this.parentLogicalZipFile = parentLogicalZipFile; this.locHeaderPos = locHeaderPos; this.entryName = entryName; this.isDeflated = isDeflated; this.compressedSize = compressedSize; this.uncompressedSize = !isDeflated && uncompressedSize < 0 ? compressedSize : uncompressedSize; - this.nestedJarHandler = nestedJarHandler; + this.lastModifiedTimeMillis = lastModifiedTimeMillis; + this.lastModifiedTimeMSDOS = lastModifiedTimeMSDOS; + this.lastModifiedDateMSDOS = lastModifiedDateMSDOS; + this.fileAttributes = fileAttributes; // Get multi-release jar version number, and strip any version prefix int entryVersion = 8; @@ -149,7 +159,7 @@ public class FastZipEntry implements Comparable { if (entryVersion < 9 || entryVersion > VersionFinder.JAVA_MAJOR_VERSION) { entryVersion = 8; } - if (entryVersion > 8) { + if (!enableMultiReleaseVersions && entryVersion > 8) { // Strip version path prefix entryNameWithoutVersionPrefix = entryName.substring(nextSlashIdx + 1); // For META-INF/versions/{versionInt}/META-INF/*, don't strip version prefix: @@ -169,421 +179,31 @@ public class FastZipEntry implements Comparable { // ------------------------------------------------------------------------------------------------------------- /** - * Lazily find zip entry data start offset -- this is deferred until zip entry data needs to be read, in order - * to avoid randomly seeking within zipfile for every entry as the central directory is read. + * Lazily get zip entry slice -- this is deferred until zip entry data needs to be read, in order to avoid + * randomly seeking within zipfile for every entry as the central directory is read. * * @return the offset within the physical zip file of the entry's start offset. * @throws IOException * If an I/O exception occurs. - * @throws InterruptedException - * If the thread was interrupted. */ - long getEntryDataStartOffsetWithinPhysicalZipFile() throws IOException, InterruptedException { - if (entryDataStartOffsetWithinPhysicalZipFile == -1L) { - // Create zipfile slice reader for zip entry - try (RecycleOnClose zipFileSliceReaderRecycleOnClose = // - parentLogicalZipFile.zipFileSliceReaderRecycler.acquireRecycleOnClose()) { - final ZipFileSliceReader headerReader = zipFileSliceReaderRecycleOnClose.get(); - // Check header magic - if (headerReader.getInt(locHeaderPos) != 0x04034b50) { - throw new IOException("Zip entry has bad LOC header: " + entryName); - } - final long dataStartPos = locHeaderPos + 30 + headerReader.getShort(locHeaderPos + 26) - + headerReader.getShort(locHeaderPos + 28); - if (dataStartPos > parentLogicalZipFile.len) { - throw new IOException("Unexpected EOF when trying to read zip entry data: " + entryName); - } - entryDataStartOffsetWithinPhysicalZipFile = parentLogicalZipFile.startOffsetWithinPhysicalZipFile - + dataStartPos; - } - } - return entryDataStartOffsetWithinPhysicalZipFile; - } - - // ------------------------------------------------------------------------------------------------------------- - - /** - * True if the entire zip entry can be opened as a single ByteBuffer slice. - * - * @return true if the entire zip entry can be opened as a single ByteBuffer slice -- the entry must be STORED, - * and span only one 2GB buffer chunk. - * @throws IOException - * If an I/O exception occurs. - * @throws InterruptedException - * If the thread was interrupted. - */ - public boolean canGetAsSlice() throws IOException, InterruptedException { - final long dataStartOffsetWithinPhysicalZipFile = getEntryDataStartOffsetWithinPhysicalZipFile(); - return !isDeflated // - && dataStartOffsetWithinPhysicalZipFile / FileUtils.MAX_BUFFER_SIZE // - == (dataStartOffsetWithinPhysicalZipFile + uncompressedSize) / FileUtils.MAX_BUFFER_SIZE; - } - - /** - * Open the ZipEntry as a ByteBuffer slice. Only call this method if {@link #canGetAsSlice()} returned true. - * - * @return the ZipEntry as a ByteBuffer. - * @throws IOException - * If an I/O exception occurs. - * @throws InterruptedException - * If the thread was interrupted. - */ - public ByteBuffer getAsSlice() throws IOException, InterruptedException { - // Check the file is STORED and resides in only one chunk - if (!canGetAsSlice()) { - throw new IllegalArgumentException("Cannot open zip entry as a slice"); - } - // Fetch the ByteBuffer for the applicable chunk - final long dataStartOffsetWithinPhysicalZipFile = getEntryDataStartOffsetWithinPhysicalZipFile(); - final int chunkIdx = (int) (dataStartOffsetWithinPhysicalZipFile / FileUtils.MAX_BUFFER_SIZE); - final long chunkStart = chunkIdx * (long) FileUtils.MAX_BUFFER_SIZE; - final ByteBuffer dupdBuf = parentLogicalZipFile.physicalZipFile.getByteBuffer(chunkIdx).duplicate(); - // Create and return a slice on the chunk ByteBuffer that contains only this zip entry - // N.B. the cast to Buffer is necessary, see: - // https://github.com/plasma-umass/doppio/issues/497#issuecomment-334740243 - // https://github.com/classgraph/classgraph/issues/284#issuecomment-443612800 - ((Buffer) dupdBuf).position((int) (dataStartOffsetWithinPhysicalZipFile - chunkStart)); - ((Buffer) dupdBuf).limit((int) (dataStartOffsetWithinPhysicalZipFile + uncompressedSize - chunkStart)); - return dupdBuf.slice(); - } - - // ------------------------------------------------------------------------------------------------------------- - - /** - * Open the data of the zip entry as an {@link InputStream}, inflating the data if the entry is deflated. - * - * @return the input stream - * @throws IOException - * If an I/O exception occurs. - * @throws InterruptedException - * if the thread was interrupted. - */ - public InputStream open() throws IOException, InterruptedException { - if (recyclableInflaterInstance != null) { - throw new IOException("Zip entry already open"); - } - if (isDeflated) { - recyclableInflaterInstance = nestedJarHandler.inflaterRecycler.acquire(); - } - return new InputStream() { - /** The data start offset within the physical zip file. */ - private final long dataStartOffsetWithinPhysicalZipFile = getEntryDataStartOffsetWithinPhysicalZipFile(); - - /** A scratch buffer. */ - private final byte[] scratch = new byte[8192]; - - /** The current 2GB chunk of the zip entry. */ - private ByteBuffer currChunkByteBuf; - - /** True if the current 2GB chunk is the last chunk in the zip entry. */ - private boolean isLastChunk; - - /** The index of the current 2GB chunk. */ - private int currChunkIdx; - - /** True if the end of the zip entry has been reached. */ - private boolean eof; - - /** The {@link Inflater} instance, or null if the entry is stored rather than deflated. */ - private final Inflater inflater = isDeflated ? recyclableInflaterInstance.getInflater() : null; - - /** True if this {@link InputStream} has been closed. */ - private final AtomicBoolean closed = new AtomicBoolean(false); - - /** The size of the {@link Inflate} buffer to use. */ - private static final int INFLATE_BUF_SIZE = 1024; - - // Open the first 2GB chunk. - { - // Calculate the chunk index for the first chunk - currChunkIdx = (int) (dataStartOffsetWithinPhysicalZipFile / FileUtils.MAX_BUFFER_SIZE); - - // Get the MappedByteBuffer for the 2GB chunk, and duplicate it - currChunkByteBuf = parentLogicalZipFile.physicalZipFile.getByteBuffer(currChunkIdx).duplicate(); - - // Calculate the start position within the first chunk, and set the position of the slice. - // N.B. the cast to Buffer is necessary, see: - // https://github.com/plasma-umass/doppio/issues/497#issuecomment-334740243 - // https://github.com/classgraph/classgraph/issues/284#issuecomment-443612800 - final int chunkPos = (int) (dataStartOffsetWithinPhysicalZipFile - - (((long) currChunkIdx) * (long) FileUtils.MAX_BUFFER_SIZE)); - ((Buffer) currChunkByteBuf).position(chunkPos); - - // Calculate end pos for the first chunk, and truncate it if it overflows 2GB - final long endPos = chunkPos + compressedSize; - ((Buffer) currChunkByteBuf).limit((int) Math.min(FileUtils.MAX_BUFFER_SIZE, endPos)); - isLastChunk = endPos <= FileUtils.MAX_BUFFER_SIZE; - } - - /** Advance to the next 2GB chunk. */ - private boolean readNextChunk() throws IOException, InterruptedException { - currChunkIdx++; - if (currChunkIdx >= parentLogicalZipFile.physicalZipFile.numMappedByteBuffers) { - // Ran out of chunks - return false; - } - - // Calculate how many bytes were consumed in previous chunks - final long chunkStartOff = ((long) currChunkIdx) * (long) FileUtils.MAX_BUFFER_SIZE; - final long priorBytes = chunkStartOff - dataStartOffsetWithinPhysicalZipFile; - final long remainingBytes = compressedSize - priorBytes; - if (remainingBytes <= 0) { - return false; - } - - // Get the MappedByteBuffer for the next 2GB chunk, and duplicate it - currChunkByteBuf = parentLogicalZipFile.physicalZipFile.getByteBuffer(currChunkIdx).duplicate(); - - // The start position for 2nd and subsequent chunks is 0. - // N.B. the cast to Buffer is necessary, see: - // https://github.com/plasma-umass/doppio/issues/497#issuecomment-334740243 - // https://github.com/classgraph/classgraph/issues/284#issuecomment-443612800 - ((Buffer) currChunkByteBuf).position(0); - - // Calculate end pos for the next chunk, and truncate it if it overflows 2GB - ((Buffer) currChunkByteBuf).limit((int) Math.min(FileUtils.MAX_BUFFER_SIZE, remainingBytes)); - isLastChunk = remainingBytes <= FileUtils.MAX_BUFFER_SIZE; - return true; - } - - /** - * Inflate deflated data. - * - * @param buf - * the buffer to inflate into. - * @param off - * the offset within buf to start writing. - * @param len - * the number of bytes of uncompressed data to read. - * @return the number of bytes read. - * @throws IOException - * if an I/O exception occurred. - * @throws InterruptedException - * if the thread was interrupted. - */ - private int readDeflated(final byte[] buf, final int off, final int len) - throws IOException, InterruptedException { - try { - final byte[] inflateBuf = new byte[INFLATE_BUF_SIZE]; - int numInflatedBytes; - while ((numInflatedBytes = inflater.inflate(buf, off, len)) == 0) { - if (inflater.finished() || inflater.needsDictionary()) { - eof = true; - return -1; - } - if (inflater.needsInput()) { - // Check if there's still data left in the current chunk - if (!currChunkByteBuf.hasRemaining() - // No more bytes in current chunk -- get next chunk, and then make sure - // that currChunkByteBuf.hasRemaining() subsequently returns true - && !(readNextChunk() && currChunkByteBuf.hasRemaining())) { - // Ran out of data in the current chunk, and could not read a new chunk - throw new IOException("Unexpected EOF in deflated data"); - } - // Set inflater input for the current chunk - - // In JDK11+: simply use the following instead of all the lines below: - // inflater.setInput(currChunkByteBuf); - // N.B. the ByteBuffer version of setInput doesn't seem to need the extra - // padding byte at the end when using the "nowrap" Inflater option. - - // Copy from the ByteBuffer into a temporary byte[] array (needed for JDK<11). - try { - final int remaining = currChunkByteBuf.remaining(); - if (isLastChunk && remaining < inflateBuf.length) { - // An extra dummy byte is needed at the end of the input stream when - // using the "nowrap" Inflater option. - // See: ZipFile.ZipFileInputStream.fill() - currChunkByteBuf.get(inflateBuf, 0, remaining); - inflateBuf[remaining] = (byte) 0; - inflater.setInput(inflateBuf, 0, remaining + 1); - } else if (isLastChunk && remaining == inflateBuf.length) { - // If this is the last chunk to read, and the number of remaining - // bytes is exactly the size of the buffer, read one byte fewer than - // the number of remaining bytes, to cause the last byte to be read - // in an extra pass. - currChunkByteBuf.get(inflateBuf, 0, remaining - 1); - inflater.setInput(inflateBuf, 0, remaining - 1); - } else { - // There are more than inflateBuf.length bytes remaining to be read, - // or this is not the last chunk (i.e. read all remaining bytes in - // this chunk, which will trigger the next chunk to be read on the - // next loop iteration) - final int bytesToRead = Math.min(inflateBuf.length, remaining); - currChunkByteBuf.get(inflateBuf, 0, bytesToRead); - inflater.setInput(inflateBuf, 0, bytesToRead); - } - } catch (final BufferUnderflowException e) { - // Should not happen - throw new IOException("Unexpected EOF in deflated data"); - } - } - } - return numInflatedBytes; - } catch (final DataFormatException e) { - throw new ZipException( - e.getMessage() != null ? e.getMessage() : "Invalid deflated zip entry data"); - } - } + public Slice getSlice() throws IOException { + if (slice == null) { + final RandomAccessReader randomAccessReader = parentLogicalZipFile.slice.randomAccessReader(); - /** - * Copy stored (non-deflated) data from ByteBuffer to target buffer. - * - * @param buf - * the buffer to copy the stored entry into. - * @param off - * the offset within buf to start writing. - * @param len - * the number of bytes to read. - * @return the number of bytes read. - * @throws IOException - * if an I/O exception occurred. - * @throws InterruptedException - * if the thread was interrupted. - */ - private int readStored(final byte[] buf, final int off, final int len) - throws IOException, InterruptedException { - int read = 0; - while (read < len) { - if (!currChunkByteBuf.hasRemaining() && !readNextChunk()) { - return read == 0 ? -1 : read; - } - final int remainingToRead = len - read; - final int remainingInBuf = currChunkByteBuf.remaining(); - final int numBytesRead = Math.min(remainingToRead, remainingInBuf); - try { - currChunkByteBuf.get(buf, off + read, numBytesRead); - } catch (final BufferUnderflowException e) { - // Should not happen - throw new EOFException("Unexpected EOF in stored (non-deflated) zip entry data"); - } - read += numBytesRead; - } - return read; - } - - @Override - public int read(final byte[] buf, final int off, final int len) throws IOException { - if (closed.get()) { - throw new IOException("Stream closed"); - } - if (buf == null) { - throw new NullPointerException(); - } else if (off < 0 || len < 0 || len > buf.length - off) { - throw new IndexOutOfBoundsException(); - } else if (len == 0) { - return 0; - } else if (parentLogicalZipFile.physicalZipFile.fileLen == 0) { - return -1; - } - try { - if (isDeflated) { - return readDeflated(buf, off, len); - } else { - return readStored(buf, off, len); - } - } catch (final InterruptedException e) { - nestedJarHandler.interruptionChecker.interrupt(); - throw new IOException("Thread was interrupted"); - } + // Check header magic + if (randomAccessReader.readInt(locHeaderPos) != 0x04034b50) { + throw new IOException("Zip entry has bad LOC header: " + entryName); } - - @Override - public int read() throws IOException { - if (closed.get()) { - throw new IOException("Stream closed"); - } - return read(scratch, 0, 1) == -1 ? -1 : scratch[0] & 0xff; + final long dataStartPos = locHeaderPos + 30 + randomAccessReader.readShort(locHeaderPos + 26) + + randomAccessReader.readShort(locHeaderPos + 28); + if (dataStartPos > parentLogicalZipFile.slice.sliceLength) { + throw new IOException("Unexpected EOF when trying to read zip entry data: " + entryName); } - @Override - public int available() throws IOException { - if (closed.get()) { - throw new IOException("Stream closed"); - } - if (inflater.finished()) { - eof = true; - } - return eof ? 0 : 1; - } - - @Override - public long skip(final long n) throws IOException { - if (closed.get()) { - throw new IOException("Stream closed"); - } - if (n < 0) { - throw new IllegalArgumentException("Invalid skip value"); - } - long total = 0; - while (total < n) { - final int numSkipped = read(scratch, 0, (int) Math.min(n - total, scratch.length)); - if (numSkipped == -1) { - eof = true; - break; - } - total += numSkipped; - } - return total; - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public synchronized void mark(final int readlimit) { - throw new IllegalArgumentException("Not supported"); - } - - @Override - public synchronized void reset() throws IOException { - throw new IllegalArgumentException("Not supported"); - } - - @Override - public void close() throws IOException { - if (!closed.getAndSet(true)) { - currChunkByteBuf = null; - if (recyclableInflaterInstance != null) { - // Reset and recycle the Inflater - nestedJarHandler.inflaterRecycler.recycle(recyclableInflaterInstance); - recyclableInflaterInstance = null; - } - } - } - }; - } - - /** - * Load the content of the zip entry, and return it as a byte array. - * - * @return the entry as a byte[] array - * @throws IOException - * If an I/O exception occurs. - * @throws InterruptedException - * If the thread was interrupted. - */ - public byte[] load() throws IOException, InterruptedException { - try (InputStream is = open()) { - return FileUtils.readAllBytesAsArray(is, uncompressedSize); - } - } - - /** - * Load the content of the zip entry, and return it as a String (converting from UTF-8 byte format). - * - * @return the entry as a String - * @throws IOException - * If an I/O exception occurs. - * @throws InterruptedException - * If the thread was interrupted. - */ - public String loadAsString() throws IOException, InterruptedException { - try (InputStream is = open()) { - return FileUtils.readAllBytesAsString(is, uncompressedSize); + // Create a new Slice that wraps just the data of the zip entry, and mark whether it is deflated + slice = parentLogicalZipFile.slice.slice(dataStartPos, compressedSize, isDeflated, uncompressedSize); } + return slice; } // ------------------------------------------------------------------------------------------------------------- @@ -598,12 +218,33 @@ public String getPath() { return parentLogicalZipFile.getPath() + "!/" + entryName; } - /* (non-Javadoc) - * @see java.lang.Object#toString() + /** + * Get the last modified time in Epoch millis, or 0L if unknown. + * + * @return the last modified time in Epoch millis. */ - @Override - public String toString() { - return "jar:file:" + getPath(); + public long getLastModifiedTimeMillis() { + // If lastModifiedTimeMillis is zero, but there is an MSDOS date and time available + if (lastModifiedTimeMillis == 0L && (lastModifiedDateMSDOS != 0 || lastModifiedTimeMSDOS != 0)) { + // Convert from MS-DOS Date & Time Format to Epoch millis + final int lastModifiedSecond = (lastModifiedTimeMSDOS & 0b11111) * 2; + final int lastModifiedMinute = lastModifiedTimeMSDOS >> 5 & 0b111111; + final int lastModifiedHour = lastModifiedTimeMSDOS >> 11; + final int lastModifiedDay = lastModifiedDateMSDOS & 0b11111; + final int lastModifiedMonth = (lastModifiedDateMSDOS >> 5 & 0b111) - 1; + final int lastModifiedYear = (lastModifiedDateMSDOS >> 9) + 1980; + + final Calendar lastModifiedCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + lastModifiedCalendar.set(lastModifiedYear, lastModifiedMonth, lastModifiedDay, lastModifiedHour, + lastModifiedMinute, lastModifiedSecond); + lastModifiedCalendar.set(Calendar.MILLISECOND, 0); + + // Cache converted time by overwriting the zero lastModifiedTimeMillis field + lastModifiedTimeMillis = lastModifiedCalendar.getTimeInMillis(); + } + + // Return the last modified time, or 0L if it is totally unknown. + return lastModifiedTimeMillis; } /** @@ -634,6 +275,14 @@ public int compareTo(final FastZipEntry o) { return diff3 < 0L ? -1 : diff3 > 0L ? 1 : 0; } + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return parentLogicalZipFile.hashCode() ^ version ^ entryName.hashCode() ^ (int) locHeaderPos; + } + /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @@ -641,8 +290,7 @@ public int compareTo(final FastZipEntry o) { public boolean equals(final Object obj) { if (this == obj) { return true; - } - if (!(obj instanceof FastZipEntry)) { + } else if (!(obj instanceof FastZipEntry)) { return false; } final FastZipEntry other = (FastZipEntry) obj; @@ -650,10 +298,10 @@ public boolean equals(final Object obj) { } /* (non-Javadoc) - * @see java.lang.Object#hashCode() + * @see java.lang.Object#toString() */ @Override - public int hashCode() { - return parentLogicalZipFile.hashCode() ^ version ^ entryName.hashCode() ^ (int) locHeaderPos; + public String toString() { + return "jar:file:" + getPath(); } -} \ No newline at end of file +} diff --git a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/LogicalZipFile.java b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/LogicalZipFile.java index f543df77f..007224d6f 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/LogicalZipFile.java +++ b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/LogicalZipFile.java @@ -44,17 +44,18 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import io.github.classgraph.ClassGraphException; -import nonapi.io.github.classgraph.recycler.RecycleOnClose; +import nonapi.io.github.classgraph.fileslice.ArraySlice; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessReader; +import nonapi.io.github.classgraph.utils.CollectionUtils; import nonapi.io.github.classgraph.utils.FileUtils; -import nonapi.io.github.classgraph.utils.Join; import nonapi.io.github.classgraph.utils.LogNode; +import nonapi.io.github.classgraph.utils.StringUtils; import nonapi.io.github.classgraph.utils.VersionFinder; /** * A logical zipfile, which represents a zipfile contained within a ZipFileSlice of a PhysicalZipFile. */ -public class LogicalZipFile extends ZipFileSlice implements AutoCloseable { +public class LogicalZipFile extends ZipFileSlice { /** The zipfile entries. */ public List entries; @@ -67,6 +68,9 @@ public class LogicalZipFile extends ZipFileSlice implements AutoCloseable { /** The value of the "Class-Path" manifest entry, if present in the manifest, else null. */ public String classPathManifestEntryValue; + /** The value of the "Bundle-ClassPath" manifest entry, if present in the manifest, else null. */ + public String bundleClassPathManifestEntryValue; + /** The value of the "Add-Exports" manifest entry, if present in the manifest, else null. */ public String addExportsManifestEntryValue; @@ -79,6 +83,9 @@ public class LogicalZipFile extends ZipFileSlice implements AutoCloseable { /** If true, this is a JRE jar. */ public boolean isJREJar; + /** If true, multi-release versions should not be stripped in resource names. */ + private final boolean enableMultiReleaseVersions; + // ------------------------------------------------------------------------------------------------------------- /** {@code "META_INF/"}. */ @@ -88,7 +95,7 @@ public class LogicalZipFile extends ZipFileSlice implements AutoCloseable { private static final String MANIFEST_PATH = META_INF_PATH_PREFIX + "MANIFEST.MF"; /** {@code "META-INF/versions/"}. */ - static final String MULTI_RELEASE_PATH_PREFIX = META_INF_PATH_PREFIX + "versions/"; + public static final String MULTI_RELEASE_PATH_PREFIX = META_INF_PATH_PREFIX + "versions/"; /** The {@code "Implementation-Title"} manifest key. */ private static final byte[] IMPLEMENTATION_TITLE_KEY = manifestKeyToBytes("Implementation-Title"); @@ -99,6 +106,9 @@ public class LogicalZipFile extends ZipFileSlice implements AutoCloseable { /** The {@code "Class-Path"} manifest key. */ private static final byte[] CLASS_PATH_KEY = manifestKeyToBytes("Class-Path"); + /** The {@code "Bundle-ClassPath"} manifest key. */ + private static final byte[] BUNDLE_CLASSPATH_KEY = manifestKeyToBytes("Bundle-ClassPath"); + /** The {@code "Spring-Boot-Classes"} manifest key. */ private static final byte[] SPRING_BOOT_CLASSES_KEY = manifestKeyToBytes("Spring-Boot-Classes"); @@ -132,6 +142,8 @@ public class LogicalZipFile extends ZipFileSlice implements AutoCloseable { * * @param zipFileSlice * the zipfile slice + * @param nestedJarHandler + * the nested jar handler * @param log * the log * @throws IOException @@ -139,12 +151,11 @@ public class LogicalZipFile extends ZipFileSlice implements AutoCloseable { * @throws InterruptedException * if the thread was interrupted. */ - LogicalZipFile(final ZipFileSlice zipFileSlice, final LogNode log) throws IOException, InterruptedException { + LogicalZipFile(final ZipFileSlice zipFileSlice, final NestedJarHandler nestedJarHandler, final LogNode log, + final boolean enableMultiReleaseVersions) throws IOException, InterruptedException { super(zipFileSlice); - try (RecycleOnClose zipFileSliceReaderRecycleOnClose = // - zipFileSliceReaderRecycler.acquireRecycleOnClose()) { - readCentralDirectory(zipFileSliceReaderRecycleOnClose.get(), log); - } + this.enableMultiReleaseVersions = enableMultiReleaseVersions; + readCentralDirectory(nestedJarHandler, log); } // ------------------------------------------------------------------------------------------------------------- @@ -217,7 +228,7 @@ private static Entry getManifestValue(final byte[] manifest, fi val = buf.toString("UTF-8"); } catch (final UnsupportedEncodingException e) { // Should not happen - throw ClassGraphException.newClassGraphException("UTF-8 encoding unsupported", e); + throw new RuntimeException("UTF-8 encoding is not supported in your JRE", e); } } return new SimpleEntry<>(val.endsWith(" ") ? val.trim() : val, curr); @@ -277,7 +288,7 @@ private static boolean keyMatchesAtPosition(final byte[] manifest, final byte[] private void parseManifest(final FastZipEntry manifestZipEntry, final LogNode log) throws IOException, InterruptedException { // Load contents of manifest entry as a byte array - final byte[] manifest = manifestZipEntry.load(); + final byte[] manifest = manifestZipEntry.getSlice().load(); // Find field keys (separated by newlines) for (int i = 0; i < manifest.length;) { @@ -313,6 +324,16 @@ private void parseManifest(final FastZipEntry manifestZipEntry, final LogNode lo } i = manifestValueAndEndIdx.getValue(); + } else if (keyMatchesAtPosition(manifest, BUNDLE_CLASSPATH_KEY, i)) { + final Entry manifestValueAndEndIdx = getManifestValue(manifest, + i + BUNDLE_CLASSPATH_KEY.length + 1); + // Add Bundle-ClassPath manifest entry values to classpath + bundleClassPathManifestEntryValue = manifestValueAndEndIdx.getKey(); + if (log != null) { + log.log("Found Bundle-ClassPath entry in manifest file: " + bundleClassPathManifestEntryValue); + } + i = manifestValueAndEndIdx.getValue(); + } else if (keyMatchesAtPosition(manifest, SPRING_BOOT_CLASSES_KEY, i)) { final Entry manifestValueAndEndIdx = getManifestValue(manifest, i + SPRING_BOOT_CLASSES_KEY.length + 1); @@ -377,6 +398,7 @@ private void parseManifest(final FastZipEntry manifestZipEntry, final LogNode lo i = manifestValueAndEndIdx.getValue(); } else { + // Key name was unrecognized -- skip to next key skip = true; } @@ -405,9 +427,9 @@ private void parseManifest(final FastZipEntry manifestZipEntry, final LogNode lo /** * Read the central directory of the zipfile. - * - * @param zipFileSliceReader - * the zipfile slice reader + * + * @param nestedJarHandler + * the nested jar handler * @param log * the log * @throws IOException @@ -415,72 +437,104 @@ private void parseManifest(final FastZipEntry manifestZipEntry, final LogNode lo * @throws InterruptedException * if the thread was interrupted. */ - private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, final LogNode log) + @SuppressWarnings("resource") + private void readCentralDirectory(final NestedJarHandler nestedJarHandler, final LogNode log) throws IOException, InterruptedException { - // Scan for End Of Central Directory (EOCD) signature + if (slice.sliceLength < 22) { + throw new IOException("Zipfile too short to have a central directory"); + } + + final RandomAccessReader reader = slice.randomAccessReader(); + + // Scan for End Of Central Directory (EOCD) signature. Final comment can be up to 64kB in length, + // so need to scan back that far to determine if this is a valid zipfile. However for speed, + // initially just try reading back a maximum of 32 characters. long eocdPos = -1; - for (long i = len - 22; i >= 0; --i) { - if (zipFileSliceReader.getInt(i) == 0x06054b50) { + for (long i = slice.sliceLength - 22, iMin = slice.sliceLength - 22 - 32; i >= iMin && i >= 0L; --i) { + if (reader.readUnsignedInt(i) == 0x06054b50L) { eocdPos = i; break; } } + if (eocdPos < 0 && slice.sliceLength > 22 + 32) { + // If EOCD signature was not found, read the last 64kB of file to RAM in a single chunk + // so that we can scan back through it at higher speed to locate the EOCD signature + final int bytesToRead = (int) Math.min(slice.sliceLength, 65536); + final byte[] eocdBytes = new byte[bytesToRead]; + final long readStartOff = slice.sliceLength - bytesToRead; + if (reader.read(readStartOff, eocdBytes, 0, bytesToRead) < bytesToRead) { + // Should not happen + throw new IOException("Zipfile is truncated"); + } + try (final ArraySlice arraySlice = new ArraySlice(eocdBytes, /* isDeflatedZipEntry = */ false, + /* inflatedLengthHint = */ 0L, nestedJarHandler)) { + final RandomAccessReader eocdReader = arraySlice.randomAccessReader(); + for (long i = eocdBytes.length - 22L; i >= 0L; --i) { + if (eocdReader.readUnsignedInt(i) == 0x06054b50L) { + eocdPos = i + readStartOff; + break; + } + } + } + } if (eocdPos < 0) { throw new IOException("Jarfile central directory signature not found: " + getPath()); } - long numEnt = zipFileSliceReader.getShort(eocdPos + 8); - if (zipFileSliceReader.getShort(eocdPos + 4) > 0 || zipFileSliceReader.getShort(eocdPos + 6) > 0 - || numEnt != zipFileSliceReader.getShort(eocdPos + 10)) { + long numEnt = reader.readUnsignedShort(eocdPos + 8); + if (reader.readUnsignedShort(eocdPos + 4) > 0 || reader.readUnsignedShort(eocdPos + 6) > 0 + || numEnt != reader.readUnsignedShort(eocdPos + 10)) { throw new IOException("Multi-disk jarfiles not supported: " + getPath()); } - long cenSize = zipFileSliceReader.getInt(eocdPos + 12); - if (cenSize > eocdPos) { - throw new IOException( - "Central directory size out of range: " + cenSize + " vs. " + eocdPos + ": " + getPath()); - } - long cenOff = zipFileSliceReader.getInt(eocdPos + 16); + long cenSize = reader.readUnsignedInt(eocdPos + 12); + long cenOff = reader.readUnsignedInt(eocdPos + 16); long cenPos = eocdPos - cenSize; // Check for Zip64 End Of Central Directory Locator record final long zip64cdLocIdx = eocdPos - 20; - if (zip64cdLocIdx >= 0 && zipFileSliceReader.getInt(zip64cdLocIdx) == 0x07064b50) { - if (zipFileSliceReader.getInt(zip64cdLocIdx + 4) > 0 - || zipFileSliceReader.getInt(zip64cdLocIdx + 16) > 1) { + if (zip64cdLocIdx >= 0 && reader.readUnsignedInt(zip64cdLocIdx) == 0x07064b50L) { + if (reader.readUnsignedInt(zip64cdLocIdx + 4) > 0 || reader.readUnsignedInt(zip64cdLocIdx + 16) > 1) { throw new IOException("Multi-disk jarfiles not supported: " + getPath()); } - final long eocdPos64 = zipFileSliceReader.getLong(zip64cdLocIdx + 8); - if (zipFileSliceReader.getInt(eocdPos64) != 0x06064b50) { + final long eocdPos64 = reader.readLong(zip64cdLocIdx + 8); + if (reader.readUnsignedInt(eocdPos64) != 0x06064b50L) { throw new IOException("Zip64 central directory at location " + eocdPos64 + " does not have Zip64 central directory header: " + getPath()); } - final long numEnt64 = zipFileSliceReader.getLong(eocdPos64 + 24); - if (zipFileSliceReader.getInt(eocdPos64 + 16) > 0 || zipFileSliceReader.getInt(eocdPos64 + 20) > 0 - || numEnt64 != zipFileSliceReader.getLong(eocdPos64 + 32)) { + final long numEnt64 = reader.readLong(eocdPos64 + 24); + if (reader.readUnsignedInt(eocdPos64 + 16) > 0 || reader.readUnsignedInt(eocdPos64 + 20) > 0 + || numEnt64 != reader.readLong(eocdPos64 + 32)) { throw new IOException("Multi-disk jarfiles not supported: " + getPath()); } - if (numEnt != numEnt64 && numEnt != 0xffff) { + if (numEnt == 0xffff) { + numEnt = numEnt64; + } else if (numEnt != numEnt64) { // Entry size mismatch -- trigger manual counting of entries numEnt = -1L; - } else { - numEnt = numEnt64; } - final long cenSize64 = zipFileSliceReader.getLong(eocdPos64 + 40); - if (cenSize != cenSize64 && cenSize != 0xffffffff) { + final long cenSize64 = reader.readLong(eocdPos64 + 40); + if (cenSize == 0xffffffffL) { + cenSize = cenSize64; + } else if (cenSize != cenSize64) { throw new IOException( "Mismatch in central directory size: " + cenSize + " vs. " + cenSize64 + ": " + getPath()); } - cenSize = cenSize64; // Recalculate the central directory position cenPos = eocdPos64 - cenSize; - final long cenOff64 = zipFileSliceReader.getLong(eocdPos64 + 48); - if (cenOff != cenOff64 && cenOff != 0xffffffff) { + final long cenOff64 = reader.readLong(eocdPos64 + 48); + if (cenOff == 0xffffffffL) { + cenOff = cenOff64; + } else if (cenOff != cenOff64) { throw new IOException( "Mismatch in central directory offset: " + cenOff + " vs. " + cenOff64 + ": " + getPath()); } - cenOff = cenOff64; + } + + if (cenSize > eocdPos) { + throw new IOException( + "Central directory size out of range: " + cenSize + " vs. " + eocdPos + ": " + getPath()); } // Get offset of first local file header @@ -491,27 +545,39 @@ private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, f // Read entries into a byte array, if central directory is smaller than 2GB. If central directory // is larger than 2GB, need to read each entry field from the file directly using ZipFileSliceReader. - final byte[] entryBytes = cenSize > FileUtils.MAX_BUFFER_SIZE ? null : new byte[(int) cenSize]; - if (entryBytes != null) { - zipFileSliceReader.read(cenPos, entryBytes, 0, (int) cenSize); + RandomAccessReader cenReader; + if (cenSize > FileUtils.MAX_BUFFER_SIZE) { + // Create a slice that covers the central directory (this allows a central directory larger than + // 2GB to be accessed using the slower FileSlice API, which reads the file directly, but also + // the slice can be accessed without adding cenPos to each read offset, so that this slice or + // the slice in the "else" clause below are accessed with the same index, which is the offset + // from the start of the central directory). + cenReader = slice.slice(cenPos, cenSize, /* isDeflatedZipEntry = */ false, /* inflatedSizeHint = */ 0L) + .randomAccessReader(); + } else { + // Read the central directory into RAM for speed, then wrap it in an ArraySlice + // (random access is faster for ArraySlice than for FileSlice) + final byte[] entryBytes = new byte[(int) cenSize]; + if (reader.read(cenPos, entryBytes, 0, (int) cenSize) < cenSize) { + // Should not happen + throw new IOException("Zipfile is truncated"); + } + cenReader = new ArraySlice(entryBytes, /* isDeflatedZipEntry = */ false, /* inflatedSizeHint = */ 0L, + nestedJarHandler).randomAccessReader(); } if (numEnt == -1L) { // numEnt and numEnt64 were inconsistent -- manually count entries numEnt = 0; for (long entOff = 0; entOff + 46 <= cenSize;) { - final int sig = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff) - : zipFileSliceReader.getInt(cenPos + entOff); - if (sig != 0x02014b50) { - throw new IOException("Invalid central directory signature: 0x" + Integer.toString(sig, 16) - + ": " + getPath()); - } - final int filenameLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 28) - : zipFileSliceReader.getShort(cenPos + entOff + 28); - final int extraFieldLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 30) - : zipFileSliceReader.getShort(cenPos + entOff + 30); - final int commentLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 32) - : zipFileSliceReader.getShort(cenPos + entOff + 32); + final long sig = cenReader.readUnsignedInt(entOff); + if (sig != 0x02014b50L) { + throw new IOException("Invalid central directory signature: 0x" + + Integer.toString((int) sig, 16) + ": " + getPath()); + } + final int filenameLen = cenReader.readUnsignedShort(entOff + 28); + final int extraFieldLen = cenReader.readUnsignedShort(entOff + 30); + final int commentLen = cenReader.readUnsignedShort(entOff + 32); entOff += 46 + filenameLen + extraFieldLen + commentLen; numEnt++; } @@ -524,10 +590,10 @@ private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, f } // Make sure there's no DoS attack vector by using a fake number of entries - if (entryBytes != null && numEnt > entryBytes.length / 46) { + if (numEnt > cenSize / 46) { // The smallest directory entry is 46 bytes in size - throw new IOException("Too many zipfile entries: " + numEnt + " (expected a max of " - + entryBytes.length / 46 + " based on central directory size)"); + throw new IOException("Too many zipfile entries: " + numEnt + " (expected a max of " + cenSize / 46 + + " based on central directory size)"); } // Enumerate entries @@ -536,18 +602,14 @@ private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, f try { int entSize = 0; for (long entOff = 0; entOff + 46 <= cenSize; entOff += entSize) { - final int sig = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff) - : zipFileSliceReader.getInt(cenPos + entOff); - if (sig != 0x02014b50) { - throw new IOException("Invalid central directory signature: 0x" + Integer.toString(sig, 16) - + ": " + getPath()); - } - final int filenameLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 28) - : zipFileSliceReader.getShort(cenPos + entOff + 28); - final int extraFieldLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 30) - : zipFileSliceReader.getShort(cenPos + entOff + 30); - final int commentLen = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 32) - : zipFileSliceReader.getShort(cenPos + entOff + 32); + final long sig = cenReader.readUnsignedInt(entOff); + if (sig != 0x02014b50L) { + throw new IOException("Invalid central directory signature: 0x" + + Integer.toString((int) sig, 16) + ": " + getPath()); + } + final int filenameLen = cenReader.readUnsignedShort(entOff + 28); + final int extraFieldLen = cenReader.readUnsignedShort(entOff + 30); + final int commentLen = cenReader.readUnsignedShort(entOff + 32); entSize = 46 + filenameLen + extraFieldLen + commentLen; // Get and sanitize entry name @@ -559,19 +621,16 @@ private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, f } break; } - final String entryName = entryBytes != null - ? ZipFileSliceReader.getString(entryBytes, filenameStartOff, filenameLen) - : zipFileSliceReader.getString(cenPos + filenameStartOff, filenameLen); - final String entryNameSanitized = FileUtils.sanitizeEntryPath(entryName, - /* removeInitialSlash = */ true); + final String entryName = cenReader.readString(filenameStartOff, filenameLen); + String entryNameSanitized = FileUtils.sanitizeEntryPath(entryName, /* removeInitialSlash = */ true, + /* removeFinalSlash = */ false); if (entryNameSanitized.isEmpty() || entryName.endsWith("/")) { // Skip directory entries continue; } // Check entry flag bits - final int flags = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, entOff + 8) - : zipFileSliceReader.getShort(cenPos + entOff + 8); + final int flags = cenReader.readUnsignedShort(entOff + 8); if ((flags & 1) != 0) { if (log != null) { log.log("Skipping encrypted zip entry: " + entryNameSanitized); @@ -580,9 +639,7 @@ private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, f } // Check compression method - final int compressionMethod = entryBytes != null - ? ZipFileSliceReader.getShort(entryBytes, entOff + 10) - : zipFileSliceReader.getShort(cenPos + entOff + 10); + final int compressionMethod = cenReader.readUnsignedShort(entOff + 10); if (compressionMethod != /* stored */ 0 && compressionMethod != /* deflated */ 8) { if (log != null) { log.log("Skipping zip entry with invalid compression method " + compressionMethod + ": " @@ -593,62 +650,132 @@ private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, f final boolean isDeflated = compressionMethod == /* deflated */ 8; // Get compressed and uncompressed size - long compressedSize = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff + 20) - : zipFileSliceReader.getInt(cenPos + entOff + 20); - long uncompressedSize = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff + 24) - : zipFileSliceReader.getInt(cenPos + entOff + 24); - long pos = entryBytes != null ? ZipFileSliceReader.getInt(entryBytes, entOff + 42) - : zipFileSliceReader.getInt(cenPos + entOff + 42); + long compressedSize = (cenReader.readUnsignedInt(entOff + 20)); + long uncompressedSize = (cenReader.readUnsignedInt(entOff + 24)); + + // Get external file attributes + final int fileAttributes = cenReader.readUnsignedShort(entOff + 40); + + long pos = cenReader.readUnsignedInt(entOff + 42); // Check for Zip64 header in extra fields + // See: + // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + // https://github.com/LuaDist/zip/blob/master/proginfo/extrafld.txt + long lastModifiedMillis = 0L; if (extraFieldLen > 0) { for (int extraFieldOff = 0; extraFieldOff + 4 < extraFieldLen;) { final long tagOff = filenameEndOff + extraFieldOff; - final int tag = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, tagOff) - : zipFileSliceReader.getShort(cenPos + tagOff); - final int size = entryBytes != null ? ZipFileSliceReader.getShort(entryBytes, tagOff + 2) - : zipFileSliceReader.getShort(cenPos + tagOff + 2); + final int tag = cenReader.readUnsignedShort(tagOff); + final int size = cenReader.readUnsignedShort(tagOff + 2); if (extraFieldOff + 4 + size > extraFieldLen) { // Invalid size + if (log != null) { + log.log("Skipping zip entry with invalid extra field size: " + entryNameSanitized); + } break; } - if (tag == /* EXTID_ZIP64 */ 1 && size >= 24) { - final long uncompressedSizeL = entryBytes != null - ? ZipFileSliceReader.getLong(entryBytes, tagOff + 4 + 0) - : zipFileSliceReader.getLong(cenPos + tagOff + 4 + 0); - if (uncompressedSize == 0xffffffff) { - uncompressedSize = uncompressedSizeL; + if (tag == 1 && size >= 20) { + // Zip64 extended information extra field + final long uncompressedSize64 = cenReader.readLong(tagOff + 4 + 0); + if (uncompressedSize == 0xffffffffL) { + uncompressedSize = uncompressedSize64; + } else if (uncompressedSize != uncompressedSize64) { + throw new IOException("Mismatch in uncompressed size: " + uncompressedSize + " vs. " + + uncompressedSize64 + ": " + entryNameSanitized); } - final long compressedSizeL = entryBytes != null - ? ZipFileSliceReader.getLong(entryBytes, tagOff + 4 + 8) - : zipFileSliceReader.getLong(cenPos + tagOff + 4 + 8); - if (compressedSize == 0xffffffff) { - compressedSize = compressedSizeL; + final long compressedSize64 = cenReader.readLong(tagOff + 4 + 8); + if (compressedSize == 0xffffffffL) { + compressedSize = compressedSize64; + } else if (compressedSize != compressedSize64) { + throw new IOException("Mismatch in compressed size: " + compressedSize + " vs. " + + compressedSize64 + ": " + entryNameSanitized); } - final long posL = entryBytes != null - ? ZipFileSliceReader.getLong(entryBytes, tagOff + 4 + 16) - : zipFileSliceReader.getLong(cenPos + tagOff + 4 + 16); - if (pos == 0xffffffff) { - pos = posL; + // Only compressed size and uncompressed size are required fields + if (size >= 28) { + final long pos64 = cenReader.readLong(tagOff + 4 + 16); + if (pos == 0xffffffffL) { + pos = pos64; + } else if (pos != pos64) { + throw new IOException("Mismatch in entry pos: " + pos + " vs. " + pos64 + ": " + + entryNameSanitized); + } } break; + + } else if (tag == 0x5455 && size >= 5) { + // Extended Unix timestamp + final int bits = cenReader.readUnsignedByte(tagOff + 4 + 0); + if ((bits & 1) == 1 && size >= 5 + 8) { + lastModifiedMillis = cenReader.readLong(tagOff + 4 + 1) * 1000L; + } + + } else if (tag == 0x5855 && size >= 20) { + // Unix extra field (deprecated) + lastModifiedMillis = cenReader.readLong(tagOff + 4 + 8) * 1000L; + // There are also optional UID and GID fields in this extra field (currently ignored) + + } else if (tag == 0x7855) { + // Info-ZIP Unix UID and GID fields (currently ignored) + + } else if (tag == 0x7075) { + // Info-ZIP Unicode path extra field + final int version = cenReader.readUnsignedByte(tagOff + 4 + 0); + if (version != 1) { + throw new IOException("Unknown Unicode entry name format " + version + + " in extra field: " + entryNameSanitized); + } else if (size > 9) { + // Replace non-Unicode entry name with Unicode version + try { + entryNameSanitized = cenReader.readString(tagOff + 9, size - 9); + } catch (final IllegalArgumentException e) { + throw new IOException("Malformed extended Unicode entry name for entry: " + + entryNameSanitized); + } + } } extraFieldOff += 4 + size; } } - if (compressedSize < 0 || pos < 0) { + int lastModifiedTimeMSDOS = 0; + int lastModifiedDateMSDOS = 0; + if (lastModifiedMillis == 0L) { + // If Unix timestamp was not provided, convert zip entry timestamp from MS-DOS format + lastModifiedTimeMSDOS = cenReader.readUnsignedShort(entOff + 12); + lastModifiedDateMSDOS = cenReader.readUnsignedShort(entOff + 14); + } + + if (compressedSize < 0) { + if (log != null) { + log.log("Skipping zip entry with invalid compressed size (" + compressedSize + "): " + + entryNameSanitized); + } + continue; + } + if (uncompressedSize < 0) { + if (log != null) { + log.log("Skipping zip entry with invalid uncompressed size (" + uncompressedSize + "): " + + entryNameSanitized); + } + continue; + } + if (pos < 0) { + if (log != null) { + log.log("Skipping zip entry with invalid pos (" + pos + "): " + entryNameSanitized); + } continue; } final long locHeaderPos = locPos + pos; if (locHeaderPos < 0) { if (log != null) { - log.log("Skipping zip entry with invalid loc header position: " + entryNameSanitized); + log.log("Skipping zip entry with invalid loc header position (" + locHeaderPos + "): " + + entryNameSanitized); } continue; } - if (locHeaderPos + 4 >= len) { + if (locHeaderPos + 4 >= slice.sliceLength) { if (log != null) { log.log("Unexpected EOF when trying to read LOC header: " + entryNameSanitized); } @@ -657,7 +784,8 @@ private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, f // Add zip entry final FastZipEntry entry = new FastZipEntry(this, locHeaderPos, entryNameSanitized, isDeflated, - compressedSize, uncompressedSize, physicalZipFile.nestedJarHandler); + compressedSize, uncompressedSize, lastModifiedMillis, lastModifiedTimeMSDOS, + lastModifiedDateMSDOS, fileAttributes, enableMultiReleaseVersions); entries.add(entry); // Record manifest entry @@ -696,12 +824,13 @@ private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, f } } final List versionsFoundSorted = new ArrayList<>(versionsFound); - Collections.sort(versionsFoundSorted); - log.log("This is a multi-release jar, with versions: " + Join.join(", ", versionsFoundSorted)); + CollectionUtils.sortIfNotEmpty(versionsFoundSorted); + log.log("This is a multi-release jar, with versions: " + + StringUtils.join(", ", versionsFoundSorted)); } // Sort in decreasing order of version in preparation for version masking - Collections.sort(entries); + CollectionUtils.sortIfNotEmpty(entries); // Mask files that appear in multiple version sections, so that there is only one entry // for each unversioned path, i.e. the versioned path with the highest version number @@ -727,17 +856,11 @@ private void readCentralDirectory(final ZipFileSliceReader zipFileSliceReader, f // ------------------------------------------------------------------------------------------------------------- - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.fastzipfilereader.ZipFileSlice#equals(java.lang.Object) - */ @Override public boolean equals(final Object o) { return super.equals(o); } - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.fastzipfilereader.ZipFileSlice#hashCode() - */ @Override public int hashCode() { return super.hashCode(); @@ -750,18 +873,4 @@ public int hashCode() { public String toString() { return getPath(); } - - /* (non-Javadoc) - * @see java.lang.AutoCloseable#close() - */ - @Override - public void close() { - if (zipFileSliceReaderRecycler != null) { - zipFileSliceReaderRecycler.close(); - } - if (entries != null) { - entries.clear(); - entries = null; - } - } } diff --git a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/NestedJarHandler.java b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/NestedJarHandler.java index c54be5c34..2bad6e807 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/NestedJarHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/NestedJarHandler.java @@ -28,39 +28,63 @@ */ package nonapi.io.github.classgraph.fastzipfilereader; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.RandomAccessFile; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; +import java.net.URLConnection; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemNotFoundException; import java.nio.file.Files; -import java.nio.file.StandardCopyOption; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Map.Entry; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.DataFormatException; import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipException; -import io.github.classgraph.ClassGraphException; import io.github.classgraph.ModuleReaderProxy; import io.github.classgraph.ModuleRef; import io.github.classgraph.ScanResult; -import nonapi.io.github.classgraph.ScanSpec; import nonapi.io.github.classgraph.concurrency.InterruptionChecker; import nonapi.io.github.classgraph.concurrency.SingletonMap; +import nonapi.io.github.classgraph.fileslice.ArraySlice; +import nonapi.io.github.classgraph.fileslice.FileSlice; +import nonapi.io.github.classgraph.fileslice.Slice; import nonapi.io.github.classgraph.recycler.Recycler; +import nonapi.io.github.classgraph.recycler.Resettable; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; import nonapi.io.github.classgraph.utils.FastPathResolver; import nonapi.io.github.classgraph.utils.FileUtils; +import nonapi.io.github.classgraph.utils.JarUtils; import nonapi.io.github.classgraph.utils.LogNode; /** Open and read jarfiles, which may be nested within other jarfiles. */ public class NestedJarHandler { /** The {@link ScanSpec}. */ - private final ScanSpec scanSpec; + public final ScanSpec scanSpec; + + public ReflectionUtils reflectionUtils; /** * A singleton map from a zipfile's {@link File} to the {@link PhysicalZipFile} for that file, used to ensure @@ -70,17 +94,10 @@ public class NestedJarHandler { canonicalFileToPhysicalZipFileMap = new SingletonMap() { @Override public PhysicalZipFile newInstance(final File canonicalFile, final LogNode log) throws IOException { - if (closed.get()) { - throw ClassGraphException - .newClassGraphException(NestedJarHandler.class.getSimpleName() + " already closed"); - } - return new PhysicalZipFile(canonicalFile, NestedJarHandler.this); + return new PhysicalZipFile(canonicalFile, NestedJarHandler.this, log); } }; - /** {@link PhysicalZipFile} instances created to extract nested jarfiles to disk or RAM. */ - private Queue additionalAllocatedPhysicalZipFiles = new ConcurrentLinkedQueue<>(); - /** * A singleton map from a {@link FastZipEntry} to the {@link ZipFileSlice} wrapping either the zip entry data, * if the entry is stored, or a ByteBuffer, if the zip entry was inflated to memory, or a physical file on disk @@ -93,184 +110,105 @@ public ZipFileSlice newInstance(final FastZipEntry childZipEntry, final LogNode throws IOException, InterruptedException { ZipFileSlice childZipEntrySlice; if (!childZipEntry.isDeflated) { - // Wrap the child entry (a stored nested zipfile) in a new ZipFileSlice -- there is - // nothing else to do. (Most nested zipfiles are stored, not deflated, so this fast - // path will be followed most often.) + // The child zip entry is a stored nested zipfile -- wrap it in a new + // ZipFileSlice. + // Hopefully nested zipfiles are stored, not deflated, as this is the fast path. childZipEntrySlice = new ZipFileSlice(childZipEntry); } else { // If child entry is deflated i.e. (for a deflated nested zipfile), must inflate // the contents of the entry before its central directory can be read (most of // the time nested zipfiles are stored, not deflated, so this should be rare) - if ((childZipEntry.uncompressedSize < 0L - || childZipEntry.uncompressedSize >= INFLATE_TO_DISK_THRESHOLD - // Also check compressed size for safety, in case uncompressed size is wrong - || childZipEntry.compressedSize >= INFLATE_TO_DISK_THRESHOLD)) { - // If child entry's size is unknown or the file is large, inflate to disk - File tempFile = null; - try { - // Create temp file - tempFile = makeTempFile(childZipEntry.entryName, /* onlyUseLeafname = */ true); - - // Inflate zip entry to temp file - if (log != null) { - log.log("Deflating zip entry to temporary file: " + childZipEntry - + " ; uncompressed size: " + childZipEntry.uncompressedSize + " ; temp file: " - + tempFile); - } - try (InputStream inputStream = childZipEntry.open()) { - Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - - // Get or create a PhysicalZipFile instance for the new temp file - PhysicalZipFile physicalZipFile; - try { - physicalZipFile = canonicalFileToPhysicalZipFileMap.get(tempFile, log); - } catch (final NullSingletonException e) { - throw new IOException("Could not get physical zipfile " + tempFile + " : " + e); - } - additionalAllocatedPhysicalZipFiles.add(physicalZipFile); - - // Create a new logical slice of the whole physical zipfile - childZipEntrySlice = new ZipFileSlice(physicalZipFile); - - } catch (final IllegalArgumentException | IOException e) { - // Could not make temp file, or failed to extract entire contents of entry - if (log != null) { - log.log("Deflating zip entry to temporary file failed: " + e); - } - if (tempFile != null) { - // Delete temp file, in case it contains partially-extracted data - // due to running out of disk space - try { - Files.delete(tempFile.toPath()); - } catch (final IOException | SecurityException e2) { - if (log != null) { - log.log("Removing temporary file failed: " + e2); - } - } - } - childZipEntrySlice = null; - } - } else { - childZipEntrySlice = null; + if (log != null) { + log.log("Inflating nested zip entry: " + childZipEntry + " ; uncompressed size: " + + childZipEntry.uncompressedSize); } - if (childZipEntrySlice == null) { - // If the uncompressed size known and small, or inflating to temp file failed, - // inflate to a ByteBuffer in memory instead - if (childZipEntry.uncompressedSize > FileUtils.MAX_BUFFER_SIZE) { - // Impose 2GB limit (i.e. a max of one ByteBuffer chunk) on inflation to memory - throw new IOException("Uncompressed size of zip entry (" + childZipEntry.uncompressedSize - + ") is too large to inflate to memory: " + childZipEntry.entryName); - } - // Open the zip entry to fetch inflated data, and read the whole contents of the - // InputStream to a byte[] array, then wrap it in a ByteBuffer - if (log != null) { - log.log("Deflating zip entry to RAM: " + childZipEntry + " ; uncompressed size: " - + childZipEntry.uncompressedSize); - } - ByteBuffer byteBuffer; - try (InputStream inputStream = childZipEntry.open()) { - byteBuffer = ByteBuffer - .wrap(FileUtils.readAllBytesAsArray(inputStream, childZipEntry.uncompressedSize)); - } - - // Create a new PhysicalZipFile that wraps the ByteBuffer as if the buffer had been - // mmap'd to a file on disk - final PhysicalZipFile physicalZipFileInRam = new PhysicalZipFile(byteBuffer, - /* outermostFile = */ childZipEntry.parentLogicalZipFile.physicalZipFile.getFile(), - childZipEntry.getPath(), NestedJarHandler.this); - additionalAllocatedPhysicalZipFiles.add(physicalZipFileInRam); + // Read the InputStream for the child zip entry to a RAM buffer, or spill to + // disk if it's too large + final PhysicalZipFile physicalZipFile = new PhysicalZipFile(childZipEntry.getSlice().open(), + childZipEntry.uncompressedSize >= 0L + && childZipEntry.uncompressedSize <= FileUtils.MAX_BUFFER_SIZE + ? (int) childZipEntry.uncompressedSize + : -1, + childZipEntry.entryName, NestedJarHandler.this, log); - // Create a new logical slice of the whole physical in-memory zipfile - childZipEntrySlice = new ZipFileSlice(physicalZipFileInRam, childZipEntry); - } + // Create a new logical slice of the extracted inner zipfile + childZipEntrySlice = new ZipFileSlice(physicalZipFile, childZipEntry); } return childZipEntrySlice; } }; - /** A singleton map from a {@link ZipFileSlice} to the {@link LogicalZipFile} for that slice. */ + /** + * A singleton map from a {@link ZipFileSlice} to the {@link LogicalZipFile} for that slice. + */ private SingletonMap // zipFileSliceToLogicalZipFileMap = new SingletonMap() { @Override public LogicalZipFile newInstance(final ZipFileSlice zipFileSlice, final LogNode log) throws IOException, InterruptedException { - if (closed.get()) { - throw ClassGraphException - .newClassGraphException(NestedJarHandler.class.getSimpleName() + " already closed"); - } - // Read the central directory for the logical zipfile slice - final LogicalZipFile logicalZipFile = new LogicalZipFile(zipFileSlice, log); - allocatedLogicalZipFiles.add(logicalZipFile); - return logicalZipFile; + // Read the central directory for the zipfile + return new LogicalZipFile(zipFileSlice, NestedJarHandler.this, log, + scanSpec.enableMultiReleaseVersions); } }; - /** All allocated LogicalZipFile instances. */ - private final Queue allocatedLogicalZipFiles = new ConcurrentLinkedQueue<>(); - /** * A singleton map from nested jarfile path to a tuple of the logical zipfile for the path, and the package root * within the logical zipfile. */ - public SingletonMap, IOException> // + public SingletonMap, IOException> // nestedPathToLogicalZipFileAndPackageRootMap = // new SingletonMap, IOException>() { @Override public Entry newInstance(final String nestedJarPathRaw, final LogNode log) throws IOException, InterruptedException { - if (closed.get()) { - throw ClassGraphException - .newClassGraphException(NestedJarHandler.class.getSimpleName() + " already closed"); - } final String nestedJarPath = FastPathResolver.resolve(nestedJarPathRaw); final int lastPlingIdx = nestedJarPath.lastIndexOf('!'); if (lastPlingIdx < 0) { - // nestedJarPath is a simple file path or URL (i.e. doesn't have any '!' sections). + // nestedJarPath is a simple file path or URL (i.e. doesn't have any '!' + // sections). // This is also the last frame of recursion for the 'else' clause below. - // If the path starts with "http(s)://", download the jar to a temp file - final boolean isRemote = nestedJarPath.startsWith("http://") - || nestedJarPath.startsWith("https://"); - File canonicalFile; - if (isRemote) { - // Jarfile is at http(s) URL - if (scanSpec.enableRemoteJarScanning) { - canonicalFile = downloadTempFile(nestedJarPath, log); - if (canonicalFile == null) { - throw new IOException("Could not download jarfile " + nestedJarPath); - } - } else { - throw new IOException( - "Remote jar scanning has not been enabled, cannot scan classpath element: " - + nestedJarPath); + // If the path starts with "http://" or "https://" or any other URI/URL scheme, + // download the jar to a temp file or to a ByteBuffer in RAM. ("jar:" and + // "file:" + // have already been stripped from any URL/URI.) + final boolean isURL = JarUtils.URL_SCHEME_PATTERN.matcher(nestedJarPath).matches(); + PhysicalZipFile physicalZipFile; + if (isURL) { + final String scheme = nestedJarPath.substring(0, nestedJarPath.indexOf(':')); + if (scanSpec.allowedURLSchemes == null + || !scanSpec.allowedURLSchemes.contains(scheme)) { + // No URL schemes other than "file:" (with optional "jar:" prefix) allowed + // (these + // schemes were already stripped by FastPathResolver.resolve(nestedJarPathRaw)) + throw new IOException("Scanning of URL scheme \"" + scheme + + "\" has not been enabled -- cannot scan classpath element: " + + nestedJarPath); } + + // Download jar from URL to a ByteBuffer in RAM, or to a temp file on disk + physicalZipFile = downloadJarFromURL(nestedJarPath, log); + } else { - // Jarfile should be local + // Jarfile should be a local file -- wrap in a PhysicalZipFile instance try { - canonicalFile = new File(nestedJarPath).getCanonicalFile(); + // Get canonical file + final File canonicalFile = new File(nestedJarPath).getCanonicalFile(); + // Get or create a PhysicalZipFile instance for the canonical file + physicalZipFile = canonicalFileToPhysicalZipFileMap.get(canonicalFile, log); + } catch (final NullSingletonException | NewInstanceException e) { + // If getting PhysicalZipFile failed, re-wrap in IOException + throw new IOException("Could not get PhysicalZipFile for path " + nestedJarPath + + " : " + (e.getCause() == null ? e : e.getCause())); } catch (final SecurityException e) { + // getCanonicalFile() failed (it may have also failed with IOException) throw new IOException( "Path component " + nestedJarPath + " could not be canonicalized: " + e); } } - if (!FileUtils.canRead(canonicalFile)) { - throw new IOException("Path component " + nestedJarPath + " does not exist"); - } - if (!canonicalFile.isFile()) { - throw new IOException( - "Path component " + nestedJarPath + " is not a file (expected a jarfile)"); - } - - // Get or create a PhysicalZipFile instance for the canonical file - PhysicalZipFile physicalZipFile; - try { - physicalZipFile = canonicalFileToPhysicalZipFileMap.get(canonicalFile, log); - } catch (final NullSingletonException e) { - throw new IOException("Could not get physical zipfile " + canonicalFile + " : " + e); - } // Create a new logical slice of the whole physical zipfile final ZipFileSlice topLevelSlice = new ZipFileSlice(physicalZipFile); @@ -279,6 +217,8 @@ public Entry newInstance(final String nestedJarPathRaw, logicalZipFile = zipFileSliceToLogicalZipFileMap.get(topLevelSlice, log); } catch (final NullSingletonException e) { throw new IOException("Could not get toplevel slice " + topLevelSlice + " : " + e); + } catch (final NewInstanceException e) { + throw new IOException("Could not get toplevel slice " + topLevelSlice, e); } // Return new logical zipfile with an empty package root @@ -289,11 +229,15 @@ public Entry newInstance(final String nestedJarPathRaw, final String parentPath = nestedJarPath.substring(0, lastPlingIdx); String childPath = nestedJarPath.substring(lastPlingIdx + 1); // "file.jar!/path" -> "file.jar!path" - childPath = FileUtils.sanitizeEntryPath(childPath, /* removeInitialSlash = */ true); + childPath = FileUtils.sanitizeEntryPath(childPath, /* removeInitialSlash = */ true, + /* removeFinalSlash = */ true); - // Recursively remove one '!' section at a time, back towards the beginning of the URL or - // file path. At the last frame of recursion, the toplevel jarfile will be reached and - // returned. The recursion is guaranteed to terminate because parentPath gets one + // Recursively remove one '!' section at a time, back towards the beginning of + // the URL or + // file path. At the last frame of recursion, the toplevel jarfile will be + // reached and + // returned. The recursion is guaranteed to terminate because parentPath gets + // one // '!'-section shorter with each recursion frame. Entry parentLogicalZipFileAndPackageRoot; try { @@ -301,24 +245,36 @@ public Entry newInstance(final String nestedJarPathRaw, .get(parentPath, log); } catch (final NullSingletonException e) { throw new IOException("Could not get parent logical zipfile " + parentPath + " : " + e); + } catch (final NewInstanceException e) { + throw new IOException("Could not get parent logical zipfile " + parentPath, e); } - // Only the last item in a '!'-delimited list can be a non-jar path, so the parent must + // Only the last item in a '!'-delimited list can be a non-jar path, so the + // parent must // always be a jarfile. final LogicalZipFile parentLogicalZipFile = parentLogicalZipFileAndPackageRoot.getKey(); // Look up the child path within the parent zipfile boolean isDirectory = false; while (childPath.endsWith("/")) { - // Child path is definitely a directory, it ends with a slash + // Child path is definitely a directory, it ends with a slash isDirectory = true; childPath = childPath.substring(0, childPath.length() - 1); } FastZipEntry childZipEntry = null; if (!isDirectory) { // If child path doesn't end with a slash, see if there's a non-directory entry - // with a name matching the child path (LogicalZipFile discards directory entries - // ending with a slash when reading the central directory of a zipfile) + // with a name matching the child path (LogicalZipFile discards directory + // entries + // ending with a slash when reading the central directory of a zipfile). + // N.B. We perform an O(N) search here because we assume the number of classpath + // elements containing "!" sections is relatively small compared to the total + // number + // of entries in all jarfiles (i.e. building a HashMap of entry path to entry + // for + // every jarfile would generally be more expensive than performing this linear + // search, and unless the classpath is enormous, the overall time performance + // will not tend towards O(N^2). for (final FastZipEntry entry : parentLogicalZipFile.entries) { if (entry.entryName.equals(childPath)) { childZipEntry = entry; @@ -327,7 +283,8 @@ public Entry newInstance(final String nestedJarPathRaw, } } if (childZipEntry == null) { - // If there is no non-directory zipfile entry with a name matching the child path, + // If there is no non-directory zipfile entry with a name matching the child + // path, // test to see if any entries in the zipfile have the child path as a dir prefix final String childPathPrefix = childPath + "/"; for (final FastZipEntry entry : parentLogicalZipFile.entries) { @@ -336,10 +293,6 @@ public Entry newInstance(final String nestedJarPathRaw, break; } } - if (!isDirectory) { - throw new IOException( - "Path " + childPath + " does not exist in jarfile " + parentLogicalZipFile); - } } // At this point, either isDirectory is true, or childZipEntry is non-null @@ -359,16 +312,25 @@ public Entry newInstance(final String nestedJarPathRaw, return new SimpleEntry<>(parentLogicalZipFile, childPath); } + if (childZipEntry == null /* i.e. if (!isDirectory) */) { + throw new IOException( + "Path " + childPath + " does not exist in jarfile " + parentLogicalZipFile); + } + // Do not extract nested jar, if nested jar scanning is disabled if (!scanSpec.scanNestedJars) { throw new IOException( "Nested jar scanning is disabled -- skipping nested jar " + nestedJarPath); } - // The child path corresponds to a non-directory zip entry, so it must be a nested jar - // (since non-jar nested files cannot be used on the classpath). Map the nested jar as - // a new ZipFileSlice if it is stored, or inflate it to RAM or to a temporary file if - // it is deflated, then create a new ZipFileSlice over the temporary file or ByteBuffer. + // The child path corresponds to a non-directory zip entry, so it must be a + // nested jar + // (since non-jar nested files cannot be used on the classpath). Map the nested + // jar as + // a new ZipFileSlice if it is stored, or inflate it to RAM or to a temporary + // file if + // it is deflated, then create a new ZipFileSlice over the temporary file or + // ByteBuffer. // Get zip entry as a ZipFileSlice, possibly inflating to disk or RAM @@ -378,6 +340,8 @@ public Entry newInstance(final String nestedJarPathRaw, } catch (final NullSingletonException e) { throw new IOException( "Could not get child zip entry slice " + childZipEntry + " : " + e); + } catch (final NewInstanceException e) { + throw new IOException("Could not get child zip entry slice " + childZipEntry, e); } final LogNode zipSliceLog = log == null ? null @@ -392,6 +356,8 @@ public Entry newInstance(final String nestedJarPathRaw, } catch (final NullSingletonException e) { throw new IOException( "Could not get child logical zipfile " + childZipEntrySlice + " : " + e); + } catch (final NewInstanceException e) { + throw new IOException("Could not get child logical zipfile " + childZipEntrySlice, e); } // Return new logical zipfile with an empty package root @@ -400,7 +366,9 @@ public Entry newInstance(final String nestedJarPathRaw, } }; - /** A singleton map from a {@link ModuleRef} to a {@link ModuleReaderProxy} recycler for the module. */ + /** + * A singleton map from a {@link ModuleRef} to a {@link ModuleReaderProxy} recycler for the module. + */ public SingletonMap, IOException> // moduleRefToModuleReaderProxyRecyclerMap = // new SingletonMap, IOException>() { @@ -410,10 +378,6 @@ public Recycler newInstance(final ModuleRef modu return new Recycler() { @Override public ModuleReaderProxy newInstance() throws IOException { - if (closed.get()) { - throw ClassGraphException.newClassGraphException( - NestedJarHandler.class.getSimpleName() + " already closed"); - } return moduleRef.open(); } }; @@ -421,36 +385,38 @@ public ModuleReaderProxy newInstance() throws IOException { }; /** A recycler for {@link Inflater} instances. */ - Recycler // + private Recycler // inflaterRecycler = new Recycler() { @Override public RecyclableInflater newInstance() throws RuntimeException { - if (closed.get()) { - throw ClassGraphException - .newClassGraphException(NestedJarHandler.class.getSimpleName() + " already closed"); - } return new RecyclableInflater(); } }; + /** {@link FileSlice} instances that are currently open. */ + private Set openSlices = Collections.newSetFromMap(new ConcurrentHashMap()); + /** Any temporary files created while scanning. */ - private ConcurrentLinkedDeque tempFiles = new ConcurrentLinkedDeque<>(); + private Set tempFiles = Collections.newSetFromMap(new ConcurrentHashMap()); /** The separator between random temp filename part and leafname. */ public static final String TEMP_FILENAME_LEAF_SEPARATOR = "---"; - /** - * The threshold uncompressed size at which nested deflated jars are inflated to a temporary file on disk, - * rather than to RAM. - */ - private static final int INFLATE_TO_DISK_THRESHOLD = 32 * 1024 * 1024; - /** True if {@link #close(LogNode)} has been called. */ private final AtomicBoolean closed = new AtomicBoolean(false); /** The interruption checker. */ public InterruptionChecker interruptionChecker; + /** The default size of a file buffer. */ + private static final int DEFAULT_BUFFER_SIZE = 16384; + + /** The maximum initial buffer size. */ + private static final int MAX_INITIAL_BUFFER_SIZE = 16 * 1024 * 1024; + + /** HTTP(S) timeout, ms. */ + private static final int HTTP_TIMEOUT = 5000; + // ------------------------------------------------------------------------------------------------------------- /** @@ -461,9 +427,11 @@ public RecyclableInflater newInstance() throws RuntimeException { * @param interruptionChecker * the interruption checker */ - public NestedJarHandler(final ScanSpec scanSpec, final InterruptionChecker interruptionChecker) { + public NestedJarHandler(final ScanSpec scanSpec, final InterruptionChecker interruptionChecker, + final ReflectionUtils reflectionUtils) { this.scanSpec = scanSpec; this.interruptionChecker = interruptionChecker; + this.reflectionUtils = reflectionUtils; } // ------------------------------------------------------------------------------------------------------------- @@ -494,7 +462,7 @@ private String sanitizeFilename(final String filename) { /** * Create a temporary file, and mark it for deletion on exit. * - * @param filePath + * @param filePathBase * The path to derive the temporary filename from. * @param onlyUseLeafname * If true, only use the leafname of filePath to derive the temporary filename. @@ -502,48 +470,558 @@ private String sanitizeFilename(final String filename) { * @throws IOException * If the temporary file could not be created. */ - private File makeTempFile(final String filePath, final boolean onlyUseLeafname) throws IOException { - final File tempFile = File.createTempFile("ClassGraph--", - TEMP_FILENAME_LEAF_SEPARATOR + sanitizeFilename(onlyUseLeafname ? leafname(filePath) : filePath)); + public File makeTempFile(final String filePathBase, final boolean onlyUseLeafname) throws IOException { + final File tempFile = File.createTempFile("ClassGraph--", TEMP_FILENAME_LEAF_SEPARATOR + + sanitizeFilename(onlyUseLeafname ? leafname(filePathBase) : filePathBase)); tempFile.deleteOnExit(); tempFiles.add(tempFile); return tempFile; } /** - * Download a jar from a URL to a temporary file. + * Attempt to remove a temporary file. + * + * @param tempFile + * the temp file + * @throws IOException + * If the temporary file could not be removed. + * @throws SecurityException + * If the temporary file is inaccessible. + */ + void removeTempFile(final File tempFile) throws IOException, SecurityException { + if (tempFiles.remove(tempFile)) { + Files.delete(tempFile.toPath()); + } else { + throw new IOException("Not a temp file: " + tempFile); + } + } + + /** + * Mark a {@link Slice} as open, so it can be closed when the {@link ScanResult} is closed. + * + * @param slice + * the {@link Slice} that was just opened. + * @throws IOException + * Signals that an I/O exception has occurred. + */ + public void markSliceAsOpen(final Slice slice) throws IOException { + openSlices.add(slice); + } + + /** + * Mark a {@link Slice} as closed. + * + * @param slice + * the {@link Slice} to close. + */ + public void markSliceAsClosed(final Slice slice) { + openSlices.remove(slice); + } + + /** + * Download a jar from a URL to a temporary file, or to a ByteBuffer if the temporary directory is not writeable + * or full. The downloaded jar is returned wrapped in a {@link PhysicalZipFile} instance. * * @param jarURL * the jar URL * @param log * the log - * @return the temporary file the jar was downloaded to + * @return the temporary file or {@link ByteBuffer} the jar was downloaded to, wrapped in a + * {@link PhysicalZipFile} instance. + * @throws IOException + * If the jar could not be downloaded, or the jar URL is malformed. + * @throws InterruptedException + * if the thread was interrupted + * @throws IllegalArgumentException + * If the temp dir is not writeable, or has insufficient space to download the jar. (This is thrown + * as a separate exception from IOException, so that the case of an unwriteable temp dir can be + * handled separately, by downloading the jar to a ByteBuffer in RAM.) */ - private File downloadTempFile(final String jarURL, final LogNode log) { - final LogNode subLog = log == null ? null : log.log(jarURL, "Downloading URL " + jarURL); - File tempFile; + private PhysicalZipFile downloadJarFromURL(final String jarURL, final LogNode log) + throws IOException, InterruptedException { + URL url = null; try { - tempFile = makeTempFile(jarURL, /* onlyUseLeafname = */ true); - final URL url = new URL(jarURL); - try (InputStream inputStream = url.openStream()) { - Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + url = new URL(jarURL); + } catch (final MalformedURLException e1) { + try { + url = new URI(jarURL).toURL(); + } catch (final MalformedURLException | IllegalArgumentException | URISyntaxException e2) { + throw new IOException("Could not parse URL: " + jarURL); } - if (subLog != null) { - subLog.addElapsedTime(); + } + + final String scheme = url.getProtocol(); + if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) { + // Check if this URL is backed by a filesystem -- if it is, don't download a + // copy of the file + // over the URL; instead, access the filesystem directly + try { + final Path path = Paths.get(url.toURI()); + // Fails with FileSystemNotFoundException if filesystem not registered for URL + final FileSystem fs = path.getFileSystem(); + if (log != null) { + log.log("URL " + jarURL + " is backed by filesystem " + fs.getClass().getName()); + } + // Wrap Path in PhysicalZipFile and return it + return new PhysicalZipFile(path, this, log); + } catch (final IllegalArgumentException | SecurityException | URISyntaxException e) { + throw new IOException("Could not convert URL to URI (" + e + "): " + url); + } catch (final FileSystemNotFoundException e) { + // Not a custom filesystem } - } catch (final IOException | SecurityException e) { - if (subLog != null) { - subLog.log("Could not download " + jarURL, e); + } + try (final CloseableUrlConnection urlConn = new CloseableUrlConnection(url)) { + long contentLengthHint = -1L; + urlConn.conn.setConnectTimeout(HTTP_TIMEOUT); + urlConn.conn.connect(); + if (urlConn.httpConn != null) { + // Get content length from HTTP headers, if available + if (urlConn.httpConn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException( + "Got response code " + urlConn.httpConn.getResponseCode() + " for URL " + url); + } + } else if (url.getProtocol().equalsIgnoreCase("file")) { + // We ended up with a "file:" URL, which can happen as a result of a custom URL + // scheme that + // rewrites its URLs into "file:" URLs (see Issue400.java). + try { + // If this is a "file:" URL, get the file from the URL and return it as a new + // PhysicalZipFile + // (this avoids going through an InputStream). Throws IOException if the file + // cannot be read. + final File file = Paths.get(url.toURI()).toFile(); + return new PhysicalZipFile(file, this, log); + + } catch (final Exception e) { + // Fall through -- unknown URL type + } + } + // Try to read content length hint + contentLengthHint = urlConn.conn.getContentLengthLong(); + if (contentLengthHint < -1L) { + contentLengthHint = -1L; + } + // Fetch content from URL + final LogNode subLog = log == null ? null : log.log("Downloading jar from URL " + jarURL); + try (InputStream inputStream = urlConn.conn.getInputStream()) { + // Fetch the jar contents from the URL's InputStream. If it doesn't fit in RAM, + // spill over to disk. + final PhysicalZipFile physicalZipFile = new PhysicalZipFile(inputStream, contentLengthHint, jarURL, + this, subLog); + if (subLog != null) { + subLog.addElapsedTime(); + subLog.log("***** Note that it is time-consuming to scan jars at non-\"file:\" URLs, " + + "the URL must be opened (possibly after an http(s) fetch) for every scan, " + + "and the same URL must also be separately opened by the ClassLoader *****"); + } + return physicalZipFile; + + } catch (final MalformedURLException e) { + throw new IOException("Malformed URL: " + jarURL); } - return null; } - if (subLog != null) { - subLog.log("Downloaded to temporary file " + tempFile); - subLog.log("***** Note that it is time-consuming to scan jars at http(s) addresses, " - + "they must be downloaded for every scan, and the same jars must also be " - + "separately downloaded by the ClassLoader *****"); + } + + private static class CloseableUrlConnection implements AutoCloseable { + public final URLConnection conn; + public final HttpURLConnection httpConn; + + public CloseableUrlConnection(final URL url) throws IOException { + conn = url.openConnection(); + httpConn = conn instanceof HttpURLConnection ? (HttpURLConnection) conn : null; + } + + @Override + public void close() { + if (httpConn != null) { + httpConn.disconnect(); + } + } + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Wrapper class that allows an {@link Inflater} instance to be reset for reuse and then recycled by a + * {@link Recycler}. + */ + private static class RecyclableInflater implements Resettable, AutoCloseable { + /** + * Create a new {@link Inflater} instance with the "nowrap" option (which is needed for zipfile entries). + */ + private final Inflater inflater = new Inflater(/* nowrap = */ true); + + /** + * Get the {@link Inflater} instance. + * + * @return the {@link Inflater} instance. + */ + public Inflater getInflater() { + return inflater; + } + + /** + * Called when an {@link Inflater} instance is recycled, to reset the inflater so it can accept new input. + */ + @Override + public void reset() { + inflater.reset(); + } + + /** + * Called when the {@link Recycler} instance is closed, to destroy the {@link Inflater} instance. + */ + @Override + public void close() { + inflater.end(); + } + } + + /** + * Wrap an {@link InputStream} with an {@link InflaterInputStream}, recycling the {@link Inflater} instance. + * + * @param rawInputStream + * the raw input stream + * @return the inflater input stream + * @throws IOException + * Signals that an I/O exception has occurred. + */ + public InputStream openInflaterInputStream(final InputStream rawInputStream) throws IOException { + if (closed.get()) { + throw new IOException("Already closed"); + } + @SuppressWarnings("resource") + final RecyclableInflater recyclableInflater = inflaterRecycler.acquire(); + final Inflater inflater = recyclableInflater.getInflater(); + return new InputStream() { + // Gen Inflater instance with nowrap set to true (needed by zip entries) + private final AtomicBoolean closed = new AtomicBoolean(); + private final byte[] buf = new byte[INFLATE_BUF_SIZE]; + private static final int INFLATE_BUF_SIZE = 8192; + + @Override + public int read() throws IOException { + if (closed.get()) { + throw new IOException("Already closed"); + } else if (inflater.finished()) { + return -1; + } + final int numDeflatedBytesRead = read(buf, 0, 1); + if (numDeflatedBytesRead < 0) { + return -1; + } else { + return buf[0] & 0xff; + } + } + + @Override + public int read(final byte[] outBuf, final int off, final int len) throws IOException { + if (closed.get()) { + throw new IOException("Already closed"); + } else if (len < 0) { + throw new IllegalArgumentException("len cannot be negative"); + } else if (len == 0) { + return 0; + } + try { + // Keep fetching data from rawInputStream until buffer is full or inflater has + // finished + int totInflatedBytes = 0; + while (!inflater.finished() && totInflatedBytes < len) { + final int numInflatedBytes = inflater.inflate(outBuf, off + totInflatedBytes, + len - totInflatedBytes); + if (numInflatedBytes == 0) { + if (inflater.needsDictionary()) { + // Should not happen for jarfiles + throw new IOException("Inflater needs preset dictionary"); + } else if (inflater.needsInput()) { + // Read a chunk of data from the raw InputStream + final int numRawBytesRead = rawInputStream.read(buf, 0, buf.length); + if (numRawBytesRead == -1) { + // An extra dummy byte is needed at the end of the input stream when + // using the "nowrap" Inflater option. + // See: ZipFile.ZipFileInflaterInputStream.fill() + buf[0] = (byte) 0; + inflater.setInput(buf, 0, 1); + } else { + // Deflate the chunk of data + inflater.setInput(buf, 0, numRawBytesRead); + } + } + } else { + totInflatedBytes += numInflatedBytes; + } + } + if (totInflatedBytes == 0) { + // If no bytes were inflated, return -1 as required by read() API contract + return -1; + } + return totInflatedBytes; + + } catch (final DataFormatException e) { + throw new ZipException( + e.getMessage() != null ? e.getMessage() : "Invalid deflated zip entry data"); + } + } + + @Override + public long skip(final long numToSkip) throws IOException { + if (closed.get()) { + throw new IOException("Already closed"); + } else if (numToSkip < 0) { + throw new IllegalArgumentException("numToSkip cannot be negative"); + } else if (numToSkip == 0) { + return 0; + } else if (inflater.finished()) { + return -1; + } + long totBytesSkipped = 0L; + for (;;) { + final int readLen = (int) Math.min(numToSkip - totBytesSkipped, buf.length); + final int numBytesRead = read(buf, 0, readLen); + if (numBytesRead > 0) { + totBytesSkipped -= numBytesRead; + } else { + break; + } + } + return totBytesSkipped; + } + + @Override + public int available() throws IOException { + if (closed.get()) { + throw new IOException("Already closed"); + } + // We don't know how many bytes are available, but have to return greater than + // zero if there is still input, according to the API contract. Hopefully + // nothing + // relies on this and ends up reading just one byte at a time. + return inflater.finished() ? 0 : 1; + } + + @Override + public synchronized void mark(final int readlimit) { + throw new IllegalArgumentException("Not supported"); + } + + @Override + public synchronized void reset() throws IOException { + throw new IllegalArgumentException("Not supported"); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public void close() { + if (!closed.getAndSet(true)) { + try { + rawInputStream.close(); + } catch (final Exception e) { + // Ignore + } + // Reset and recycle inflater instance + inflaterRecycler.recycle(recyclableInflater); + } + } + }; + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Read all the bytes in an {@link InputStream}, with spillover to a temporary file on disk if a maximum buffer + * size is exceeded. + * + * @param inputStream + * the {@link InputStream} to read from. + * @param tempFileBaseName + * the source URL or zip entry that inputStream was opened from (used to name temporary file, if + * needed). + * @param inputStreamLengthHint + * the length of inputStream if known, else -1L. + * @param log + * the log. + * @return if the {@link InputStream} could be read into a byte array, an {@link ArraySlice} will be returned. + * If this fails and the {@link InputStream} is spilled over to disk, a {@link FileSlice} will be + * returned. + * + * @throws IOException + * If the contents could not be read. + */ + public Slice readAllBytesWithSpilloverToDisk(final InputStream inputStream, final String tempFileBaseName, + final long inputStreamLengthHint, final LogNode log) throws IOException { + // Open an InflaterInputStream on the slice + try (InputStream inptStream = inputStream) { + if (inputStreamLengthHint <= scanSpec.maxBufferedJarRAMSize) { + // inputStreamLengthHint is unknown (-1) or shorter than + // scanSpec.maxBufferedJarRAMSize, + // so try reading from the InputStream into an array of size + // scanSpec.maxBufferedJarRAMSize + // or inputStreamLengthHint respectively. Also if inputStreamLengthHint == 0, + // which may or + // may not be valid, use a buffer size of 16kB to avoid spilling to disk in case + // this is + // wrong but the file is still small. + final int bufSize = inputStreamLengthHint == -1L ? scanSpec.maxBufferedJarRAMSize + : inputStreamLengthHint == 0L ? 16384 + : Math.min((int) inputStreamLengthHint, scanSpec.maxBufferedJarRAMSize); + byte[] buf = new byte[bufSize]; + final int bufLength = buf.length; + + int bufBytesUsed = 0; + int bytesRead = 0; + while ((bytesRead = inptStream.read(buf, bufBytesUsed, bufLength - bufBytesUsed)) > 0) { + // Fill buffer until nothing more can be read + bufBytesUsed += bytesRead; + } + if (bytesRead == 0) { + // If bytesRead was zero rather than -1, we need to probe the InputStream (by + // reading + // one more byte) to see if inputStreamHint underestimated the actual length of + // the stream + final byte[] overflowBuf = new byte[1]; + final int overflowBufBytesUsed = inptStream.read(overflowBuf, 0, 1); + if (overflowBufBytesUsed == 1) { + // We were able to read one more byte, so we're still not at the end of the + // stream, + // and we need to spill to disk, because buf is full + return spillToDisk(inptStream, tempFileBaseName, buf, overflowBuf, log); + } + // else (overflowBufBytesUsed == -1), so reached the end of the stream => don't + // spill to disk + } + // Successfully reached end of stream + if (bufBytesUsed < buf.length) { + // Trim array if needed (this is needed if inputStreamLengthHint was -1, or + // overestimated + // the length of the InputStream) + buf = Arrays.copyOf(buf, bufBytesUsed); + } + // Return buf as new ArraySlice + return new ArraySlice(buf, /* isDeflatedZipEntry = */ false, /* inflatedSizeHint = */ + 0L, this); + + } + // inputStreamLengthHint is longer than scanSpec.maxJarRamSize, so immediately + // spill to disk + return spillToDisk(inptStream, tempFileBaseName, /* buf = */ null, /* overflowBuf = */ null, log); + } + } + + /** + * Spill an {@link InputStream} to disk if the stream is too large to fit in RAM. + * + * @param inputStream + * The {@link InputStream}. + * @param tempFileBaseName + * The stem to base the temporary filename on. + * @param buf + * The first buffer to write to the beginning of the file, or null if none. + * @param overflowBuf + * The second buffer to write to the beginning of the file, or null if none. (Should have same + * nullity as buf.) + * @param log + * The log. + * @return the file slice + * @throws IOException + * If anything went wrong creating or writing to the temp file. + */ + private FileSlice spillToDisk(final InputStream inputStream, final String tempFileBaseName, final byte[] buf, + final byte[] overflowBuf, final LogNode log) throws IOException { + // Create temp file + File tempFile; + try { + tempFile = makeTempFile(tempFileBaseName, /* onlyUseLeafname = */ true); + } catch (final IOException e) { + throw new IOException("Could not create temporary file: " + e.getMessage()); + } + if (log != null) { + log.log("Could not fit InputStream content into max RAM buffer size, saving to temporary file: " + + tempFileBaseName + " -> " + tempFile); + } + + // Copy everything read so far and the rest of the InputStream to the temporary + // file + try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(tempFile))) { + // Write already-read buffered bytes to temp file, if anything was read + if (buf != null) { + outputStream.write(buf); + outputStream.write(overflowBuf); + } + // Copy the rest of the InputStream to the file + final byte[] copyBuf = new byte[8192]; + for (int bytesRead; (bytesRead = inputStream.read(copyBuf, 0, copyBuf.length)) > 0;) { + outputStream.write(copyBuf, 0, bytesRead); + } + } + + // Return a new FileSlice for the temporary file + return new FileSlice(tempFile, this, log); + } + + /** + * Read all the bytes in an {@link InputStream}. + * + * @param inputStream + * The {@link InputStream}. + * @param uncompressedLengthHint + * The length of the data once inflated from the {@link InputStream}, if known, otherwise -1L. + * @return The contents of the {@link InputStream} as a byte array. + * @throws IOException + * If the contents could not be read. + */ + public static byte[] readAllBytesAsArray(final InputStream inputStream, final long uncompressedLengthHint) + throws IOException { + if (uncompressedLengthHint > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("InputStream is too large to read"); + } + try (InputStream inptStream = inputStream) { + final int bufferSize = uncompressedLengthHint < 1L + // If fileSizeHint is zero or unknown, use default buffer size + ? DEFAULT_BUFFER_SIZE + // fileSizeHint is just a hint -- limit the max allocated buffer size, so that + // invalid ZipEntry + // lengths do not become a memory allocation attack vector + : Math.min((int) uncompressedLengthHint, MAX_INITIAL_BUFFER_SIZE); + byte[] buf = new byte[bufferSize]; + int totBytesRead = 0; + for (int bytesRead;;) { + while ((bytesRead = inptStream.read(buf, totBytesRead, buf.length - totBytesRead)) > 0) { + // Fill buffer until nothing more can be read + totBytesRead += bytesRead; + } + if (bytesRead < 0) { + // Reached end of stream without filling buf + break; + } + + // bytesRead == 0: either the buffer was the correct size and the end of the + // stream has been + // reached, or the buffer was too small. Need to try reading one more byte to + // see which is + // the case. + final int extraByte = inptStream.read(); + if (extraByte == -1) { + // Reached end of stream + break; + } + + // Haven't reached end of stream yet. Need to grow the buffer (double its size), + // and append + // the extra byte that was just read. + if (buf.length == FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("InputStream too large to read into array"); + } + buf = Arrays.copyOf(buf, (int) Math.min(buf.length * 2L, FileUtils.MAX_BUFFER_SIZE)); + buf[totBytesRead++] = (byte) extraByte; + } + // Return buffer and number of bytes read + return totBytesRead == buf.length ? buf : Arrays.copyOf(buf, totBytesRead); } - return tempFile; } // ------------------------------------------------------------------------------------------------------------- @@ -556,18 +1034,20 @@ private File downloadTempFile(final String jarURL, final LogNode log) { */ public void close(final LogNode log) { if (!closed.getAndSet(true)) { - if (inflaterRecycler != null) { - inflaterRecycler.forceClose(); - inflaterRecycler = null; - } + boolean interrupted = false; if (moduleRefToModuleReaderProxyRecyclerMap != null) { - try { - for (final Recycler recycler : // - moduleRefToModuleReaderProxyRecyclerMap.values()) { - recycler.forceClose(); + boolean completedWithoutInterruption = false; + while (!completedWithoutInterruption) { + try { + for (final Recycler recycler : // + moduleRefToModuleReaderProxyRecyclerMap.values()) { + recycler.forceClose(); + } + completedWithoutInterruption = true; + } catch (final InterruptedException e) { + // Try again if interrupted + interrupted = true; } - } catch (final InterruptedException e) { - interruptionChecker.interrupt(); } moduleRefToModuleReaderProxyRecyclerMap.clear(); moduleRefToModuleReaderProxyRecyclerMap = null; @@ -580,49 +1060,75 @@ public void close(final LogNode log) { nestedPathToLogicalZipFileAndPackageRootMap.clear(); nestedPathToLogicalZipFileAndPackageRootMap = null; } - for (LogicalZipFile logicalZipFile; (logicalZipFile = allocatedLogicalZipFiles.poll()) != null;) { - logicalZipFile.close(); - } if (canonicalFileToPhysicalZipFileMap != null) { - try { - for (final PhysicalZipFile physicalZipFile : canonicalFileToPhysicalZipFileMap.values()) { - physicalZipFile.close(); - } - } catch (final InterruptedException e) { - interruptionChecker.interrupt(); - } canonicalFileToPhysicalZipFileMap.clear(); canonicalFileToPhysicalZipFileMap = null; } - if (additionalAllocatedPhysicalZipFiles != null) { - for (PhysicalZipFile physicalZipFile; (physicalZipFile = additionalAllocatedPhysicalZipFiles - .poll()) != null;) { - physicalZipFile.close(); - } - additionalAllocatedPhysicalZipFiles.clear(); - additionalAllocatedPhysicalZipFiles = null; - } if (fastZipEntryToZipFileSliceMap != null) { fastZipEntryToZipFileSliceMap.clear(); fastZipEntryToZipFileSliceMap = null; } - // Temp files have to be deleted last, after all PhysicalZipFiles are closed + if (openSlices != null) { + while (!openSlices.isEmpty()) { + for (final Slice slice : new ArrayList<>(openSlices)) { + try { + slice.close(); + } catch (final IOException e) { + // Ignore + } + markSliceAsClosed(slice); + } + } + openSlices.clear(); + openSlices = null; + } + if (inflaterRecycler != null) { + inflaterRecycler.forceClose(); + } + // Temp files have to be deleted last, after all PhysicalZipFiles are closed and + // files are unmapped if (tempFiles != null) { final LogNode rmLog = tempFiles.isEmpty() || log == null ? null : log.log("Removing temporary files"); while (!tempFiles.isEmpty()) { - final File tempFile = tempFiles.removeLast(); - try { - Files.delete(tempFile.toPath()); - } catch (final IOException | SecurityException e) { - if (rmLog != null) { - rmLog.log("Removing temporary file failed: " + e); + for (final File tempFile : new ArrayList<>(tempFiles)) { + try { + removeTempFile(tempFile); + } catch (IOException | SecurityException e) { + if (rmLog != null) { + rmLog.log("Removing temporary file failed: " + tempFile); + } } } } - tempFiles.clear(); tempFiles = null; } + if (interrupted) { + interruptionChecker.interrupt(); + } } } + + /** + * System.runFinalization() -- deprecated in JDK 18, so accessed by reflection. + */ + private static Method runFinalizationMethod; + + public void runFinalizationMethod() { + if (runFinalizationMethod == null) { + runFinalizationMethod = reflectionUtils.staticMethodForNameOrNull("System", "runFinalization"); + } + if (runFinalizationMethod != null) { + try { + // Call System.runFinalization() (deprecated in JDK 18) + runFinalizationMethod.invoke(null); + } catch (final Throwable t) { + // Ignore + } + } + } + + public void closeDirectByteBuffer(final ByteBuffer backingByteBuffer) { + FileUtils.closeDirectByteBuffer(backingByteBuffer, reflectionUtils, /* log = */ null); + } } diff --git a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/PhysicalZipFile.java b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/PhysicalZipFile.java index 9c0315b5c..df212f4bd 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/PhysicalZipFile.java +++ b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/PhysicalZipFile.java @@ -28,56 +28,40 @@ */ package nonapi.io.github.classgraph.fastzipfilereader; -import java.io.Closeable; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; +import java.io.InputStream; import java.nio.channels.FileChannel; -import java.util.concurrent.atomic.AtomicBoolean; +import java.nio.file.Path; +import java.util.Objects; -import nonapi.io.github.classgraph.concurrency.SingletonMap; -import nonapi.io.github.classgraph.concurrency.SingletonMap.NullSingletonException; +import nonapi.io.github.classgraph.fileslice.ArraySlice; +import nonapi.io.github.classgraph.fileslice.FileSlice; +import nonapi.io.github.classgraph.fileslice.PathSlice; +import nonapi.io.github.classgraph.fileslice.Slice; import nonapi.io.github.classgraph.utils.FastPathResolver; import nonapi.io.github.classgraph.utils.FileUtils; import nonapi.io.github.classgraph.utils.LogNode; /** A physical zipfile, which is mmap'd using a {@link FileChannel}. */ -class PhysicalZipFile implements Closeable { - /** The {@link File} backing this {@link PhysicalZipFile}. */ - private final File file; +class PhysicalZipFile { + /** The {@link Path} backing this {@link PhysicalZipFile}, if any. */ + private Path path; - /** The path to the zipfile. */ - private final String path; - - /** The {@link RandomAccessFile}. */ - private RandomAccessFile raf; - - /** The {@link FileChannel}. */ - private FileChannel fc; - - /** The file length. */ - final long fileLen; - - /** The number of mapped byte buffers. */ - final int numMappedByteBuffers; + /** The {@link File} backing this {@link PhysicalZipFile}, if any. */ + private File file; - /** The cached mapped byte buffers for each 2GB chunk. */ - private ByteBuffer[] mappedByteBuffersCached; + /** The path to the zipfile. */ + private final String pathStr; - /** A singleton map from chunk index to byte buffer, ensuring that any given chunk is only mapped once. */ - private SingletonMap chunkIdxToByteBuffer; + /** The {@link Slice} for the zipfile. */ + Slice slice; /** The nested jar handler. */ NestedJarHandler nestedJarHandler; - /** True if the zipfile was deflated to RAM, rather than mapped from disk. */ - boolean isDeflatedToRam; - - /** Set to true once this {@link PhysicalZipFile} is closed. */ - private final AtomicBoolean closed = new AtomicBoolean(false); + /** The cached hashCode. */ + private int hashCode; /** * Construct a {@link PhysicalZipFile} from a file on disk. @@ -86,141 +70,106 @@ class PhysicalZipFile implements Closeable { * the file * @param nestedJarHandler * the nested jar handler + * @param log + * the log * @throws IOException * if an I/O exception occurs. */ - PhysicalZipFile(final File file, final NestedJarHandler nestedJarHandler) throws IOException { - this.file = file; + PhysicalZipFile(final File file, final NestedJarHandler nestedJarHandler, final LogNode log) + throws IOException { this.nestedJarHandler = nestedJarHandler; + this.file = file; + this.pathStr = FastPathResolver.resolve(FileUtils.currDirPath(), file.getPath()); + this.slice = new FileSlice(file, nestedJarHandler, log); + } - path = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, file.getPath()); - - if (!file.exists()) { - throw new IOException("File does not exist: " + file); - } - if (!FileUtils.canRead(file)) { - throw new IOException("Cannot read file: " + file); - } - if (!file.isFile()) { - throw new IOException("Is not a file: " + file); - } - - try { - raf = new RandomAccessFile(file, "r"); - fileLen = raf.length(); - if (fileLen == 0L) { - throw new IOException("Zipfile is empty: " + file); - } - fc = raf.getChannel(); - } catch (final IOException | SecurityException e) { - if (raf != null) { - raf.close(); - raf = null; - } - if (fc != null) { - fc.close(); - fc = null; - } - throw e; - } - - // Implement an array of MappedByteBuffers to support jarfiles >2GB in size: - // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6347833 - numMappedByteBuffers = (int) ((fileLen + FileUtils.MAX_BUFFER_SIZE) / FileUtils.MAX_BUFFER_SIZE); - mappedByteBuffersCached = new MappedByteBuffer[numMappedByteBuffers]; - chunkIdxToByteBuffer = new SingletonMap() { - @Override - public ByteBuffer newInstance(final Integer chunkIdxI, final LogNode log) throws IOException { - // Map the indexed 2GB chunk of the file to a MappedByteBuffer - final long pos = chunkIdxI.longValue() * FileUtils.MAX_BUFFER_SIZE; - final long chunkSize = Math.min(FileUtils.MAX_BUFFER_SIZE, fileLen - pos); - MappedByteBuffer buffer = null; - try { - buffer = fc.map(FileChannel.MapMode.READ_ONLY, pos, chunkSize); - } catch (final FileNotFoundException e) { - throw e; - } catch (IOException | OutOfMemoryError e) { - // If map failed, try calling System.gc() to free some allocated MappedByteBuffers - // (there is a limit to the number of mapped files -- 64k on Linux) - // See: http://www.mapdb.org/blog/mmap_files_alloc_and_jvm_crash/ - System.gc(); - // Then try calling map again - buffer = fc.map(FileChannel.MapMode.READ_ONLY, pos, chunkSize); - } - return buffer; - } - }; + /** + * Construct a {@link PhysicalZipFile} from a {@link Path}. + * + * @param path + * the path + * @param nestedJarHandler + * the nested jar handler + * @param log + * the log + * @throws IOException + * if an I/O exception occurs. + */ + PhysicalZipFile(final Path path, final NestedJarHandler nestedJarHandler, final LogNode log) + throws IOException { + this.nestedJarHandler = nestedJarHandler; + this.path = path; + this.pathStr = FastPathResolver.resolve(FileUtils.currDirPath(), path.toString()); + this.slice = new PathSlice(path, nestedJarHandler); } /** - * Construct a {@link PhysicalZipFile} from a ByteBuffer in memory. + * Construct a {@link PhysicalZipFile} from a byte array. * - * @param byteBuffer - * the byte buffer + * @param arr + * the array containing the zipfile. * @param outermostFile * the outermost file - * @param path + * @param pathStr * the path * @param nestedJarHandler * the nested jar handler * @throws IOException * if an I/O exception occurs. */ - PhysicalZipFile(final ByteBuffer byteBuffer, final File outermostFile, final String path, + PhysicalZipFile(final byte[] arr, final File outermostFile, final String pathStr, final NestedJarHandler nestedJarHandler) throws IOException { - this.file = outermostFile; - this.path = path; this.nestedJarHandler = nestedJarHandler; - this.isDeflatedToRam = true; - - fileLen = byteBuffer.remaining(); - if (fileLen == 0L) { - throw new IOException("Zipfile is empty: " + path); - } - - // Implement an array of MappedByteBuffers to support jarfiles >2GB in size: - // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6347833 - numMappedByteBuffers = 1; - mappedByteBuffersCached = new ByteBuffer[numMappedByteBuffers]; - mappedByteBuffersCached[0] = byteBuffer; + this.file = outermostFile; + this.pathStr = pathStr; + this.slice = new ArraySlice(arr, /* isDeflatedZipEntry = */ false, /* inflatedSizeHint = */ 0L, + nestedJarHandler); } /** - * Get a mmap'd chunk of the file, where chunkIdx denotes which 2GB chunk of the file to return (0 for the first - * 2GB of the file, or for files smaller than 2GB; 1 for the 2-4GB chunk, etc.). - * - * @param chunkIdx - * The index of the 2GB chunk to read - * @return The {@link MappedByteBuffer} for the requested file chunk, up to 2GB in size. + * Construct a {@link PhysicalZipFile} by reading from the {@link InputStream} to an array in RAM, or spill to + * disk if the {@link InputStream} is too long. + * + * @param inputStream + * the input stream + * @param inputStreamLengthHint + * The number of bytes to read in inputStream, or -1 if unknown. + * @param pathStr + * the source URL the InputStream was opened from, or the zip entry path of this entry in the parent + * zipfile + * @param nestedJarHandler + * the nested jar handler + * @param log + * the log * @throws IOException - * If the chunk could not be mmap'd. - * @throws InterruptedException - * If the thread was interrupted. + * if an I/O exception occurs. */ - ByteBuffer getByteBuffer(final int chunkIdx) throws IOException, InterruptedException { - if (closed.get()) { - throw new IOException(getClass().getSimpleName() + " already closed"); - } - if (chunkIdx < 0 || chunkIdx >= mappedByteBuffersCached.length) { - throw new IOException("Chunk index out of range"); - } - // Fast path: only look up singleton map if mappedByteBuffersCached is null - if (mappedByteBuffersCached[chunkIdx] == null) { - // This 2GB chunk has not yet been read -- mmap it (use a singleton map so that the mmap - // doesn't happen more than once, in case of race condition) - try { - mappedByteBuffersCached[chunkIdx] = chunkIdxToByteBuffer.get(chunkIdx, /* log = */ null); - } catch (final NullSingletonException e) { - throw new IOException("Cannot get ByteBuffer chunk " + chunkIdx + " : " + e); - } - } - return mappedByteBuffersCached[chunkIdx]; + PhysicalZipFile(final InputStream inputStream, final long inputStreamLengthHint, final String pathStr, + final NestedJarHandler nestedJarHandler, final LogNode log) throws IOException { + this.nestedJarHandler = nestedJarHandler; + this.pathStr = pathStr; + // Try downloading the InputStream to a byte array. If this succeeds, this will result in an ArraySlice. + // If it fails, the InputStream will be spilled to disk, resulting in a FileSlice. + this.slice = nestedJarHandler.readAllBytesWithSpilloverToDisk(inputStream, /* tempFileBaseName = */ pathStr, + inputStreamLengthHint, log); + this.file = this.slice instanceof FileSlice ? ((FileSlice) this.slice).file : null; + } + + /** + * Get the {@link Path} for the outermost jar file of this PhysicalZipFile. + * + * @return the {@link Path} for the outermost jar file of this PhysicalZipFile, or null if this file was + * downloaded from a URL directly to RAM, or is backed by a {@link File}. + */ + public Path getPath() { + return path; } /** * Get the {@link File} for the outermost jar file of this PhysicalZipFile. * - * @return the {@link File} for the outermost jar file of this PhysicalZipFile. + * @return the {@link File} for the outermost jar file of this PhysicalZipFile, or null if this file was + * downloaded from a URL directly to RAM, or is backed by a {@link Path}. */ public File getFile() { return file; @@ -233,8 +182,18 @@ public File getFile() { * @return the path for this PhysicalZipFile, which is the file path, if it is file-backed, or a compound nested * jar path, if it is memory-backed. */ - public String getPath() { - return path; + public String getPathStr() { + return pathStr; + } + + /** + * Get the length of the mapped file, or the initial remaining bytes in the wrapped ByteBuffer if a buffer was + * wrapped. + * + * @return the length of the mapped file + */ + public long length() { + return slice.sliceLength; } /* (non-Javadoc) @@ -242,60 +201,27 @@ public String getPath() { */ @Override public int hashCode() { - return file.hashCode(); + if (hashCode == 0) { + hashCode = (file == null ? 0 : file.hashCode()); + if (hashCode == 0) { + hashCode = 1; + } + } + return hashCode; } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(final Object obj) { - if (this == obj) { + public boolean equals(final Object o) { + if (o == this) { return true; - } - if (!(obj instanceof PhysicalZipFile)) { + } else if (!(o instanceof PhysicalZipFile)) { return false; } - return file.equals(((PhysicalZipFile) obj).file); - } - - /* (non-Javadoc) - * @see java.io.Closeable#close() - */ - @Override - public void close() { - if (!closed.getAndSet(true)) { - if (fc != null) { - try { - fc.close(); - fc = null; - } catch (final IOException e) { - // Ignore - } - } - if (raf != null) { - try { - raf.close(); - raf = null; - } catch (final IOException e) { - // Ignore - } - } - if (chunkIdxToByteBuffer != null) { - chunkIdxToByteBuffer.clear(); - chunkIdxToByteBuffer = null; - } - if (mappedByteBuffersCached != null) { - for (int i = 0; i < mappedByteBuffersCached.length; i++) { - if (mappedByteBuffersCached[i] != null) { - FileUtils.closeDirectByteBuffer(mappedByteBuffersCached[i], /* log = */ null); - mappedByteBuffersCached[i] = null; - } - } - mappedByteBuffersCached = null; - } - nestedJarHandler = null; - } + final PhysicalZipFile other = (PhysicalZipFile) o; + return Objects.equals(file, other.file); } /* (non-Javadoc) @@ -303,6 +229,6 @@ public void close() { */ @Override public String toString() { - return path; + return pathStr; } } \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/ZipFileSlice.java b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/ZipFileSlice.java index d7b665d8d..009ca5719 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/ZipFileSlice.java +++ b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/ZipFileSlice.java @@ -30,45 +30,25 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; -import nonapi.io.github.classgraph.WhiteBlackList.WhiteBlackListLeafname; -import nonapi.io.github.classgraph.recycler.Recycler; +import nonapi.io.github.classgraph.fileslice.Slice; +import nonapi.io.github.classgraph.scanspec.AcceptReject.AcceptRejectLeafname; /** A zipfile slice (a sub-range of bytes within a PhysicalZipFile. */ -class ZipFileSlice { +public class ZipFileSlice { /** The parent slice, or null if this is the toplevel slice (the whole zipfile). */ private final ZipFileSlice parentZipFileSlice; /** The underlying physical zipfile. */ - public final PhysicalZipFile physicalZipFile; - /** The start offset of the slice within the physical zipfile. */ - final long startOffsetWithinPhysicalZipFile; - /** The compressed or stored size of the zipfile slice or entry. */ - final long len; - /** For the toplevel zipfile slice, the zipfile path; For nested slices, the name of the zipfile entry. */ - private final String name; - /** A {@link Recycler} for {@link ZipFileSliceReader} instances. */ - final Recycler zipFileSliceReaderRecycler; - // N.B. if any fields are added, make sure the clone constructor below is updated + protected final PhysicalZipFile physicalZipFile; + /** For the toplevel zipfile slice, the zipfile path; For nested slices, the name/path of the zipfile entry. */ + private final String pathWithinParentZipFileSlice; + /** The {@link Slice} containing the zipfile. */ + public Slice slice; /** - * Create a new {@link Recycler} for {@link ZipFileSliceReader} instances. - * - * @return A new {@link Recycler} for {@link ZipFileSliceReader} instances. - */ - private Recycler newZipFileSliceReaderRecycler() { - return new Recycler() { - /* (non-Javadoc) - * @see nonapi.io.github.classgraph.concurrency.LazyReference#newInstance() - */ - @Override - public ZipFileSliceReader newInstance() throws RuntimeException { - return new ZipFileSliceReader(ZipFileSlice.this); - } - }; - } - - /** - * Create a ZipFileSlice that wraps an entire {@link PhysicalZipFile}. + * Create a ZipFileSlice that wraps a toplevel {@link PhysicalZipFile}. * * @param physicalZipFile * the physical zipfile @@ -76,31 +56,28 @@ public ZipFileSliceReader newInstance() throws RuntimeException { ZipFileSlice(final PhysicalZipFile physicalZipFile) { this.parentZipFileSlice = null; this.physicalZipFile = physicalZipFile; - this.startOffsetWithinPhysicalZipFile = 0; - this.len = physicalZipFile.fileLen; - this.name = physicalZipFile.getPath(); - this.zipFileSliceReaderRecycler = newZipFileSliceReaderRecycler(); + this.slice = physicalZipFile.slice; + this.pathWithinParentZipFileSlice = physicalZipFile.getPathStr(); } /** - * Create a ZipFileSlice that wraps a {@link PhysicalZipFile} extracted to a ByteBuffer in memory. + * Create a ZipFileSlice that wraps a {@link PhysicalZipFile} that was extracted or inflated from a nested jar + * to memory or disk. * - * @param physicalZipFileInRam + * @param physicalZipFile * a physical zipfile that has been extracted to RAM * @param zipEntry * the zip entry */ - ZipFileSlice(final PhysicalZipFile physicalZipFileInRam, final FastZipEntry zipEntry) { + ZipFileSlice(final PhysicalZipFile physicalZipFile, final FastZipEntry zipEntry) { this.parentZipFileSlice = zipEntry.parentLogicalZipFile; - this.physicalZipFile = physicalZipFileInRam; - this.startOffsetWithinPhysicalZipFile = 0; - this.len = physicalZipFile.fileLen; - this.name = zipEntry.entryName; - this.zipFileSliceReaderRecycler = newZipFileSliceReaderRecycler(); + this.physicalZipFile = physicalZipFile; + this.slice = physicalZipFile.slice; + this.pathWithinParentZipFileSlice = zipEntry.entryName; } /** - * Create a ZipFileSlice that wraps a single {@link FastZipEntry}. + * Create a ZipFileSlice that wraps a single stored (not deflated) {@link FastZipEntry}. * * @param zipEntry * the zip entry @@ -112,10 +89,8 @@ public ZipFileSliceReader newInstance() throws RuntimeException { ZipFileSlice(final FastZipEntry zipEntry) throws IOException, InterruptedException { this.parentZipFileSlice = zipEntry.parentLogicalZipFile; this.physicalZipFile = zipEntry.parentLogicalZipFile.physicalZipFile; - this.startOffsetWithinPhysicalZipFile = zipEntry.getEntryDataStartOffsetWithinPhysicalZipFile(); - this.len = zipEntry.compressedSize; - this.name = zipEntry.entryName; - this.zipFileSliceReaderRecycler = newZipFileSliceReaderRecycler(); + this.slice = zipEntry.getSlice(); + this.pathWithinParentZipFileSlice = zipEntry.entryName; } /** @@ -127,26 +102,42 @@ public ZipFileSliceReader newInstance() throws RuntimeException { ZipFileSlice(final ZipFileSlice other) { this.parentZipFileSlice = other.parentZipFileSlice; this.physicalZipFile = other.physicalZipFile; - this.startOffsetWithinPhysicalZipFile = other.startOffsetWithinPhysicalZipFile; - this.len = other.len; - this.name = other.name; - // Reuse the recycler for clones - this.zipFileSliceReaderRecycler = other.zipFileSliceReaderRecycler; + this.slice = other.slice; + this.pathWithinParentZipFileSlice = other.pathWithinParentZipFileSlice; } /** - * Check whether this zipfile slice and all of its parent slices are whitelisted and not blacklisted in the - * jarfile white/blacklist. + * Check whether this zipfile slice and all of its parent slices are accepted and not rejected in the jarfile + * accept/reject criteria. * - * @param jarWhiteBlackList - * the jar white black list - * @return true if this zipfile slice and all of its parent slices are whitelisted and not blacklisted in the - * jarfile white/blacklist. + * @param jarAcceptReject + * the jar accept/reject criteria + * @return true if this zipfile slice and all of its parent slices are accepted and not rejected in the jarfile + * accept/reject criteria. */ - public boolean isWhitelistedAndNotBlacklisted(final WhiteBlackListLeafname jarWhiteBlackList) { - return jarWhiteBlackList.isWhitelistedAndNotBlacklisted(name) // - && (parentZipFileSlice == null - || parentZipFileSlice.isWhitelistedAndNotBlacklisted(jarWhiteBlackList)); + public boolean isAcceptedAndNotRejected(final AcceptRejectLeafname jarAcceptReject) { + return jarAcceptReject.isAcceptedAndNotRejected(pathWithinParentZipFileSlice) // + && (parentZipFileSlice == null || parentZipFileSlice.isAcceptedAndNotRejected(jarAcceptReject)); + } + + /** + * Get the parent ZipFileslice, or return null if this is a toplevel slice (i.e. if this slice wraps an entire + * physical zipfile). + * + * @return the parent ZipFileslice, or null if this is a toplevel slice. + */ + public ZipFileSlice getParentZipFileSlice() { + return parentZipFileSlice; + } + + /** + * Get the name of the slice (either the entry name/path within the parent zipfile slice, or the path of the + * physical zipfile if this slice is a toplevel slice (i.e. if this slice wraps an entire physical zipfile). + * + * @return the name of the slice. + */ + public String getPathWithinParentZipFileSlice() { + return pathWithinParentZipFileSlice; } /** @@ -158,15 +149,15 @@ public boolean isWhitelistedAndNotBlacklisted(final WhiteBlackListLeafname jarWh private void appendPath(final StringBuilder buf) { if (parentZipFileSlice != null) { parentZipFileSlice.appendPath(buf); + if (buf.length() > 0) { + buf.append("!/"); + } } - if (buf.length() > 0) { - buf.append("!/"); - } - buf.append(name); + buf.append(pathWithinParentZipFileSlice); } /** - * Get the path of this zipfile slice, e.g. "/path/to/jarfile.jar!/nestedjar1.jar!/nestedfile". + * Get the path of this zipfile slice, e.g. "/path/to/jarfile.jar!/nestedjar1.jar". * * @return the path of this zipfile slice. */ @@ -176,49 +167,48 @@ public String getPath() { return buf.toString(); } - /** - * Get the path of the parent of this zipfile slice. If this is a toplevel slice (i.e. if this slice corresponds - * to a whole physical zipfile), then the returned path is the directory of the containing dir. - * - * @return the path of this zipfile slice. - */ - public String getParentPath() { - final StringBuilder buf = new StringBuilder(); - appendPath(buf); - return buf.toString(); - } - /** * Get the physical {@link File} that this ZipFileSlice is a slice of. * - * @return the physical {@link File} that this ZipFileSlice is a slice of. + * @return the physical {@link File} that this ZipFileSlice is a slice of, or null if this file was downloaded + * from a URL directly to RAM. */ public File getPhysicalFile() { - return physicalZipFile.getFile(); + final Path path = physicalZipFile.getPath(); + if (path != null) { + try { + return path.toFile(); + } catch (final UnsupportedOperationException e) { + // Filesystem supports the Path API but not the File API + return null; + } + } else { + return physicalZipFile.getFile(); + } } /* (non-Javadoc) - * @see java.lang.Object#hashCode() + * @see nonapi.io.github.classgraph.fastzipfilereader.ZipFileSlice#equals(java.lang.Object) */ @Override - public int hashCode() { - return physicalZipFile.getPath().hashCode() ^ (int) startOffsetWithinPhysicalZipFile ^ (int) len; + public boolean equals(final Object o) { + if (o == this) { + return true; + } else if (!(o instanceof ZipFileSlice)) { + return false; + } else { + final ZipFileSlice other = (ZipFileSlice) o; + return Objects.equals(physicalZipFile, other.physicalZipFile) && Objects.equals(slice, other.slice) + && Objects.equals(pathWithinParentZipFileSlice, other.pathWithinParentZipFileSlice); + } } /* (non-Javadoc) - * @see java.lang.Object#equals(java.lang.Object) + * @see nonapi.io.github.classgraph.fastzipfilereader.ZipFileSlice#hashCode() */ @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (!(o instanceof ZipFileSlice)) { - return false; - } - final ZipFileSlice other = (ZipFileSlice) o; - return startOffsetWithinPhysicalZipFile == other.startOffsetWithinPhysicalZipFile && len == other.len - && this.physicalZipFile.equals(other.physicalZipFile); + public int hashCode() { + return Objects.hash(physicalZipFile, slice, pathWithinParentZipFileSlice); } /* (non-Javadoc) @@ -226,8 +216,13 @@ public boolean equals(final Object o) { */ @Override public String toString() { - return (physicalZipFile.isDeflatedToRam ? "[ByteBuffer deflated to RAM from " + getPath() + "]" - : physicalZipFile.getFile()) + " [byte range " + startOffsetWithinPhysicalZipFile + ".." - + (startOffsetWithinPhysicalZipFile + len) + " / " + physicalZipFile.fileLen + "]"; + final String path = getPath(); + String fileStr = physicalZipFile.getPath() == null ? null : physicalZipFile.getPath().toString(); + if (fileStr == null) { + fileStr = physicalZipFile.getFile() == null ? null : physicalZipFile.getFile().toString(); + } + return "[" + (fileStr != null && !fileStr.equals(path) ? path + " -> " + fileStr : path) + " ; byte range: " + + slice.sliceStartPos + ".." + (slice.sliceStartPos + slice.sliceLength) + " / " + + physicalZipFile.length() + "]"; } } \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/ZipFileSliceReader.java b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/ZipFileSliceReader.java deleted file mode 100644 index 0d40fb42b..000000000 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/ZipFileSliceReader.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * This file is part of ClassGraph. - * - * Author: Luke Hutchison - * - * Hosted at: https://github.com/classgraph/classgraph - * - * -- - * - * The MIT License (MIT) - * - * Copyright (c) 2019 Luke Hutchison - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT - * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO - * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE - * OR OTHER DEALINGS IN THE SOFTWARE. - */ -package nonapi.io.github.classgraph.fastzipfilereader; - -import java.io.EOFException; -import java.io.IOException; -import java.nio.Buffer; -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -import nonapi.io.github.classgraph.utils.FileUtils; - -/** - * A class for reading from a ZipFileSlice. - */ -class ZipFileSliceReader implements AutoCloseable { - /** The zipfile slice. */ - private final ZipFileSlice zipFileSlice; - - /** The chunk cache. */ - private final ByteBuffer[] chunkCache; - - /** A scratch buffer. */ - private final byte[] scratch = new byte[256]; - - /** - * Constructor. - * - * @param zipFileSlice - * the zipfile slice - */ - public ZipFileSliceReader(final ZipFileSlice zipFileSlice) { - this.zipFileSlice = zipFileSlice; - this.chunkCache = new ByteBuffer[zipFileSlice.physicalZipFile.numMappedByteBuffers]; - } - - /** - * Get the 2GB chunk of the zipfile with the given chunk index. - * - * @param chunkIdx - * the chunk index - * @return the chunk - * @throws IOException - * if an I/O exception occurs. - * @throws InterruptedException - * if the thread was interrupted. - */ - private ByteBuffer getChunk(final int chunkIdx) throws IOException, InterruptedException { - ByteBuffer chunk = chunkCache[chunkIdx]; - if (chunk == null) { - final ByteBuffer byteBufferDup = zipFileSlice.physicalZipFile.getByteBuffer(chunkIdx).duplicate(); - chunk = chunkCache[chunkIdx] = byteBufferDup; - } - return chunk; - } - - /** - * Copy from an offset within the file into a byte[] array (possibly spanning the boundary between two 2GB - * chunks). - * - * @param off - * the offset - * @param buf - * the buffer to copy into - * @param bufStart - * the start index within the buffer - * @param numBytesToRead - * the number of bytes to read - * @return the number of bytes read - * @throws IOException - * if an I/O exception occurs. - * @throws InterruptedException - * if the thread was interrupted. - */ - int read(final long off, final byte[] buf, final int bufStart, final int numBytesToRead) - throws IOException, InterruptedException { - if (off < 0 || bufStart < 0 || bufStart + numBytesToRead > buf.length) { - throw new IndexOutOfBoundsException(); - } - int currBufStart = bufStart; - int remainingBytesToRead = numBytesToRead; - int totBytesRead = 0; - for (long currOff = off; remainingBytesToRead > 0;) { - // Find the ByteBuffer chunk to read from - final long currOffAbsolute = zipFileSlice.startOffsetWithinPhysicalZipFile + currOff; - final int chunkIdx = (int) (currOffAbsolute / FileUtils.MAX_BUFFER_SIZE); - final ByteBuffer chunk = getChunk(chunkIdx); - final long chunkStartAbsolute = ((long) chunkIdx) * (long) FileUtils.MAX_BUFFER_SIZE; - final int startReadPos = (int) (currOffAbsolute - chunkStartAbsolute); - - // Read from current chunk. - // N.B. the cast to Buffer is necessary, see: - // https://github.com/plasma-umass/doppio/issues/497#issuecomment-334740243 - // https://github.com/classgraph/classgraph/issues/284#issuecomment-443612800 - // Otherwise compiling in JDK<9 compatibility mode using JDK9+ causes runtime breakage. - ((Buffer) chunk).mark(); - ((Buffer) chunk).position(startReadPos); - final int numBytesRead = Math.min(chunk.remaining(), remainingBytesToRead); - try { - chunk.get(buf, currBufStart, numBytesRead); - } catch (final BufferUnderflowException e) { - // Should not happen - throw new EOFException("Unexpected EOF"); - } - ((Buffer) chunk).reset(); - - currOff += numBytesRead; - currBufStart += numBytesRead; - totBytesRead += numBytesRead; - remainingBytesToRead -= numBytesRead; - } - return totBytesRead == 0 && numBytesToRead > 0 ? -1 : totBytesRead; - } - - /** - * Get a short from a byte array. - * - * @param arr - * the byte array - * @param off - * the offset to start reading from - * @return the short - * @throws IndexOutOfBoundsException - * the index out of bounds exception - */ - static int getShort(final byte[] arr, final long off) throws IndexOutOfBoundsException { - final int ioff = (int) off; - if (ioff < 0 || ioff > arr.length - 2) { - throw new IndexOutOfBoundsException(); - } - return ((arr[ioff + 1] & 0xff) << 8) | (arr[ioff] & 0xff); - } - - /** - * Get a short from the zipfile slice. - * - * @param off - * the offset to start reading from - * @return the short - * @throws IOException - * if an I/O exception occurs. - * @throws InterruptedException - * if the thread was interrupted. - */ - int getShort(final long off) throws IOException, InterruptedException { - if (off < 0 || off > zipFileSlice.len - 2) { - throw new IndexOutOfBoundsException(); - } - if (read(off, scratch, 0, 2) < 2) { - throw new EOFException("Unexpected EOF"); - } - return ((scratch[1] & 0xff) << 8) | (scratch[0] & 0xff); - } - - /** - * Get an int from a byte array. - * - * @param arr - * the byte array - * @param off - * the offset to start reading from - * @return the int - * @throws IOException - * if an I/O exception occurs. - */ - static int getInt(final byte[] arr, final long off) throws IOException { - final int ioff = (int) off; - if (ioff < 0 || ioff > arr.length - 4) { - throw new IndexOutOfBoundsException(); - } - return ((arr[ioff + 3] & 0xff) << 24) // - | ((arr[ioff + 2] & 0xff) << 16) // - | ((arr[ioff + 1] & 0xff) << 8) // - | (arr[ioff] & 0xff); - } - - /** - * Get an int from the zipfile slice. - * - * @param off - * the offset to start reading from - * @return the int - * @throws IOException - * if an I/O exception occurs. - * @throws InterruptedException - * if the thread was interrupted. - */ - int getInt(final long off) throws IOException, InterruptedException { - if (off < 0 || off > zipFileSlice.len - 4) { - throw new IndexOutOfBoundsException(); - } - if (read(off, scratch, 0, 4) < 4) { - throw new EOFException("Unexpected EOF"); - } - return ((scratch[3] & 0xff) << 24) // - | ((scratch[2] & 0xff) << 16) // - | ((scratch[1] & 0xff) << 8) // - | (scratch[0] & 0xff); - } - - /** - * Get a long from a byte array. - * - * @param arr - * the byte array - * @param off - * the offset to start reading from - * @return the long - * @throws IOException - * if an I/O exception occurs. - */ - static long getLong(final byte[] arr, final long off) throws IOException { - final int ioff = (int) off; - if (ioff < 0 || ioff > arr.length - 8) { - throw new IndexOutOfBoundsException(); - } - return // - (((long) (((arr[ioff + 7] & 0xff) << 24) // - | ((arr[ioff + 6] & 0xff) << 16) // - | ((arr[ioff + 5] & 0xff) << 8) // - | (arr[ioff + 4] & 0xff))) // - << 32) // - | (((arr[ioff + 3] & 0xff) << 24) // - | ((arr[ioff + 2] & 0xff) << 16) // - | ((arr[ioff + 1] & 0xff) << 8) // - | (arr[ioff] & 0xff)); - } - - /** - * Get a long from the zipfile slice. - * - * @param off - * the offset to start reading from - * @return the long - * @throws IOException - * if an I/O exception occurs. - * @throws InterruptedException - * if the thread was interrupted. - */ - long getLong(final long off) throws IOException, InterruptedException { - if (off < 0 || off > zipFileSlice.len - 8) { - throw new IndexOutOfBoundsException(); - } - if (read(off, scratch, 0, 8) < 8) { - throw new EOFException("Unexpected EOF"); - } - return // - (((long) (((scratch[7] & 0xff) << 24) // - | ((scratch[6] & 0xff) << 16) // - | ((scratch[5] & 0xff) << 8) // - | (scratch[4] & 0xff))) // - << 32) // - | (((scratch[3] & 0xff) << 24) // - | ((scratch[2] & 0xff) << 16) // - | ((scratch[1] & 0xff) << 8) // - | (scratch[0] & 0xff)); - } - - /** - * Get a string from a byte array. - * - * @param arr - * the byte array - * @param off - * the offset to start reading from - * @param lenBytes - * the length of the string in bytes - * @return the string - * @throws IOException - * if an I/O exception occurs. - */ - static String getString(final byte[] arr, final long off, final int lenBytes) throws IOException { - final int ioff = (int) off; - if (ioff < 0 || ioff > arr.length - lenBytes) { - throw new IndexOutOfBoundsException(); - } - return new String(arr, ioff, lenBytes, StandardCharsets.UTF_8); - } - - /** - * Get a string from the zipfile slice. - * - * @param off - * the offset to start reading from - * @param lenBytes - * the length of the string in bytes - * @return the string - * @throws IOException - * if an I/O exception occurs. - * @throws InterruptedException - * if the thread was interrupted. - */ - String getString(final long off, final int lenBytes) throws IOException, InterruptedException { - if (off < 0 || off > zipFileSlice.len - lenBytes) { - throw new IndexOutOfBoundsException(); - } - final byte[] scratchToUse = lenBytes <= scratch.length ? scratch : new byte[lenBytes]; - if (read(off, scratchToUse, 0, lenBytes) < lenBytes) { - throw new EOFException("Unexpected EOF"); - } - // Assume the entry names are encoded in UTF-8 (should be the case for all jars; the only other - // valid zipfile charset is CP437, which is the same as ASCII for printable high-bit-clear chars) - return new String(scratchToUse, 0, lenBytes, StandardCharsets.UTF_8); - } - - /* (non-Javadoc) - * @see java.lang.AutoCloseable#close() - */ - @Override - public void close() { - // Drop refs to ByteBuffer chunks so they can be garbage collected - Arrays.fill(chunkCache, null); - } -} \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/ArraySlice.java b/src/main/java/nonapi/io/github/classgraph/fileslice/ArraySlice.java new file mode 100644 index 000000000..7937de6c7 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/ArraySlice.java @@ -0,0 +1,152 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessArrayReader; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessReader; + +/** A byte array slice. */ +public class ArraySlice extends Slice { + /** The wrapped byte array. */ + public byte[] arr; + + /** + * Constructor for treating a range of an array as a slice. + * + * @param parentSlice + * the parent slice + * @param offset + * the offset of the sub-slice within the parent slice + * @param length + * the length of the sub-slice + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @param nestedJarHandler + * the nested jar handler + */ + private ArraySlice(final ArraySlice parentSlice, final long offset, final long length, + final boolean isDeflatedZipEntry, final long inflatedLengthHint, + final NestedJarHandler nestedJarHandler) { + super(parentSlice, offset, length, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + this.arr = parentSlice.arr; + } + + /** + * Constructor for treating a whole array as a slice. + * + * @param arr + * the array containing the slice. + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @param nestedJarHandler + * the nested jar handler + */ + public ArraySlice(final byte[] arr, final boolean isDeflatedZipEntry, final long inflatedLengthHint, + final NestedJarHandler nestedJarHandler) { + super(arr.length, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + this.arr = arr; + } + + /** + * Slice this slice to form a sub-slice. + * + * @param offset + * the offset relative to the start of this slice to use as the start of the sub-slice. + * @param length + * the length of the sub-slice. + * @param isDeflatedZipEntry + * the is deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @return the slice + */ + @Override + public Slice slice(final long offset, final long length, final boolean isDeflatedZipEntry, + final long inflatedLengthHint) { + if (this.isDeflatedZipEntry) { + throw new IllegalArgumentException("Cannot slice a deflated zip entry"); + } + return new ArraySlice(this, offset, length, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + } + + /** + * Load the slice as a byte array. + * + * @return the byte[] + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Override + public byte[] load() throws IOException { + if (isDeflatedZipEntry) { + // Deflate into RAM if necessary + try (InputStream inputStream = open()) { + return NestedJarHandler.readAllBytesAsArray(inputStream, inflatedLengthHint); + } + } else if (sliceStartPos == 0L && sliceLength == arr.length) { + // Fast path -- return whole array, if the array is the whole slice and is not deflated + return arr; + } else { + // Copy range of array, if it is a slice and it is not deflated + return Arrays.copyOfRange(arr, (int) sliceStartPos, (int) (sliceStartPos + sliceLength)); + } + } + + /** + * Return a new random access reader. + * + * @return the random access reader + */ + @Override + public RandomAccessReader randomAccessReader() { + return new RandomAccessArrayReader(arr, (int) sliceStartPos, (int) sliceLength); + } + + @Override + public boolean equals(final Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/FileSlice.java b/src/main/java/nonapi/io/github/classgraph/fileslice/FileSlice.java new file mode 100644 index 000000000..d230cfc23 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/FileSlice.java @@ -0,0 +1,314 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.github.classgraph.ClassGraph; +import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessByteBufferReader; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessFileChannelReader; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessReader; +import nonapi.io.github.classgraph.utils.FileUtils; +import nonapi.io.github.classgraph.utils.LogNode; + +/** A {@link File} slice. */ +public class FileSlice extends Slice { + /** The {@link File}. */ + public final File file; + + /** The {@link RandomAccessFile} opened on the {@link File}. */ + public RandomAccessFile raf; + + /** The file length. */ + private final long fileLength; + + /** The file channel. */ + private FileChannel fileChannel; + + /** The backing byte buffer, if any. */ + private ByteBuffer backingByteBuffer; + + /** True if this is a top level file slice. */ + private final boolean isTopLevelFileSlice; + + /** True if {@link #close} has been called. */ + private final AtomicBoolean isClosed = new AtomicBoolean(); + + /** + * Constructor for treating a range of a file as a slice. + * + * @param parentSlice + * the parent slice + * @param offset + * the offset of the sub-slice within the parent slice + * @param length + * the length of the sub-slice + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @param nestedJarHandler + * the nested jar handler + */ + private FileSlice(final FileSlice parentSlice, final long offset, final long length, + final boolean isDeflatedZipEntry, final long inflatedLengthHint, + final NestedJarHandler nestedJarHandler) { + super(parentSlice, offset, length, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + this.file = parentSlice.file; + this.raf = parentSlice.raf; + this.fileChannel = parentSlice.fileChannel; + this.fileLength = parentSlice.fileLength; + this.isTopLevelFileSlice = false; + + if (parentSlice.backingByteBuffer != null) { + // Duplicate and slice the backing byte buffer, if there is one + this.backingByteBuffer = parentSlice.backingByteBuffer.duplicate(); + ((Buffer) this.backingByteBuffer).position((int) sliceStartPos); + ((Buffer) this.backingByteBuffer).limit((int) (sliceStartPos + sliceLength)); + } + + // Only mark toplevel file slices as open (sub slices don't need to be marked as open since + // they don't need to be closed, they just copy the resource references of the toplevel slice) + } + + /** + * Constructor for toplevel file slice. + * + * @param file + * the file + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @param nestedJarHandler + * the nested jar handler + * @param log + * the log + * @throws IOException + * if the file cannot be opened. + */ + public FileSlice(final File file, final boolean isDeflatedZipEntry, final long inflatedLengthHint, + final NestedJarHandler nestedJarHandler, final LogNode log) throws IOException { + super(file.length(), isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + // Make sure the File is readable and is a regular file + FileUtils.checkCanReadAndIsFile(file); + this.file = file; + this.raf = new RandomAccessFile(file, "r"); + this.fileChannel = raf.getChannel(); + this.fileLength = file.length(); + this.isTopLevelFileSlice = true; + + if (nestedJarHandler.scanSpec.enableMemoryMapping) { + // TODO: for JDK 24+, use the new Arena API to memory-map the file to a MemorySegment: + // https://docs.oracle.com/en/java/javase/22/docs//api/java.base/java/nio/channels/FileChannel.html#map(java.nio.channels.FileChannel.MapMode,long,long,java.lang.foreign.Arena) + try { + // Try mapping file (some operating systems throw OutOfMemoryError if file + // can't be mapped, some throw IOException) + backingByteBuffer = fileChannel.map(MapMode.READ_ONLY, 0L, fileLength); + } catch (IOException | OutOfMemoryError e) { + // Try running garbage collection then try mapping the file again + System.gc(); + nestedJarHandler.runFinalizationMethod(); + try { + backingByteBuffer = fileChannel.map(MapMode.READ_ONLY, 0L, fileLength); + } catch (IOException | OutOfMemoryError e2) { + if (log != null) { + log.log("File " + file + " cannot be memory mapped: " + e2 + + " (using RandomAccessFile API instead)"); + } + // Fall through -- RandomAccessFile API will be used instead + } + } + } + + // Mark toplevel slice as open + nestedJarHandler.markSliceAsOpen(this); + } + + /** + * Constructor for toplevel file slice. + * + * @param file + * the file + * @param nestedJarHandler + * the nested jar handler + * @param log + * the log + * @throws IOException + * if the file cannot be opened. + */ + public FileSlice(final File file, final NestedJarHandler nestedJarHandler, final LogNode log) + throws IOException { + this(file, /* isDeflatedZipEntry = */ false, /* inflatedSizeHint = */ 0L, nestedJarHandler, log); + } + + /** + * Slice the file. + * + * @param offset + * the offset of the sub-slice within the parent slice + * @param length + * the length of the sub-slice + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @return the slice + */ + @Override + public Slice slice(final long offset, final long length, final boolean isDeflatedZipEntry, + final long inflatedLengthHint) { + if (this.isDeflatedZipEntry) { + throw new IllegalArgumentException("Cannot slice a deflated zip entry"); + } + return new FileSlice(this, offset, length, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + } + + /** + * Read directly from FileChannel (slow path, but handles >2GB). + * + * @return the random access reader + */ + @Override + public RandomAccessReader randomAccessReader() { + if (backingByteBuffer == null) { + // If file was not mmap'd, return a RandomAccessReader that uses the FileChannel + return new RandomAccessFileChannelReader(fileChannel, sliceStartPos, sliceLength); + } else { + // If file was mmap'd, return a RandomAccessReader that uses the ByteBuffer + return new RandomAccessByteBufferReader(backingByteBuffer, sliceStartPos, sliceLength); + } + } + + /** + * Load the slice as a byte array. + * + * @return the byte[] + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Override + public byte[] load() throws IOException { + if (isDeflatedZipEntry) { + // Inflate into RAM if deflated + if (inflatedLengthHint > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("Uncompressed size is larger than 2GB"); + } + try (InputStream inputStream = open()) { + return NestedJarHandler.readAllBytesAsArray(inputStream, inflatedLengthHint); + } + } else { + // Copy from either RandomAccessFile or MappedByteBuffer to byte array + if (sliceLength > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("File is larger than 2GB"); + } + final RandomAccessReader reader = randomAccessReader(); + final byte[] content = new byte[(int) sliceLength]; + if (reader.read(0, content, 0, content.length) < content.length) { + // Should not happen + throw new IOException("File is truncated"); + } + return content; + } + } + + /** + * Read the slice into a {@link ByteBuffer} (or memory-map the slice to a {@link MappedByteBuffer}, if + * {@link ClassGraph#enableMemoryMapping()} was called.) + * + * @return the byte buffer + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Override + public ByteBuffer read() throws IOException { + if (isDeflatedZipEntry) { + // Inflate to RAM if deflated (unfortunately there is no lazy-loading ByteBuffer that will + // decompress partial streams on demand, so we have to decompress the whole zip entry) + if (inflatedLengthHint > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("Uncompressed size is larger than 2GB"); + } + return ByteBuffer.wrap(load()); + } else if (backingByteBuffer == null) { + // Copy from RandomAccessFile to byte array, then wrap in a ByteBuffer + if (sliceLength > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("File is larger than 2GB"); + } + return ByteBuffer.wrap(load()); + } else { + // FileSlice is backed with a MappedByteBuffer -- duplicate it and return it (low-cost operation) + return backingByteBuffer.duplicate(); + } + } + + @Override + public boolean equals(final Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + /** Close the slice. Unmaps any backing {@link MappedByteBuffer}. */ + @Override + public void close() { + if (!isClosed.getAndSet(true)) { + if (isTopLevelFileSlice && backingByteBuffer != null) { + // Only close ByteBuffer in toplevel file slice, so that ByteBuffer is only closed once + // (also duplicates of MappedByteBuffers cannot be closed by the cleaner API) + nestedJarHandler.closeDirectByteBuffer(backingByteBuffer); + } + backingByteBuffer = null; + fileChannel = null; + try { + // Closing raf will also close the associated FileChannel + raf.close(); + } catch (final IOException e) { + // Ignore + } + raf = null; + nestedJarHandler.markSliceAsClosed(this); + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/PathSlice.java b/src/main/java/nonapi/io/github/classgraph/fileslice/PathSlice.java new file mode 100644 index 000000000..2c490c637 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/PathSlice.java @@ -0,0 +1,291 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.github.classgraph.ClassGraph; +import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessFileChannelReader; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessReader; +import nonapi.io.github.classgraph.utils.FileUtils; + +/** A {@link Path} slice. */ +public class PathSlice extends Slice { + /** The {@link Path}. */ + public final Path path; + + /** The file length. */ + private final long fileLength; + + /** The {@link FileChannel} opened on the {@link Path}. */ + private FileChannel fileChannel; + + /** True if this is a top level file slice. */ + private final boolean isTopLevelFileSlice; + + /** True if {@link #close} has been called. */ + private final AtomicBoolean isClosed = new AtomicBoolean(); + + /** + * Constructor for treating a range of a file as a slice. + * + * @param parentSlice + * the parent slice + * @param offset + * the offset of the sub-slice within the parent slice + * @param length + * the length of the sub-slice + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @param nestedJarHandler + * the nested jar handler + */ + private PathSlice(final PathSlice parentSlice, final long offset, final long length, + final boolean isDeflatedZipEntry, final long inflatedLengthHint, + final NestedJarHandler nestedJarHandler) { + super(parentSlice, offset, length, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + + this.path = parentSlice.path; + this.fileChannel = parentSlice.fileChannel; + this.fileLength = parentSlice.fileLength; + this.isTopLevelFileSlice = false; + + // Only mark toplevel file slices as open (sub slices don't need to be marked as + // open since + // they don't need to be closed, they just copy the resource references of the + // toplevel slice) + } + + /** + * Constructor for toplevel file slice. + * + * @param path + * the path + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @param nestedJarHandler + * the nested jar handler + * @throws IOException + * if the file cannot be opened. + */ + public PathSlice(final Path path, final boolean isDeflatedZipEntry, final long inflatedLengthHint, + final NestedJarHandler nestedJarHandler) throws IOException { + this(path, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler, true); + } + + /** + * Constructor for toplevel file slice. + * + * @param path + * the path + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @param nestedJarHandler + * the nested jar handler + * @param checkAccess + * whether it is needed to check read access and if it is a file + * @throws IOException + * if the file cannot be opened. + */ + public PathSlice(final Path path, final boolean isDeflatedZipEntry, final long inflatedLengthHint, + final NestedJarHandler nestedJarHandler, final boolean checkAccess) throws IOException { + super(0L, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + + if (checkAccess) { + // Make sure the File is readable and is a regular file + FileUtils.checkCanReadAndIsFile(path); + } + + this.path = path; + this.fileChannel = FileChannel.open(path, StandardOpenOption.READ); + this.fileLength = fileChannel.size(); + this.isTopLevelFileSlice = true; + + // Had to use 0L for sliceLength in call to super, since FileChannel wasn't open + // yet => update sliceLength + this.sliceLength = fileLength; + + // Mark toplevel slice as open + nestedJarHandler.markSliceAsOpen(this); + } + + /** + * Constructor for toplevel file slice. + * + * @param path + * the path + * @param nestedJarHandler + * the nested jar handler + * @throws IOException + * if the file cannot be opened. + */ + public PathSlice(final Path path, final NestedJarHandler nestedJarHandler) throws IOException { + this(path, /* isDeflatedZipEntry = */ false, /* inflatedSizeHint = */ 0L, nestedJarHandler); + } + + /** + * Slice the file. + * + * @param offset + * the offset of the sub-slice within the parent slice + * @param length + * the length of the sub-slice + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @return the slice + */ + @Override + public Slice slice(final long offset, final long length, final boolean isDeflatedZipEntry, + final long inflatedLengthHint) { + if (this.isDeflatedZipEntry) { + throw new IllegalArgumentException("Cannot slice a deflated zip entry"); + } + return new PathSlice(this, offset, length, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + } + + /** + * Read directly from FileChannel (slow path, but handles >2GB). + * + * @return the random access reader + */ + @Override + public RandomAccessReader randomAccessReader() { + // Return a RandomAccessReader that uses the FileChannel + return new RandomAccessFileChannelReader(fileChannel, sliceStartPos, sliceLength); + } + + /** + * Load the slice as a byte array. + * + * @return the byte[] + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Override + public byte[] load() throws IOException { + if (isDeflatedZipEntry) { + // Inflate into RAM if deflated + if (inflatedLengthHint > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("Uncompressed size is larger than 2GB"); + } + try (InputStream inputStream = open()) { + return NestedJarHandler.readAllBytesAsArray(inputStream, inflatedLengthHint); + } + } else { + // Copy from FileChannel to byte array + if (sliceLength > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("File is larger than 2GB"); + } + final RandomAccessReader reader = randomAccessReader(); + final byte[] content = new byte[(int) sliceLength]; + if (reader.read(0, content, 0, content.length) < content.length) { + // Should not happen + throw new IOException("File is truncated"); + } + return content; + } + } + + /** + * Read the slice into a {@link ByteBuffer} (or memory-map the slice to a {@link MappedByteBuffer}, if + * {@link ClassGraph#enableMemoryMapping()} was called.) + * + * @return the byte buffer + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Override + public ByteBuffer read() throws IOException { + if (isDeflatedZipEntry) { + // Inflate to RAM if deflated (unfortunately there is no lazy-loading ByteBuffer + // that will + // decompress partial streams on demand, so we have to decompress the whole zip + // entry) + if (inflatedLengthHint > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("Uncompressed size is larger than 2GB"); + } + return ByteBuffer.wrap(load()); + } + // Copy from FileChannel to byte array, then wrap in a ByteBuffer + if (sliceLength > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("File is larger than 2GB"); + } + return ByteBuffer.wrap(load()); + } + + @Override + public boolean equals(final Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + /** Close the slice. Unmaps any backing {@link MappedByteBuffer}. */ + @Override + public void close() { + if (!isClosed.getAndSet(true)) { + if (isTopLevelFileSlice && fileChannel != null) { + // Only close the FileChannel in the toplevel file slice, so that it is only + // closed once + try { + // Closing raf will also close the associated FileChannel + fileChannel.close(); + } catch (final IOException e) { + // Ignore + } + fileChannel = null; + } + fileChannel = null; + nestedJarHandler.markSliceAsClosed(this); + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/Slice.java b/src/main/java/nonapi/io/github/classgraph/fileslice/Slice.java new file mode 100644 index 000000000..78c842198 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/Slice.java @@ -0,0 +1,315 @@ + +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.github.classgraph.Resource; +import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; +import nonapi.io.github.classgraph.fileslice.reader.RandomAccessReader; +import nonapi.io.github.classgraph.utils.FileUtils; + +/** + * A slice of a {@link File}, {@link ByteBuffer} or {@link InputStream}. A single {@link Slice} instance should only + * be used by a single thread. + */ +public abstract class Slice implements Closeable { + /** The {@link NestedJarHandler}. */ + protected final NestedJarHandler nestedJarHandler; + + /** The parent slice. */ + protected final Slice parentSlice; + + /** The start position of the slice. */ + public final long sliceStartPos; + + /** The length of the slice, or -1L if unknown (for {@link InputStream}). */ + public long sliceLength; + + /** If true, the slice is a deflated zip entry, and needs to be inflated to access the content. */ + public final boolean isDeflatedZipEntry; + + /** If the slice is a deflated zip entry, this is the expected uncompressed length, or -1L if unknown. */ + public final long inflatedLengthHint; + + /** The cached hashCode. */ + private int hashCode; + + /** + * Constructor for treating a range of a slice as a sub-slice. + * + * @param parentSlice + * the parent slice + * @param offset + * the offset of the sub-slice within the parent slice + * @param length + * the length of the sub-slice + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @param nestedJarHandler + * the nested jar handler + */ + protected Slice(final Slice parentSlice, final long offset, final long length, final boolean isDeflatedZipEntry, + final long inflatedLengthHint, final NestedJarHandler nestedJarHandler) { + this.parentSlice = parentSlice; + final long parentSliceStartPos = parentSlice == null ? 0L : parentSlice.sliceStartPos; + this.sliceStartPos = parentSliceStartPos + offset; + this.sliceLength = length; + this.isDeflatedZipEntry = isDeflatedZipEntry; + this.inflatedLengthHint = inflatedLengthHint; + this.nestedJarHandler = nestedJarHandler; + + if (sliceStartPos < 0L) { + throw new IllegalArgumentException("Invalid startPos"); + } + if (length < 0L) { + throw new IllegalArgumentException("Invalid length"); + } + if (parentSlice != null && (sliceStartPos < parentSliceStartPos + || sliceStartPos + length > parentSliceStartPos + parentSlice.sliceLength)) { + throw new IllegalArgumentException("Child slice is not completely contained within parent slice"); + } + } + + /** + * Constructor. + * + * @param length + * the length + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @param nestedJarHandler + * the nested jar handler + */ + protected Slice(final long length, final boolean isDeflatedZipEntry, final long inflatedLengthHint, + final NestedJarHandler nestedJarHandler) { + this(/* parentSlice = */ null, 0L, length, isDeflatedZipEntry, inflatedLengthHint, nestedJarHandler); + } + + /** + * Get a child {@link Slice} from this parent {@link Slice}. The child slice must be smaller than the parent + * slice, and completely contained within it. + * + * @param offset + * The offset to start slicing from, relative to this parent slice's start position. + * @param length + * The length of the slice. + * @param isDeflatedZipEntry + * true if this is a deflated zip entry + * @param inflatedLengthHint + * the uncompressed size of a deflated zip entry, or -1 if unknown, or 0 of this is not a deflated + * zip entry. + * @return The child slice. + */ + public abstract Slice slice(long offset, long length, boolean isDeflatedZipEntry, + final long inflatedLengthHint); + + /** + * Open this {@link Slice} as an {@link InputStream}. + * + * @return the input stream + * @throws IOException + * if an inflater cannot be created for this {@link Slice}. + */ + public InputStream open() throws IOException { + return open(null); + } + + /** + * Open this {@link Slice} as an {@link InputStream}. + * + * @param resourceToClose + * the {@link Resource} to close when the returned {@code InputStream} is closed, or null if none. + * @return the input stream + * @throws IOException + * if an inflater cannot be created for this {@link Slice}. + */ + public InputStream open(final Resource resourceToClose) throws IOException { + final InputStream rawInputStream = new InputStream() { + RandomAccessReader randomAccessReader = randomAccessReader(); + private long currOff; + private long markOff; + private final byte[] byteBuf = new byte[1]; + private final AtomicBoolean closed = new AtomicBoolean(); + + @Override + public int read() throws IOException { + if (closed.get()) { + throw new IOException("Already closed"); + } + return read(byteBuf, 0, 1); + } + + // InputStream's default implementation of this method is very slow -- it calls read() + // for every byte. This method reads the maximum number of bytes possible in one call. + @Override + public int read(final byte[] buf, final int off, final int len) throws IOException { + if (closed.get()) { + throw new IOException("Already closed"); + } else if (len == 0) { + return 0; + } + final int numBytesToRead = Math.min(len, available()); + if (numBytesToRead < 1) { + return -1; + } + final int numBytesRead = randomAccessReader.read(currOff, buf, off, numBytesToRead); + if (numBytesRead > 0) { + currOff += numBytesRead; + } + return numBytesRead; + } + + @Override + public long skip(final long n) throws IOException { + if (closed.get()) { + throw new IOException("Already closed"); + } + final long newOff = Math.min(currOff + n, sliceLength); + final long skipped = newOff - currOff; + currOff = newOff; + return skipped; + } + + @Override + public int available() { + return (int) Math.min(Math.max(sliceLength - currOff, 0L), FileUtils.MAX_BUFFER_SIZE); + } + + @Override + public synchronized void mark(final int readlimit) { + // Ignore readlimit + markOff = currOff; + } + + @Override + public synchronized void reset() { + currOff = markOff; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void close() { + if (resourceToClose != null) { + try { + resourceToClose.close(); + } catch (final Exception e) { + // Ignore + } + } + closed.getAndSet(true); + } + }; + return isDeflatedZipEntry ? nestedJarHandler.openInflaterInputStream(rawInputStream) : rawInputStream; + } + + /** + * Create a new {@link RandomAccessReader} for this {@link Slice}. + * + * @return the random access reader + */ + public abstract RandomAccessReader randomAccessReader(); + + /** + * Load the slice as a byte array. + * + * @return the byte[] + * @throws IOException + * Signals that an I/O exception has occurred. + */ + public abstract byte[] load() throws IOException; + + /** + * Load the slice as a string. + * + * @return the string + * @throws IOException + * if slice cannot be read. + */ + public String loadAsString() throws IOException { + return new String(load(), StandardCharsets.UTF_8); + } + + /** + * Read the slice into a {@link ByteBuffer}. + * + * @return the byte buffer + * @throws IOException + * Signals that an I/O exception has occurred. + */ + public ByteBuffer read() throws IOException { + return ByteBuffer.wrap(load()); + } + + @Override + public void close() throws IOException { + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = (parentSlice == null ? 1 : parentSlice.hashCode()) ^ ((int) sliceStartPos * 7) + ^ ((int) sliceLength * 15); + if (hashCode == 0) { + hashCode = 1; + } + } + return hashCode; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } else if (!(o instanceof Slice)) { + return false; + } else { + final Slice other = (Slice) o; + return this.parentSlice == other.parentSlice && this.sliceStartPos == other.sliceStartPos + && this.sliceLength == other.sliceLength; + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/reader/ClassfileReader.java b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/ClassfileReader.java new file mode 100644 index 000000000..e007169ce --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/ClassfileReader.java @@ -0,0 +1,465 @@ + +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice.reader; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.Buffer; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; +import java.util.Arrays; + +import io.github.classgraph.Resource; +import nonapi.io.github.classgraph.fileslice.ArraySlice; +import nonapi.io.github.classgraph.fileslice.FileSlice; +import nonapi.io.github.classgraph.fileslice.Slice; +import nonapi.io.github.classgraph.utils.FileUtils; +import nonapi.io.github.classgraph.utils.StringUtils; + +/** + * A {@link Slice} reader that works as either a {@link RandomAccessReader} or a {@link SequentialReader}. The file + * is buffered up to the point it has been read so far. Reads in big endian order, as required by the + * classfile format. + */ +public class ClassfileReader implements RandomAccessReader, SequentialReader, Closeable { + /** The underlying resource to close when {@link ClassfileReader#close()} is called. */ + private Resource resourceToClose; + + /** If slice is deflated, a wrapper for {@link InflateInputStream}. */ + private InputStream inflaterInputStream; + + /** + * If slice is not deflated, a {@link RandomAccessReader} for either the {@link ArraySlice} or {@link FileSlice} + * concrete subclass. + */ + private RandomAccessReader randomAccessReader; + + /** Buffer. */ + private byte[] arr; + + /** The number of bytes used in arr. */ + private int arrUsed; + + /** The current read index within the slice. */ + private int currIdx; + + /** + * The length of the classfile if known (because it is not deflated), or -1 if unknown (because it is deflated). + */ + private int classfileLengthHint = -1; + + /** + * Initial buffer size. For most classfiles, only the first 16-64kb needs to be read (we don't read the + * bytecodes). + */ + private static final int INITIAL_BUF_SIZE = 16384; + + /** + * Read this many bytes each time there is a buffer underrun. This is smaller than 8k by 8 bytes to prevent the + * doubling of the array size when the last chunk doesn't quite fit within the 16kb of INITIAL_BUF_SIZE, since + * the number of bytes that can be requested is up to 8 (for longs). Otherwise we could request to read to (8kb + * * 2 + 8), which would double the size of the buffer to 32kb, but if we only need to read between 8kb and + * 16kb, then we unnecessarily copied the buffer content one extra time. + */ + private static final int BUF_CHUNK_SIZE = 8192 - 8; + + /** + * Constructor. + * + * @param slice + * the {@link Slice} to read. + * @param resourceToClose + * the resource to close when {@link ClassfileReader#close()} is called, or null. + * @throws IOException + * If an inflater cannot be opened on the {@link Slice}. + */ + public ClassfileReader(final Slice slice, final Resource resourceToClose) throws IOException { + this.classfileLengthHint = (int) slice.sliceLength; + this.resourceToClose = resourceToClose; + if (slice.isDeflatedZipEntry) { + // If this is a deflated slice, need to read from an InflaterInputStream to fill buffer + inflaterInputStream = slice.open(); + arr = new byte[INITIAL_BUF_SIZE]; + classfileLengthHint = (int) Math.min(slice.inflatedLengthHint, FileUtils.MAX_BUFFER_SIZE); + } else { + if (slice instanceof ArraySlice) { + // If slice is an ArraySlice, avoid copying by simply reusing the wrapped byte array + // in place of the buffer array, and mark it as fully loaded + final ArraySlice arraySlice = (ArraySlice) slice; + if (arraySlice.sliceStartPos == 0 && arraySlice.sliceLength == arraySlice.arr.length) { + // ArraySlice is the whole array + arr = arraySlice.arr; + } else { + // ArraySlice covers only a partial array, and this class doesn't support a starting + // offset, so copy the sliced part of the array to a new buffer + arr = Arrays.copyOfRange(arraySlice.arr, (int) arraySlice.sliceStartPos, + (int) (arraySlice.sliceStartPos + arraySlice.sliceLength)); + } + arrUsed = arr.length; + classfileLengthHint = arr.length; + } else { + // Otherwise this is a FileSlice -- need to fetch chunks of bytes using a random access reader + randomAccessReader = slice.randomAccessReader(); + arr = new byte[INITIAL_BUF_SIZE]; + classfileLengthHint = (int) Math.min(slice.sliceLength, FileUtils.MAX_BUFFER_SIZE); + } + } + } + + /** + * Constructor for reader of module {@link InputStream} (which is not deflated). + * + * @param inputStream + * the {@link InputStream} to read from. + * @param resourceToClose + * the underlying resource to close when {@link ClassfileReader#close()} is called, or null. + * @throws IOException + * If an inflater cannot be opened on the {@link Slice}. + */ + public ClassfileReader(final InputStream inputStream, final Resource resourceToClose) throws IOException { + inflaterInputStream = inputStream; + arr = new byte[INITIAL_BUF_SIZE]; + this.resourceToClose = resourceToClose; + } + + /** + * Curr pos. + * + * @return the current read position. + */ + public int currPos() { + return currIdx; + } + + /** + * Buf. + * + * @return the buffer. + */ + public byte[] buf() { + return arr; + } + + /** + * Called when there is a buffer underrun to ensure there are sufficient bytes available in the array to read + * the given number of bytes at the given start index. + * + * @param targetArrUsed + * the target value for {@link #arrUsed} (i.e. the number of bytes that must be filled in the array) + * @throws IOException + * Signals that an I/O exception has occurred. + */ + private void readTo(final int targetArrUsed) throws IOException { + // Array does not need to grow larger than the length hint (if the uncompressed size of the zip entry + // is an underestimate, classfile will be truncated). If -1, assume 2GB is the max size. + final int maxArrLen = classfileLengthHint == -1 ? FileUtils.MAX_BUFFER_SIZE : classfileLengthHint; + if (inflaterInputStream == null && randomAccessReader == null) { + // If neither inflaterInputStream nor randomAccessReader is set, then slice is an ArraySlice, + // and array is already "fully loaded" (the ArraySlice's backing array is used as the buffer). + throw new IOException("Tried to read past end of fixed array buffer"); + } + if (targetArrUsed > FileUtils.MAX_BUFFER_SIZE || targetArrUsed < 0 || arrUsed == maxArrLen) { + throw new IOException("Hit 2GB limit while trying to grow buffer array"); + } + + // Need to read at least BUF_CHUNK_SIZE (but don't overshoot past 2GB limit) + final int maxNewArrUsed = (int) Math.min(Math.max(targetArrUsed, (long) (arrUsed + BUF_CHUNK_SIZE)), + maxArrLen); + + // Double the size of the array if it's too small to contain the new chunk of bytes + long newArrLength = arr.length; + while (newArrLength < maxNewArrUsed) { + newArrLength = Math.min(maxNewArrUsed, newArrLength * 2L); + } + if (newArrLength > FileUtils.MAX_BUFFER_SIZE) { + throw new IOException("Hit 2GB limit while trying to grow buffer array"); + } + arr = Arrays.copyOf(arr, (int) Math.min(newArrLength, maxArrLen)); + + // Figure out the maximum number of bytes that can be read into the array + final int maxBytesToRead = arr.length - arrUsed; + + // Read a new chunk into the buffer, starting at position arrUsed + if (inflaterInputStream != null) { + // Read from inflater input stream + final int numRead = inflaterInputStream.read(arr, arrUsed, maxBytesToRead); + if (numRead > 0) { + arrUsed += numRead; + } + } else /* randomAccessReader == null, so this is a (non-deflated) FileSlice */ { + // Don't read past end of slice + final int bytesToRead = Math.min(maxBytesToRead, maxArrLen - arrUsed); + // Read bytes from FileSlice into arr + final int numBytesRead = randomAccessReader.read(/* srcOffset = */ arrUsed, /* dstArr = */ arr, + /* dstArrStart = */ arrUsed, /* numBytes = */ bytesToRead); + if (numBytesRead > 0) { + arrUsed += numBytesRead; + } + } + + // Check the buffer was able to be filled to the requested position + if (arrUsed < targetArrUsed) { + throw new IOException("Buffer underflow"); + } + } + + /** + * Ensure that the given number of bytes have been read into the buffer from the beginning of the slice. + * + * @param numBytes + * the number of bytes to ensure have been buffered + * @throws IOException + * on EOF or if the bytes could not be read. + */ + public void bufferTo(final int numBytes) throws IOException { + if (numBytes > arrUsed) { + readTo(numBytes); + } + } + + @Override + public int read(final long srcOffset, final byte[] dstArr, final int dstArrStart, final int numBytes) + throws IOException { + if (numBytes == 0) { + return 0; + } + final int idx = (int) srcOffset; + if (idx + numBytes > arrUsed) { + readTo(idx + numBytes); + } + final int numBytesToRead = Math.max(Math.min(numBytes, dstArr.length - dstArrStart), 0); + if (numBytesToRead == 0) { + return -1; + } + try { + System.arraycopy(arr, idx, dstArr, dstArrStart, numBytesToRead); + return numBytesToRead; + } catch (final IndexOutOfBoundsException e) { + throw new IOException("Read index out of bounds"); + } + } + + @Override + public int read(final long srcOffset, final ByteBuffer dstBuf, final int dstBufStart, final int numBytes) + throws IOException { + if (numBytes == 0) { + return 0; + } + final int idx = (int) srcOffset; + if (idx + numBytes > arrUsed) { + readTo(idx + numBytes); + } + final int numBytesToRead = Math.max(Math.min(numBytes, dstBuf.capacity() - dstBufStart), 0); + if (numBytesToRead == 0) { + return -1; + } + try { + ((Buffer) dstBuf).position(dstBufStart); + ((Buffer) dstBuf).limit(dstBufStart + numBytesToRead); + dstBuf.put(arr, idx, numBytesToRead); + return numBytesToRead; + } catch (BufferUnderflowException | IndexOutOfBoundsException | ReadOnlyBufferException e) { + throw new IOException("Read index out of bounds"); + } + } + + @Override + public byte readByte(final long offset) throws IOException { + final int idx = (int) offset; + if (idx + 1 > arrUsed) { + readTo(idx + 1); + } + return arr[idx]; + } + + @Override + public int readUnsignedByte(final long offset) throws IOException { + final int idx = (int) offset; + if (idx + 1 > arrUsed) { + readTo(idx + 1); + } + return arr[idx] & 0xff; + } + + @Override + public short readShort(final long offset) throws IOException { + return (short) readUnsignedShort(offset); + } + + @Override + public int readUnsignedShort(final long offset) throws IOException { + final int idx = (int) offset; + if (idx + 2 > arrUsed) { + readTo(idx + 2); + } + return ((arr[idx] & 0xff) << 8) // + | (arr[idx + 1] & 0xff); + } + + @Override + public int readInt(final long offset) throws IOException { + final int idx = (int) offset; + if (idx + 4 > arrUsed) { + readTo(idx + 4); + } + return ((arr[idx] & 0xff) << 24) // + | ((arr[idx + 1] & 0xff) << 16) // + | ((arr[idx + 2] & 0xff) << 8) // + | (arr[idx + 3] & 0xff); + } + + @Override + public long readUnsignedInt(final long offset) throws IOException { + return readInt(offset) & 0xffffffffL; + } + + @Override + public long readLong(final long offset) throws IOException { + final int idx = (int) offset; + if (idx + 8 > arrUsed) { + readTo(idx + 8); + } + return ((arr[idx] & 0xffL) << 56) // + | ((arr[idx + 1] & 0xffL) << 48) // + | ((arr[idx + 2] & 0xffL) << 40) // + | ((arr[idx + 3] & 0xffL) << 32) // + | ((arr[idx + 4] & 0xffL) << 24) // + | ((arr[idx + 5] & 0xffL) << 16) // + | ((arr[idx + 6] & 0xffL) << 8) // + | (arr[idx + 7] & 0xffL); + } + + @Override + public byte readByte() throws IOException { + final byte val = readByte(currIdx); + currIdx++; + return val; + } + + @Override + public int readUnsignedByte() throws IOException { + final int val = readUnsignedByte(currIdx); + currIdx++; + return val; + } + + @Override + public short readShort() throws IOException { + final short val = readShort(currIdx); + currIdx += 2; + return val; + } + + @Override + public int readUnsignedShort() throws IOException { + final int val = readUnsignedShort(currIdx); + currIdx += 2; + return val; + } + + @Override + public int readInt() throws IOException { + final int val = readInt(currIdx); + currIdx += 4; + return val; + } + + @Override + public long readUnsignedInt() throws IOException { + final long val = readUnsignedInt(currIdx); + currIdx += 4; + return val; + } + + @Override + public long readLong() throws IOException { + final long val = readLong(currIdx); + currIdx += 8; + return val; + } + + @Override + public void skip(final int bytesToSkip) throws IOException { + if (bytesToSkip < 0) { + throw new IllegalArgumentException("Tried to skip a negative number of bytes"); + } + final int idx = currIdx; + if (idx + bytesToSkip > arrUsed) { + readTo(idx + bytesToSkip); + } + currIdx += bytesToSkip; + } + + @Override + public String readString(final long offset, final int numBytes, final boolean replaceSlashWithDot, + final boolean stripLSemicolon) throws IOException { + final int idx = (int) offset; + if (idx + numBytes > arrUsed) { + readTo(idx + numBytes); + } + return StringUtils.readString(arr, idx, numBytes, replaceSlashWithDot, stripLSemicolon); + } + + @Override + public String readString(final int numBytes, final boolean replaceSlashWithDot, final boolean stripLSemicolon) + throws IOException { + final String val = StringUtils.readString(arr, currIdx, numBytes, replaceSlashWithDot, stripLSemicolon); + currIdx += numBytes; + return val; + } + + @Override + public String readString(final long offset, final int numBytes) throws IOException { + return readString(offset, numBytes, false, false); + } + + @Override + public String readString(final int numBytes) throws IOException { + return readString(numBytes, false, false); + } + + @Override + public void close() { + try { + if (inflaterInputStream != null) { + inflaterInputStream.close(); + inflaterInputStream = null; + } + if (resourceToClose != null) { + resourceToClose.close(); + resourceToClose = null; + } + } catch (final Exception e) { + // Ignore + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessArrayReader.java b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessArrayReader.java new file mode 100644 index 000000000..87fafbbe3 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessArrayReader.java @@ -0,0 +1,177 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice.reader; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; + +import nonapi.io.github.classgraph.utils.StringUtils; + +/** + * {@link RandomAccessReader} backed by a byte array. Reads in little endian order, as required by the + * zipfile format. + */ +public class RandomAccessArrayReader implements RandomAccessReader { + /** The array. */ + private final byte[] arr; + + /** The start index of the slice within the array. */ + private final int sliceStartPos; + + /** The length of the slice within the array. */ + private final int sliceLength; + + /** + * Constructor for slicing an array. + * + * @param arr + * the array to slice. + * @param sliceStartPos + * the start index of the slice within the array. + * @param sliceLength + * the length of the slice within the array. + */ + public RandomAccessArrayReader(final byte[] arr, final int sliceStartPos, final int sliceLength) { + this.arr = arr; + this.sliceStartPos = sliceStartPos; + this.sliceLength = sliceLength; + } + + @Override + public int read(final long srcOffset, final byte[] dstArr, final int dstArrStart, final int numBytes) + throws IOException { + if (numBytes == 0) { + return 0; + } + if (srcOffset < 0L || numBytes < 0 || numBytes > sliceLength - srcOffset) { + throw new IOException("Read index out of bounds"); + } + try { + final int numBytesToRead = Math.max(Math.min(numBytes, dstArr.length - dstArrStart), 0); + if (numBytesToRead == 0) { + return -1; + } + final int srcStart = (int) (sliceStartPos + srcOffset); + System.arraycopy(arr, srcStart, dstArr, dstArrStart, numBytesToRead); + return numBytesToRead; + } catch (final IndexOutOfBoundsException e) { + throw new IOException("Read index out of bounds"); + } + } + + @Override + public int read(final long srcOffset, final ByteBuffer dstBuf, final int dstBufStart, final int numBytes) + throws IOException { + if (numBytes == 0) { + return 0; + } + if (srcOffset < 0L || numBytes < 0 || numBytes > sliceLength - srcOffset) { + throw new IOException("Read index out of bounds"); + } + try { + final int numBytesToRead = Math.max(Math.min(numBytes, dstBuf.capacity() - dstBufStart), 0); + if (numBytesToRead == 0) { + return -1; + } + final int srcStart = (int) (sliceStartPos + srcOffset); + ((Buffer) dstBuf).position(dstBufStart); + ((Buffer) dstBuf).limit(dstBufStart + numBytesToRead); + dstBuf.put(arr, srcStart, numBytesToRead); + return numBytesToRead; + } catch (BufferUnderflowException | IndexOutOfBoundsException | ReadOnlyBufferException e) { + throw new IOException("Read index out of bounds"); + } + } + + @Override + public byte readByte(final long offset) throws IOException { + final int idx = sliceStartPos + (int) offset; + return arr[idx]; + } + + @Override + public int readUnsignedByte(final long offset) throws IOException { + final int idx = sliceStartPos + (int) offset; + return arr[idx] & 0xff; + } + + @Override + public short readShort(final long offset) throws IOException { + return (short) readUnsignedShort(offset); + } + + @Override + public int readUnsignedShort(final long offset) throws IOException { + final int idx = sliceStartPos + (int) offset; + return ((arr[idx + 1] & 0xff) << 8) // + | (arr[idx] & 0xff); + } + + @Override + public int readInt(final long offset) throws IOException { + final int idx = sliceStartPos + (int) offset; + return ((arr[idx + 3] & 0xff) << 24) // + | ((arr[idx + 2] & 0xff) << 16) // + | ((arr[idx + 1] & 0xff) << 8) // + | (arr[idx] & 0xff); + } + + @Override + public long readUnsignedInt(final long offset) throws IOException { + return readInt(offset) & 0xffffffffL; + } + + @Override + public long readLong(final long offset) throws IOException { + final int idx = sliceStartPos + (int) offset; + return ((arr[idx + 7] & 0xffL) << 56) // + | ((arr[idx + 6] & 0xffL) << 48) // + | ((arr[idx + 5] & 0xffL) << 40) // + | ((arr[idx + 4] & 0xffL) << 32) // + | ((arr[idx + 3] & 0xffL) << 24) // + | ((arr[idx + 2] & 0xffL) << 16) // + | ((arr[idx + 1] & 0xffL) << 8) // + | (arr[idx] & 0xffL); + } + + @Override + public String readString(final long offset, final int numBytes, final boolean replaceSlashWithDot, + final boolean stripLSemicolon) throws IOException { + final int idx = sliceStartPos + (int) offset; + return StringUtils.readString(arr, idx, numBytes, replaceSlashWithDot, stripLSemicolon); + } + + @Override + public String readString(final long offset, final int numBytes) throws IOException { + return readString(offset, numBytes, false, false); + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessByteBufferReader.java b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessByteBufferReader.java new file mode 100644 index 000000000..a981dfa69 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessByteBufferReader.java @@ -0,0 +1,180 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice.reader; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ReadOnlyBufferException; + +import nonapi.io.github.classgraph.utils.StringUtils; + +/** + * {@link RandomAccessReader} for a {@link ByteBuffer}. Reads in little endian order, as required by the + * zipfile format. + */ +public class RandomAccessByteBufferReader implements RandomAccessReader { + /** The byte buffer. */ + private final ByteBuffer byteBuffer; + + /** The slice start pos. */ + private final int sliceStartPos; + + /** The slice length. */ + private final int sliceLength; + + /** + * Constructor. + * + * @param byteBuffer + * the byte buffer + * @param sliceStartPos + * the slice start pos + * @param sliceLength + * the slice length + */ + public RandomAccessByteBufferReader(final ByteBuffer byteBuffer, final long sliceStartPos, + final long sliceLength) { + this.byteBuffer = byteBuffer.duplicate(); + this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.sliceStartPos = (int) sliceStartPos; + this.sliceLength = (int) sliceLength; + ((Buffer) this.byteBuffer).position(this.sliceStartPos); + ((Buffer) this.byteBuffer).limit(this.sliceStartPos + this.sliceLength); + } + + @Override + public int read(final long srcOffset, final byte[] dstArr, final int dstArrStart, final int numBytes) + throws IOException { + if (numBytes == 0) { + return 0; + } + if (srcOffset < 0L || numBytes < 0 || numBytes > sliceLength - srcOffset) { + throw new IOException("Read index out of bounds"); + } + try { + final int numBytesToRead = Math.max(Math.min(numBytes, dstArr.length - dstArrStart), 0); + if (numBytesToRead == 0) { + return -1; + } + final int srcStart = (int) srcOffset; + ((Buffer) byteBuffer).position(sliceStartPos + srcStart); + byteBuffer.get(dstArr, dstArrStart, numBytesToRead); + ((Buffer) byteBuffer).position(sliceStartPos); + return numBytesToRead; + } catch (final IndexOutOfBoundsException e) { + throw new IOException("Read index out of bounds"); + } + } + + @Override + public int read(final long srcOffset, final ByteBuffer dstBuf, final int dstBufStart, final int numBytes) + throws IOException { + if (numBytes == 0) { + return 0; + } + if (srcOffset < 0L || numBytes < 0 || numBytes > sliceLength - srcOffset) { + throw new IOException("Read index out of bounds"); + } + try { + final int numBytesToRead = Math.max(Math.min(numBytes, dstBuf.capacity() - dstBufStart), 0); + if (numBytesToRead == 0) { + return -1; + } + final int srcStart = (int) (sliceStartPos + srcOffset); + ((Buffer) byteBuffer).position(srcStart); + ((Buffer) dstBuf).position(dstBufStart); + ((Buffer) dstBuf).limit(dstBufStart + numBytesToRead); + dstBuf.put(byteBuffer); + ((Buffer) byteBuffer).limit(sliceStartPos + sliceLength); + ((Buffer) byteBuffer).position(sliceStartPos); + return numBytesToRead; + } catch (BufferUnderflowException | IndexOutOfBoundsException | ReadOnlyBufferException e) { + throw new IOException("Read index out of bounds"); + } + } + + @Override + public byte readByte(final long offset) throws IOException { + final int idx = (int) (sliceStartPos + offset); + return byteBuffer.get(idx); + } + + @Override + public int readUnsignedByte(final long offset) throws IOException { + final int idx = (int) (sliceStartPos + offset); + return byteBuffer.get(idx) & 0xff; + } + + @Override + public int readUnsignedShort(final long offset) throws IOException { + final int idx = (int) (sliceStartPos + offset); + return byteBuffer.getShort(idx) & 0xff; + } + + @Override + public short readShort(final long offset) throws IOException { + return (short) readUnsignedShort(offset); + } + + @Override + public int readInt(final long offset) throws IOException { + final int idx = (int) (sliceStartPos + offset); + return byteBuffer.getInt(idx); + } + + @Override + public long readUnsignedInt(final long offset) throws IOException { + return readInt(offset) & 0xffffffffL; + } + + @Override + public long readLong(final long offset) throws IOException { + final int idx = (int) (sliceStartPos + offset); + return byteBuffer.getLong(idx); + } + + @Override + public String readString(final long offset, final int numBytes, final boolean replaceSlashWithDot, + final boolean stripLSemicolon) throws IOException { + final int idx = (int) (sliceStartPos + offset); + final byte[] arr = new byte[numBytes]; + if (read(offset, arr, 0, numBytes) < numBytes) { + throw new IOException("Premature EOF while reading string"); + } + return StringUtils.readString(arr, idx, numBytes, replaceSlashWithDot, stripLSemicolon); + } + + @Override + public String readString(final long offset, final int numBytes) throws IOException { + return readString(offset, numBytes, false, false); + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessFileChannelReader.java b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessFileChannelReader.java new file mode 100644 index 000000000..937ba0e5b --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessFileChannelReader.java @@ -0,0 +1,206 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice.reader; + +import java.io.File; +import java.io.IOException; +import java.nio.Buffer; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import nonapi.io.github.classgraph.utils.StringUtils; + +/** + * {@link RandomAccessReader} for a {@link File}. Reads in little endian order, as required by the zipfile + * format. + */ +public class RandomAccessFileChannelReader implements RandomAccessReader { + + /** The file channel. */ + private final FileChannel fileChannel; + + /** The slice start pos. */ + private final long sliceStartPos; + + /** The slice length. */ + private final long sliceLength; + + /** The reusable byte buffer. */ + private ByteBuffer reusableByteBuffer; + + /** The scratch arr. */ + private final byte[] scratchArr = new byte[8]; + + /** The scratch byte buf. */ + private final ByteBuffer scratchByteBuf = ByteBuffer.wrap(scratchArr); + + /** The utf 8 bytes. */ + private byte[] utf8Bytes; + + /** + * Constructor. + * + * @param fileChannel + * the file channel + * @param sliceStartPos + * the slice start pos + * @param sliceLength + * the slice length + */ + public RandomAccessFileChannelReader(final FileChannel fileChannel, final long sliceStartPos, + final long sliceLength) { + this.fileChannel = fileChannel; + this.sliceStartPos = sliceStartPos; + this.sliceLength = sliceLength; + } + + @Override + public int read(final long srcOffset, final ByteBuffer dstBuf, final int dstBufStart, final int numBytes) + throws IOException { + if (numBytes == 0) { + return 0; + } + try { + if (srcOffset < 0L || numBytes < 0 || numBytes > sliceLength - srcOffset) { + throw new IOException("Read index out of bounds"); + } + final long srcStart = sliceStartPos + srcOffset; + ((Buffer) dstBuf).position(dstBufStart); + ((Buffer) dstBuf).limit(dstBufStart + numBytes); + final int numBytesRead = fileChannel.read(dstBuf, srcStart); + return numBytesRead == 0 ? -1 : numBytesRead; + + } catch (BufferUnderflowException | IndexOutOfBoundsException e) { + throw new IOException("Read index out of bounds"); + } + } + + @Override + public int read(final long srcOffset, final byte[] dstArr, final int dstArrStart, final int numBytes) + throws IOException { + if (numBytes == 0) { + return 0; + } + try { + if (srcOffset < 0L || numBytes < 0 || numBytes > sliceLength - srcOffset) { + throw new IOException("Read index out of bounds"); + } + if (reusableByteBuffer == null || reusableByteBuffer.array() != dstArr) { + // If reusableByteBuffer is not set, or wraps a different array from a previous operation, + // wrap dstArr with a new ByteBuffer + reusableByteBuffer = ByteBuffer.wrap(dstArr); + } + // Read into reusableByteBuffer, which is backed with dstArr + return read(srcOffset, reusableByteBuffer, dstArrStart, numBytes); + + } catch (BufferUnderflowException | IndexOutOfBoundsException e) { + throw new IOException("Read index out of bounds"); + } + } + + @Override + public byte readByte(final long offset) throws IOException { + if (read(offset, scratchByteBuf, 0, 1) < 1) { + throw new IOException("Premature EOF"); + } + return scratchArr[0]; + } + + @Override + public int readUnsignedByte(final long offset) throws IOException { + if (read(offset, scratchByteBuf, 0, 1) < 1) { + throw new IOException("Premature EOF"); + } + return scratchArr[0] & 0xff; + } + + @Override + public short readShort(final long offset) throws IOException { + return (short) readUnsignedShort(offset); + } + + @Override + public int readUnsignedShort(final long offset) throws IOException { + if (read(offset, scratchByteBuf, 0, 2) < 2) { + throw new IOException("Premature EOF"); + } + return ((scratchArr[1] & 0xff) << 8) // + | (scratchArr[0] & 0xff); + } + + @Override + public int readInt(final long offset) throws IOException { + if (read(offset, scratchByteBuf, 0, 4) < 4) { + throw new IOException("Premature EOF"); + } + return ((scratchArr[3] & 0xff) << 24) // + | ((scratchArr[2] & 0xff) << 16) // + | ((scratchArr[1] & 0xff) << 8) // + | (scratchArr[0] & 0xff); + } + + @Override + public long readUnsignedInt(final long offset) throws IOException { + return readInt(offset) & 0xffffffffL; + } + + @Override + public long readLong(final long offset) throws IOException { + if (read(offset, scratchByteBuf, 0, 8) < 8) { + throw new IOException("Premature EOF"); + } + return ((scratchArr[7] & 0xffL) << 56) // + | ((scratchArr[6] & 0xffL) << 48) // + | ((scratchArr[5] & 0xffL) << 40) // + | ((scratchArr[4] & 0xffL) << 32) // + | ((scratchArr[3] & 0xffL) << 24) // + | ((scratchArr[2] & 0xffL) << 16) // + | ((scratchArr[1] & 0xffL) << 8) // + | (scratchArr[0] & 0xffL); + } + + @Override + public String readString(final long offset, final int numBytes, final boolean replaceSlashWithDot, + final boolean stripLSemicolon) throws IOException { + // Reuse UTF8 buffer array if it's non-null from a previous call, and if it's big enough + if (utf8Bytes == null || utf8Bytes.length < numBytes) { + utf8Bytes = new byte[numBytes]; + } + if (read(offset, utf8Bytes, 0, numBytes) < numBytes) { + throw new IOException("Premature EOF"); + } + return StringUtils.readString(utf8Bytes, 0, numBytes, replaceSlashWithDot, stripLSemicolon); + } + + @Override + public String readString(final long offset, final int numBytes) throws IOException { + return readString(offset, numBytes, false, false); + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessReader.java b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessReader.java new file mode 100644 index 000000000..65f6b3816 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessReader.java @@ -0,0 +1,178 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice.reader; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** Interface for random access to values in byte order. */ +public interface RandomAccessReader { + /** + * Read bytes into a {@link ByteBuffer}. + * + * @param srcOffset + * The offset to start reading from. + * @param dstBuf + * The {@link ByteBuffer} to write into. + * @param dstBufStart + * The offset within the destination buffer to start writing at. + * @param numBytes + * The number of bytes to read. + * @return The number of bytes actually read, or -1 if no more bytes could be read. + * @throws IOException + * If there was an exception while reading. + */ + int read(long srcOffset, ByteBuffer dstBuf, int dstBufStart, int numBytes) throws IOException; + + /** + * Read bytes into a byte array. + * + * @param srcOffset + * The offset to start reading from. + * @param dstArr + * The byte array to write into. + * @param dstArrStart + * The offset within the destination array to start writing at. + * @param numBytes + * The number of bytes to read. + * @return The number of bytes actually read, or -1 if no more bytes could be read. + * @throws IOException + * If there was an exception while reading. + */ + int read(long srcOffset, byte[] dstArr, int dstArrStart, int numBytes) throws IOException; + + /** + * Read a byte at a specific offset (without changing the current cursor offset). + * + * @param offset + * The buffer offset to read from. + * @return The byte at the offset. + * @throws IOException + * If there was an exception while reading. + */ + byte readByte(final long offset) throws IOException; + + /** + * Read an unsigned byte at a specific offset (without changing the current cursor offset). + * + * @param offset + * The buffer offset to read from. + * @return The unsigned byte at the offset. + * @throws IOException + * If there was an exception while reading. + */ + int readUnsignedByte(final long offset) throws IOException; + + /** + * Read a short at a specific offset (without changing the current cursor offset). + * + * @param offset + * The buffer offset to read from. + * @return The short at the offset. + * @throws IOException + * If there was an exception while reading. + */ + short readShort(final long offset) throws IOException; + + /** + * Read a unsigned short at a specific offset (without changing the current cursor offset). + * + * @param offset + * The buffer offset to read from. + * @return The unsigned short at the offset. + * @throws IOException + * If there was an exception while reading. + */ + int readUnsignedShort(final long offset) throws IOException; + + /** + * Read a int at a specific offset (without changing the current cursor offset). + * + * @param offset + * The buffer offset to read from. + * @return The int at the offset. + * @throws IOException + * If there was an exception while reading. + */ + int readInt(final long offset) throws IOException; + + /** + * Read a unsigned int at a specific offset (without changing the current cursor offset). + * + * @param offset + * The buffer offset to read from. + * @return The int at the offset, as a long. + * @throws IOException + * If there was an exception while reading. + */ + long readUnsignedInt(final long offset) throws IOException; + + /** + * Read a long at a specific offset (without changing the current cursor offset). + * + * @param offset + * The buffer offset to read from. + * @return The long at the offset. + * @throws IOException + * If there was an exception while reading. + */ + long readLong(final long offset) throws IOException; + + /** + * Reads the "modified UTF8" format defined in the Java classfile spec, optionally replacing '/' with '.', and + * optionally removing the prefix "L" and the suffix ";". + * + * @param offset + * The start offset of the string. + * @param numBytes + * The number of bytes of the UTF8 encoding of the string. + * @param replaceSlashWithDot + * If true, replace '/' with '.'. + * @param stripLSemicolon + * If true, string final ';' character. + * @return The string. + * @throws IOException + * If an I/O exception occurs. + */ + String readString(final long offset, final int numBytes, final boolean replaceSlashWithDot, + final boolean stripLSemicolon) throws IOException; + + /** + * Reads the "modified UTF8" format defined in the Java classfile spec. + * + * @param offset + * The start offset of the string. + * @param numBytes + * The number of bytes of the UTF8 encoding of the string. + * @return The string. + * @throws IOException + * If an I/O exception occurs. + */ + String readString(final long offset, final int numBytes) throws IOException; +} diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/reader/SequentialReader.java b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/SequentialReader.java new file mode 100644 index 000000000..a86ad8c79 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/SequentialReader.java @@ -0,0 +1,135 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2020 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.fileslice.reader; + +import java.io.IOException; + +/** Interface for sequentially reading values in byte order. */ +public interface SequentialReader { + /** + * Read a byte at the current cursor position. + * + * @return The byte at the current cursor position. + * @throws IOException + * If there was an exception while reading. + */ + byte readByte() throws IOException; + + /** + * Read an unsigned byte at the current cursor position. + * + * @return The unsigned byte at the current cursor position. + * @throws IOException + * If there was an exception while reading. + */ + int readUnsignedByte() throws IOException; + + /** + * Read a short at the current cursor position. + * + * @return The short at the current cursor position. + * @throws IOException + * If there was an exception while reading. + */ + short readShort() throws IOException; + + /** + * Read a unsigned short at the current cursor position. + * + * @return The unsigned shortat the current cursor position. + * @throws IOException + * If there was an exception while reading. + */ + int readUnsignedShort() throws IOException; + + /** + * Read a int at the current cursor position. + * + * @return The int at the current cursor position. + * @throws IOException + * If there was an exception while reading. + */ + int readInt() throws IOException; + + /** + * Read a unsigned int at the current cursor position. + * + * @return The int at the current cursor position, as a long. + * @throws IOException + * If there was an exception while reading. + */ + long readUnsignedInt() throws IOException; + + /** + * Read a long at the current cursor position. + * + * @return The long at the current cursor position. + * @throws IOException + * If there was an exception while reading. + */ + long readLong() throws IOException; + + /** + * Skip the given number of bytes. + * + * @param bytesToSkip + * The number of bytes to skip. + * @throws IOException + * If there was an exception while reading. + */ + void skip(final int bytesToSkip) throws IOException; + + /** + * Reads the "modified UTF8" format defined in the Java classfile spec, optionally replacing '/' with '.', and + * optionally removing the prefix "L" and the suffix ";". + * + * @param numBytes + * The number of bytes of the UTF8 encoding of the string. + * @param replaceSlashWithDot + * If true, replace '/' with '.'. + * @param stripLSemicolon + * If true, string final ';' character. + * @return The string. + * @throws IOException + * If an I/O exception occurs. + */ + String readString(final int numBytes, final boolean replaceSlashWithDot, final boolean stripLSemicolon) + throws IOException; + + /** + * Reads the "modified UTF8" format defined in the Java classfile spec. + * + * @param numBytes + * The number of bytes of the UTF8 encoding of the string. + * @return The string. + * @throws IOException + * If an I/O exception occurs. + */ + String readString(final int numBytes) throws IOException; +} diff --git a/src/main/java/nonapi/io/github/classgraph/json/ClassFieldCache.java b/src/main/java/nonapi/io/github/classgraph/json/ClassFieldCache.java index e9b779394..1fbb1897b 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/ClassFieldCache.java +++ b/src/main/java/nonapi/io/github/classgraph/json/ClassFieldCache.java @@ -61,7 +61,7 @@ import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; -import io.github.classgraph.ClassGraphException; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; /** * A cache of field types and associated constructors for each encountered class, used to speed up constructor @@ -79,6 +79,9 @@ class ClassFieldCache { private final boolean onlySerializePublicFields; /** The default constructor for each concrete type. */ + // TODO: replace these with constructor MethodHandles for speed + // TODO: (although MethodHandles are disabled for now, due to Animal Sniffer bug): + // https://github.com/mojohaus/animal-sniffer/issues/67 private final Map, Constructor> defaultConstructorForConcreteType = new HashMap<>(); /** The constructor with size hint for each concrete type. */ @@ -87,13 +90,15 @@ class ClassFieldCache { /** Placeholder constructor to signify no constructor was found previously. */ private static final Constructor NO_CONSTRUCTOR; + ReflectionUtils reflectionUtils; + static { try { NO_CONSTRUCTOR = NoConstructor.class.getDeclaredConstructor(); } catch (NoSuchMethodException | SecurityException e) { // Should not happen - throw ClassGraphException.newClassGraphException( - "Could not find or access constructor for " + NoConstructor.class.getName(), e); + throw new RuntimeException("Could not find or access constructor for " + NoConstructor.class.getName(), + e); } } @@ -115,9 +120,11 @@ public NoConstructor() { * @param onlySerializePublicFields * Set this to true if you only want to serialize public fields (ignored for deserialization). */ - ClassFieldCache(final boolean forDeserialization, final boolean onlySerializePublicFields) { + ClassFieldCache(final boolean forDeserialization, final boolean onlySerializePublicFields, + final ReflectionUtils reflectionUtils) { this.resolveTypes = forDeserialization; this.onlySerializePublicFields = !forDeserialization && onlySerializePublicFields; + this.reflectionUtils = reflectionUtils; } /** @@ -131,8 +138,8 @@ public NoConstructor() { ClassFields get(final Class cls) { ClassFields classFields = classToClassFields.get(cls); if (classFields == null) { - classToClassFields.put(cls, - classFields = new ClassFields(cls, resolveTypes, onlySerializePublicFields, this)); + classToClassFields.put(cls, classFields = new ClassFields(cls, resolveTypes, onlySerializePublicFields, + this, reflectionUtils)); } return classFields; } @@ -203,7 +210,7 @@ Constructor getDefaultConstructorForConcreteTypeOf(final Class cls) { && (c != Object.class || cls == Object.class); c = c.getSuperclass()) { try { final Constructor defaultConstructor = c.getDeclaredConstructor(); - JSONUtils.isAccessibleOrMakeAccessible(defaultConstructor); + JSONUtils.makeAccessible(defaultConstructor, reflectionUtils); // Store found constructor in cache defaultConstructorForConcreteType.put(cls, defaultConstructor); return defaultConstructor; @@ -238,7 +245,7 @@ Constructor getConstructorWithSizeHintForConcreteTypeOf(final Class cls) { && (c != Object.class || cls == Object.class); c = c.getSuperclass()) { try { final Constructor constructorWithSizeHint = c.getDeclaredConstructor(Integer.TYPE); - JSONUtils.isAccessibleOrMakeAccessible(constructorWithSizeHint); + JSONUtils.makeAccessible(constructorWithSizeHint, reflectionUtils); // Store found constructor in cache constructorForConcreteTypeWithSizeHint.put(cls, constructorWithSizeHint); return constructorWithSizeHint; diff --git a/src/main/java/nonapi/io/github/classgraph/json/ClassFields.java b/src/main/java/nonapi/io/github/classgraph/json/ClassFields.java index dcee9e8cb..dae47b6c0 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/ClassFields.java +++ b/src/main/java/nonapi/io/github/classgraph/json/ClassFields.java @@ -32,12 +32,17 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import io.github.classgraph.ScanResult; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; + /** * The list of fields that can be (de)serialized (non-final, non-transient, non-synthetic, accessible), and their * corresponding resolved (concrete) types. @@ -61,8 +66,35 @@ class ClassFields { final Map fieldNameToFieldTypeInfo = new HashMap<>(); /** If non-null, this is the field that has an {@link Id} annotation. */ + // TODO: replace this with getter/setter MethodHandles for speed Field idField; + /** Used to sort fields into deterministic order. */ + private static final Comparator FIELD_NAME_ORDER_COMPARATOR = // + new Comparator() { + @Override + public int compare(final Field a, final Field b) { + return a.getName().compareTo(b.getName()); + } + }; + + /** + * Used to sort fields into deterministic order for SerializationFormat class (which needs to have "format" + * field in first position for ClassGraph's serialization format) (#383). + */ + private static final Comparator SERIALIZATION_FORMAT_FIELD_NAME_ORDER_COMPARATOR = // + new Comparator() { + @Override + public int compare(final Field a, final Field b) { + return a.getName().equals("format") ? -1 + : b.getName().equals("format") ? 1 : a.getName().compareTo(b.getName()); + } + }; + + /** The name of the SerializationFormat class (used by ClassGraph to serialize a ScanResult). */ + private static final String SERIALIZATION_FORMAT_CLASS_NAME = ScanResult.class.getName() + + "$SerializationFormat"; + /** * Constructor. * @@ -76,7 +108,7 @@ class ClassFields { * the class field cache */ public ClassFields(final Class cls, final boolean resolveTypes, final boolean onlySerializePublicFields, - final ClassFieldCache classFieldCache) { + final ClassFieldCache classFieldCache, final ReflectionUtils reflectionUtils) { // Find declared accessible fields in all superclasses, and resolve generic types final Set visibleFieldNames = new HashSet<>(); @@ -94,7 +126,16 @@ public ClassFields(final Class cls, final boolean resolveTypes, final boolean // Class definitions should not be of type WildcardType or GenericArrayType throw new IllegalArgumentException("Illegal class type: " + currType); } + + // getDeclaredFields() does not guarantee any given order, so need to sort fields. (#383) final Field[] fields = currRawType.getDeclaredFields(); + Arrays.sort(fields, cls.getName().equals(SERIALIZATION_FORMAT_CLASS_NAME) + // Special sort order for SerializationFormat class: put "format" field first + ? SERIALIZATION_FORMAT_FIELD_NAME_ORDER_COMPARATOR + // Otherwise just sort by name so that order is deterministic + : FIELD_NAME_ORDER_COMPARATOR); + + // Find any @Id-annotated field, and get Field type info final List fieldOrderWithinClass = new ArrayList<>(); for (final Field field : fields) { // Mask superclass fields if subclass has a field of the same name @@ -110,7 +151,7 @@ public ClassFields(final Class cls, final boolean resolveTypes, final boolean idField = field; } - if (JSONUtils.fieldIsSerializable(field, onlySerializePublicFields)) { + if (JSONUtils.fieldIsSerializable(field, onlySerializePublicFields, reflectionUtils)) { // Resolve field type variables, if any, using the current type resolutions. This will // completely resolve some types (in the superclass), if the subclass extends a concrete // version of a generic superclass, but it will only partially resolve variables in diff --git a/src/main/java/nonapi/io/github/classgraph/json/FieldTypeInfo.java b/src/main/java/nonapi/io/github/classgraph/json/FieldTypeInfo.java index 84eaaf076..c4de8f3fe 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/FieldTypeInfo.java +++ b/src/main/java/nonapi/io/github/classgraph/json/FieldTypeInfo.java @@ -79,24 +79,26 @@ class FieldTypeInfo { * The Enum PrimitiveType. */ private enum PrimitiveType { - /** The non primitive. */ + /** Non-primitive type. */ NON_PRIMITIVE, - /** The integer. */ + /** Integer type. */ INTEGER, - /** The long. */ + /** Long type. */ LONG, - /** The short. */ + /** Short type. */ SHORT, - /** The double. */ + /** Double type. */ DOUBLE, - /** The float. */ + /** Float type. */ FLOAT, - /** The boolean. */ + /** Boolean type. */ BOOLEAN, - /** The byte. */ + /** Byte type. */ BYTE, - /** The character. */ - CHARACTER; + /** Character type. */ + CHARACTER, + /** Class reference */ + CLASS_REF } /** @@ -161,6 +163,8 @@ public FieldTypeInfo(final Field field, final Type fieldTypePartiallyResolved, this.primitiveType = PrimitiveType.BYTE; } else if (fieldRawType == Character.TYPE) { this.primitiveType = PrimitiveType.CHARACTER; + } else if (fieldRawType == Class.class) { + this.primitiveType = PrimitiveType.CLASS_REF; } else { this.primitiveType = PrimitiveType.NON_PRIMITIVE; } @@ -263,6 +267,13 @@ void setFieldValue(final Object containingObj, final Object value) { case NON_PRIMITIVE: field.set(containingObj, value); break; + case CLASS_REF: + if (!(value instanceof Class)) { + throw new IllegalArgumentException( + "Expected value of type Class; got " + value.getClass().getName()); + } + field.set(containingObj, value); + break; case INTEGER: if (!(value instanceof Integer)) { throw new IllegalArgumentException( diff --git a/src/main/java/nonapi/io/github/classgraph/json/JSONDeserializer.java b/src/main/java/nonapi/io/github/classgraph/json/JSONDeserializer.java index 0f32f224b..97a658ec5 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/JSONDeserializer.java +++ b/src/main/java/nonapi/io/github/classgraph/json/JSONDeserializer.java @@ -40,7 +40,7 @@ import java.util.Map; import java.util.Map.Entry; -import io.github.classgraph.ClassGraphException; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; import nonapi.io.github.classgraph.types.ParseException; /** @@ -48,7 +48,6 @@ * object graph by inserting reference ids. */ public class JSONDeserializer { - /** * Constructor. */ @@ -73,11 +72,21 @@ private static Object jsonBasicValueToObject(final Object jsonVal, final Type ex if (jsonVal == null) { return null; } else if (jsonVal instanceof JSONArray || jsonVal instanceof JSONObject) { - throw ClassGraphException.newClassGraphException("Expected a basic value type"); + throw new RuntimeException("Expected a basic value type"); } if (expectedType instanceof ParameterizedType) { - // TODO: add support for Class reference values, which may be parameterized - throw new IllegalArgumentException("Got illegal ParameterizedType: " + expectedType); + if (((ParameterizedType) expectedType).getRawType().getClass() == Class.class) { + final String str = jsonVal.toString(); + final int idx = str.indexOf('<'); + final String className = str.substring(0, idx < 0 ? str.length() : idx); + try { + return Class.forName(className); + } catch (final ClassNotFoundException e) { + throw new IllegalArgumentException("Could not deserialize class reference " + jsonVal, e); + } + } else { + throw new IllegalArgumentException("Got illegal ParameterizedType: " + expectedType); + } } else if (!(expectedType instanceof Class)) { throw new IllegalArgumentException("Got illegal basic value type: " + expectedType); } @@ -140,7 +149,7 @@ private static Object jsonBasicValueToObject(final Object jsonVal, final Type ex throw new IllegalArgumentException("Expected float; got " + jsonVal.getClass().getName()); } final double doubleValue = (Double) jsonVal; - if (doubleValue < Float.MIN_VALUE || doubleValue > Float.MAX_VALUE) { + if (doubleValue < -Float.MAX_VALUE || doubleValue > Float.MAX_VALUE) { throw new IllegalArgumentException("Expected float; got out-of-range value " + doubleValue); } return (float) doubleValue; @@ -315,8 +324,13 @@ private static void populateObjectFromJsonObject(final Object objectInstance, fi typeResolutions = null; mapKeyType = null; final Class objectResolvedCls = (Class) objectResolvedTypeGeneric; - arrayComponentType = isArray ? objectResolvedCls.getComponentType() : null; - is1DArray = isArray && !arrayComponentType.isArray(); + if (isArray) { + arrayComponentType = objectResolvedCls.getComponentType(); + is1DArray = !arrayComponentType.isArray(); + } else { + arrayComponentType = null; + is1DArray = false; + } commonResolvedValueType = null; } else if (objectResolvedTypeGeneric instanceof ParameterizedType) { // Get mapping from type variables to resolved types, by comparing the concrete type arguments @@ -371,25 +385,25 @@ private static void populateObjectFromJsonObject(final Object objectInstance, fi // Need to deserialize items in the same order as serialization: create all deserialized objects // at the current level in Pass 1, recording any ids that are found, then recurse into child nodes // in Pass 2 after objects at the current level have all been instantiated. - ArrayList itemsToRecurseToInPass2 = new ArrayList<>(); + ArrayList itemsToRecurseToInPass2 = null; // Pass 1: Convert JSON objects in JSONObject items into Java objects - final int numItems = isJsonObject ? jsonObject.items.size() - : isJsonArray ? jsonArray.items.size() : /* can't happen */ 0; + final int numItems = jsonObject != null ? jsonObject.items.size() + : jsonArray != null ? jsonArray.items.size() : /* can't happen */ 0; for (int i = 0; i < numItems; i++) { // Iterate through items of JSONObject or JSONArray (key is null for JSONArray) - final Entry jsonObjectItem = isJsonObject ? jsonObject.items.get(i) : null; final String itemJsonKey; final Object itemJsonValue; - if (isJsonObject) { + if (jsonObject != null) { + final Entry jsonObjectItem = jsonObject.items.get(i); itemJsonKey = jsonObjectItem.getKey(); itemJsonValue = jsonObjectItem.getValue(); - } else if (isJsonArray) { + } else if (jsonArray != null) { itemJsonKey = null; itemJsonValue = jsonArray.items.get(i); } else { // Can't happen (keep static analyzers happy) - throw ClassGraphException.newClassGraphException("This exception should not be thrown"); + throw new RuntimeException("This exception should not be thrown"); } final boolean itemJsonValueIsJsonObject = itemJsonValue instanceof JSONObject; final boolean itemJsonValueIsJsonArray = itemJsonValue instanceof JSONArray; @@ -399,7 +413,7 @@ private static void populateObjectFromJsonObject(final Object objectInstance, fi // If this is a standard object, look up the field info in the type cache FieldTypeInfo fieldTypeInfo; - if (isObj) { + if (classFields != null) { // Standard objects must interpret the key as a string, since field names are strings. // Look up field name directly, using the itemJsonKey string fieldTypeInfo = classFields.fieldNameToFieldTypeInfo.get(itemJsonKey); @@ -418,7 +432,7 @@ private static void populateObjectFromJsonObject(final Object objectInstance, fi // resolutions found by comparing the resolved type of the concrete containing object // with its generic type. (Fields were partially resolved before by substituting type // arguments of subclasses into type variables of superclasses.) - isObj ? fieldTypeInfo.getFullyResolvedFieldType(typeResolutions) + fieldTypeInfo != null ? fieldTypeInfo.getFullyResolvedFieldType(typeResolutions) // For arrays, the item type is the array component type : isArray ? arrayComponentType // For collections and maps, the value type is the same for all items @@ -491,8 +505,9 @@ private static void populateObjectFromJsonObject(final Object objectInstance, fi // Call the appropriate constructor for the item, whether its type is array, Collection, // Map or other class type. For collections and Maps, call the size hint constructor // for speed when adding items. - final int numSubItems = itemJsonValueIsJsonObject ? itemJsonValueJsonObject.items.size() - : itemJsonValueIsJsonArray ? itemJsonValueJsonArray.items.size() + final int numSubItems = itemJsonValueJsonObject != null + ? itemJsonValueJsonObject.items.size() + : itemJsonValueJsonArray != null ? itemJsonValueJsonArray.items.size() : /* can't happen */ 0; if ((resolvedItemValueType instanceof Class && ((Class) resolvedItemValueType).isArray())) { @@ -514,7 +529,7 @@ private static void populateObjectFromJsonObject(final Object objectInstance, fi : commonValueDefaultConstructor != null ? commonValueDefaultConstructor.newInstance() : /* can't happen */ null; - } else if (isObj) { + } else if (fieldTypeInfo != null) { // For object types, each field has its own constructor, and the constructor can // vary if the field type is completely generic (e.g. "T field"). final Constructor valueConstructorWithSizeHint = fieldTypeInfo @@ -559,9 +574,9 @@ private static void populateObjectFromJsonObject(final Object objectInstance, fi } // Add instantiated items to parent object - if (isObj) { + if (fieldTypeInfo != null) { fieldTypeInfo.setFieldValue(objectInstance, instantiatedItemObject); - } else if (isMap) { + } else if (mapInstance != null) { // For maps, key type should be deserialized from strings, to support e.g. Integer as a key type. // This only works for basic object types though (String, Integer, Enum, etc.) final Object mapKey = jsonBasicValueToObject(itemJsonKey, mapKeyType, @@ -569,7 +584,7 @@ private static void populateObjectFromJsonObject(final Object objectInstance, fi mapInstance.put(mapKey, instantiatedItemObject); } else if (isArray) { Array.set(objectInstance, i, instantiatedItemObject); - } else if (isCollection) { + } else if (collectionInstance != null) { // Can't add partially-deserialized item objects to Collections yet, since their // hashCode() and equals() methods may depend upon fields that have not yet been set. collectionElementAdders.add(new Runnable() { @@ -686,13 +701,32 @@ private static T deserializeObject(final Class expectedType, final String * @throws IllegalArgumentException * If anything goes wrong during deserialization. */ - public static T deserializeObject(final Class expectedType, final String json) - throws IllegalArgumentException { + public static T deserializeObject(final Class expectedType, final String json, + final ReflectionUtils reflectionUtils) throws IllegalArgumentException { final ClassFieldCache classFieldCache = new ClassFieldCache(/* resolveTypes = */ true, - /* onlySerializePublicFields = */ false); + /* onlySerializePublicFields = */ false, reflectionUtils); return deserializeObject(expectedType, json, classFieldCache); } + /** + * Deserialize JSON to a new object graph, with the root object of the specified expected type. Does not work + * for generic types, since it is not possible to obtain the generic type of a Class reference. + * + * @param + * The type that the JSON should conform to. + * @param expectedType + * The class reference for the type that the JSON should conform to. + * @param json + * the JSON string to deserialize. + * @return The object graph after deserialization. + * @throws IllegalArgumentException + * If anything goes wrong during deserialization. + */ + public static T deserializeObject(final Class expectedType, final String json) + throws IllegalArgumentException { + return deserializeObject(expectedType, json, new ReflectionUtils()); + } + /** * Deserialize JSON to a new object graph, with the root object of the specified expected type, and store the * root object in the named field of the given containing object. Works for generic types, since it is possible @@ -752,10 +786,29 @@ public static void deserializeToField(final Object containingObject, final Strin * @throws IllegalArgumentException * If anything goes wrong during deserialization. */ - public static void deserializeToField(final Object containingObject, final String fieldName, final String json) - throws IllegalArgumentException { + public static void deserializeToField(final Object containingObject, final String fieldName, final String json, + final ReflectionUtils reflectionUtils) throws IllegalArgumentException { final ClassFieldCache typeCache = new ClassFieldCache(/* resolveTypes = */ true, - /* onlySerializePublicFields = */ false); + /* onlySerializePublicFields = */ false, reflectionUtils); deserializeToField(containingObject, fieldName, json, typeCache); } + + /** + * Deserialize JSON to a new object graph, with the root object of the specified expected type, and store the + * root object in the named field of the given containing object. Works for generic types, since it is possible + * to obtain the generic type of a field. + * + * @param containingObject + * The object containing the named field to deserialize the object graph into. + * @param fieldName + * The name of the field to set with the result. + * @param json + * the JSON string to deserialize. + * @throws IllegalArgumentException + * If anything goes wrong during deserialization. + */ + public static void deserializeToField(final Object containingObject, final String fieldName, final String json) + throws IllegalArgumentException { + deserializeToField(containingObject, fieldName, json, new ReflectionUtils()); + } } diff --git a/src/main/java/nonapi/io/github/classgraph/json/JSONParser.java b/src/main/java/nonapi/io/github/classgraph/json/JSONParser.java index 9e7ed8038..ac398c548 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/JSONParser.java +++ b/src/main/java/nonapi/io/github/classgraph/json/JSONParser.java @@ -142,7 +142,8 @@ private CharSequence parseString() throws ParseException { while (hasMore()) { final char c = getc(); if (c == '\\') { - switch (getc()) { + final char c2 = getc(); + switch (c2) { case 'b': buf.append('\b'); break; @@ -162,7 +163,7 @@ private CharSequence parseString() throws ParseException { case '"': case '/': case '\\': - buf.append(c); + buf.append(c2); break; case 'u': int charVal = 0; @@ -248,7 +249,7 @@ private Number parseNumber() throws ParseException { throw new ParseException(this, "Expected digits after decimal point"); } } - final boolean hasExponentPart = peek() == '.'; + final boolean hasExponentPart = peek() == 'e' || peek() == 'E'; if (hasExponentPart) { next(); final char sign = peek(); @@ -270,10 +271,10 @@ private Number parseNumber() throws ParseException { final String numberStr = getSubstring(startIdx, endIdx); if (hasFractionalPart || hasExponentPart) { return Double.valueOf(numberStr); - } else if (numIntegralDigits < 9) { + } else if (numIntegralDigits < 10) { return Integer.valueOf(numberStr); - } else if (numIntegralDigits == 9) { - // For 9-digit numbers, could be int or long + } else if (numIntegralDigits == 10) { + // For 10-digit numbers, could be int or long final long longVal = Long.parseLong(numberStr); if (longVal >= Integer.MIN_VALUE && longVal <= Integer.MAX_VALUE) { return (int) longVal; @@ -402,45 +403,51 @@ private JSONObject parseJSONObject() throws ParseException { */ private Object parseJSON() throws ParseException { skipWhitespace(); - try { - final char c = peek(); - if (c == '{') { - // Parse a JSON object - return parseJSONObject(); + final char c = peek(); + if (c == '{') { + // Parse a JSON object + final JSONObject obj = parseJSONObject(); + skipWhitespace(); + return obj; - } else if (c == '[') { - // Parse a JSON array - return parseJSONArray(); + } else if (c == '[') { + // Parse a JSON array + final JSONArray arr = parseJSONArray(); + skipWhitespace(); + return arr; - } else if (c == '"') { - // Parse a JSON string or object reference - final CharSequence charSequence = parseString(); - if (charSequence == null) { - throw new ParseException(this, "Invalid string"); - } - return charSequence; + } else if (c == '"') { + // Parse a JSON string or object reference + final CharSequence charSequence = parseString(); + skipWhitespace(); + if (charSequence == null) { + throw new ParseException(this, "Invalid string"); + } + return charSequence; - } else if (peekMatches("true")) { - // Parse true value - advance(4); - return Boolean.TRUE; + } else if (peekMatches("true")) { + // Parse true value + advance(4); + skipWhitespace(); + return Boolean.TRUE; - } else if (peekMatches("false")) { - // Parse true value - advance(5); - return Boolean.FALSE; + } else if (peekMatches("false")) { + // Parse true value + advance(5); + skipWhitespace(); + return Boolean.FALSE; - } else if (peekMatches("null")) { - advance(4); - // Parse null value (in string representation) - return null; + } else if (peekMatches("null")) { + // Parse null value (in string representation) + advance(4); + skipWhitespace(); + return null; - } else { - // The only remaining option is that the value must be a number - return parseNumber(); - } - } finally { + } else { + // The only remaining option is that the value must be a number + final Number num = parseNumber(); skipWhitespace(); + return num; } } diff --git a/src/main/java/nonapi/io/github/classgraph/json/JSONSerializer.java b/src/main/java/nonapi/io/github/classgraph/json/JSONSerializer.java index 0c69d67ca..63f23956b 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/JSONSerializer.java +++ b/src/main/java/nonapi/io/github/classgraph/json/JSONSerializer.java @@ -34,7 +34,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -44,7 +43,8 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import io.github.classgraph.ClassGraphException; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.utils.CollectionUtils; /** * Fast, lightweight Java object to JSON serializer, and JSON to Java object deserializer. Handles cycles in the @@ -94,14 +94,14 @@ private static void assignObjectIds(final Object jsonVal, final Object refdObj = ((JSONReference) jsonVal).idObject; if (refdObj == null) { // Should not happen - throw ClassGraphException.newClassGraphException("Internal inconsistency"); + throw new RuntimeException("Internal inconsistency"); } // Look up the JSON object corresponding to the referenced object final ReferenceEqualityKey refdObjKey = new ReferenceEqualityKey<>(refdObj); final JSONObject refdJsonVal = objToJSONVal.get(refdObjKey); if (refdJsonVal == null) { // Should not happen - throw ClassGraphException.newClassGraphException("Internal inconsistency"); + throw new RuntimeException("Internal inconsistency"); } // See if the JSON object has an @Id field // (for serialization, typeResolutions can be null) @@ -182,6 +182,10 @@ private static void convertVals(final Object[] convertedVals, needToConvert[i] = false; } } + // Special handling for class references: Convert to class name string + if (val instanceof Class) { + convertedVals[i] = ((Class) val).getName(); + } } // Pass 2: Recursively convert items in standard objects, maps, collections and arrays to JSON objects. for (int i = 0; i < convertedVals.length; i++) { @@ -247,6 +251,11 @@ private static Object toJSONGraph(final Object obj, final Set, JSONObject> objToJSONVal, final boolean onlySerializePublicFields) { + // For class references, return class name as a string + if (obj instanceof Class) { + return ((Class) obj).getName(); + } + // For null and basic value types, just return value if (JSONUtils.isBasicValueType(obj)) { return obj; @@ -271,6 +280,7 @@ private static Object toJSONGraph(final Object obj, final Set cls = obj.getClass(); + final boolean isArray = cls.isArray(); if (Map.class.isAssignableFrom(cls)) { final Map map = (Map) obj; @@ -285,7 +295,7 @@ private static Object toJSONGraph(final Object obj, final Set) keys); + CollectionUtils.sortIfNotEmpty((ArrayList) keys); keysComparable = true; } @@ -293,12 +303,12 @@ private static Object toJSONGraph(final Object obj, final Set list = isList ? (List) obj : null; - final int n = isList ? list.size() : Array.getLength(obj); + final int n = list != null ? list.size() : isArray ? Array.getLength(obj) : 0; // Convert list items to JSON values final Object[] convertedVals = new Object[n]; for (int i = 0; i < n; i++) { - convertedVals[i] = isList ? list.get(i) : Array.get(obj, i); + convertedVals[i] = list != null ? list.get(i) : isArray ? Array.get(obj, i) : 0; } convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal, onlySerializePublicFields); @@ -344,7 +354,7 @@ private static Object toJSONGraph(final Object obj, final Set convertedValsList = new ArrayList<>(collection); if (Set.class.isAssignableFrom(cls)) { - Collections.sort(convertedValsList, SET_COMPARATOR); + CollectionUtils.sortIfNotEmpty(convertedValsList, SET_COMPARATOR); } // Convert items to JSON values @@ -357,36 +367,36 @@ private static Object toJSONGraph(final Object obj, final Set fieldOrder = resolvedFields.fieldOrder; - final int n = fieldOrder.size(); - - // Convert field values to JSON values - final String[] fieldNames = new String[n]; - final Object[] convertedVals = new Object[n]; - for (int i = 0; i < n; i++) { - final FieldTypeInfo fieldInfo = fieldOrder.get(i); - final Field field = fieldInfo.field; - fieldNames[i] = field.getName(); + // Cache class fields to include in serialization (typeResolutions can be null, + // since it's not necessary to resolve type parameters during serialization) + final ClassFields resolvedFields = classFieldCache.get(cls); + final List fieldOrder = resolvedFields.fieldOrder; + final int n = fieldOrder.size(); + + // Convert field values to JSON values + final String[] fieldNames = new String[n]; + final Object[] convertedVals = new Object[n]; + for (int i = 0; i < n; i++) { + final FieldTypeInfo fieldTypeInfo = fieldOrder.get(i); + final Field field = fieldTypeInfo.field; + fieldNames[i] = field.getName(); + try { convertedVals[i] = JSONUtils.getFieldValue(obj, field); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException("Could not get value of field \"" + fieldNames[i] + + "\" in object of class " + obj.getClass().getName(), e); } - convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal, - onlySerializePublicFields); - - // Create new JSON object representing the standard object - final List> convertedKeyValPairs = new ArrayList<>(n); - for (int i = 0; i < n; i++) { - convertedKeyValPairs.add(new SimpleEntry(fieldNames[i], convertedVals[i])); - } - jsonVal = new JSONObject(convertedKeyValPairs); + } + convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal, + onlySerializePublicFields); - } catch (IllegalArgumentException | IllegalAccessException e) { - throw ClassGraphException.newClassGraphException("Could not get value of field in object: " + obj, - e); + // Create new JSON object representing the standard object + final List> convertedKeyValPairs = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + convertedKeyValPairs.add(new SimpleEntry(fieldNames[i], convertedVals[i])); } + jsonVal = new JSONObject(convertedKeyValPairs); + } // In the case of a DAG, just serialize the same object multiple times, i.e. remove obj @@ -447,7 +457,7 @@ static void jsonValToJSONString(final Object jsonVal, } else { // Serialize a numeric or Boolean type (Integer, Long, Short, Float, Double, Boolean, Byte) to string // (doesn't need quoting or escaping) - buf.append(jsonVal.toString()); + buf.append(jsonVal); } } @@ -490,6 +500,27 @@ public static String serializeObject(final Object obj, final int indentWidth, return buf.toString(); } + /** + * Recursively serialize an Object (or array, list, map or set of objects) to JSON, skipping transient and final + * fields. + * + * @param obj + * The root object of the object graph to serialize. + * @param indentWidth + * If indentWidth == 0, no prettyprinting indentation is performed, otherwise this specifies the + * number of spaces to indent each level of JSON. + * @param onlySerializePublicFields + * If true, only serialize public fields. + * @return The object graph in JSON form. + * @throws IllegalArgumentException + * If anything goes wrong during serialization. + */ + public static String serializeObject(final Object obj, final int indentWidth, + final boolean onlySerializePublicFields, final ReflectionUtils reflectionUtils) { + return serializeObject(obj, indentWidth, onlySerializePublicFields, new ClassFieldCache( + /* resolveTypes = */ false, /* onlySerializePublicFields = */ false, reflectionUtils)); + } + /** * Recursively serialize an Object (or array, list, map or set of objects) to JSON, skipping transient and final * fields. @@ -507,8 +538,7 @@ public static String serializeObject(final Object obj, final int indentWidth, */ public static String serializeObject(final Object obj, final int indentWidth, final boolean onlySerializePublicFields) { - return serializeObject(obj, indentWidth, onlySerializePublicFields, - new ClassFieldCache(/* resolveTypes = */ false, /* onlySerializePublicFields = */ false)); + return serializeObject(obj, indentWidth, onlySerializePublicFields, new ReflectionUtils()); } /** @@ -553,7 +583,8 @@ public static String serializeFromField(final Object containingObject, final Str + " does not have a field named \"" + fieldName + "\""); } final Field field = fieldResolvedTypeInfo.field; - if (!JSONUtils.fieldIsSerializable(field, /* onlySerializePublicFields = */ false)) { + if (!JSONUtils.fieldIsSerializable(field, /* onlySerializePublicFields = */ false, + classFieldCache.reflectionUtils)) { throw new IllegalArgumentException("Field " + containingObject.getClass().getName() + "." + fieldName + " needs to be accessible, non-transient, and non-final"); } @@ -578,16 +609,40 @@ public static String serializeFromField(final Object containingObject, final Str * number of spaces to indent each level of JSON. * @param onlySerializePublicFields * If true, only serialize public fields. + * @param reflectionUtils + * The reflection driver. * @return The object graph in JSON form. * @throws IllegalArgumentException * If anything goes wrong during serialization. */ public static String serializeFromField(final Object containingObject, final String fieldName, - final int indentWidth, final boolean onlySerializePublicFields) { + final int indentWidth, final boolean onlySerializePublicFields, final ReflectionUtils reflectionUtils) { // Don't need to resolve types during serialization final ClassFieldCache classFieldCache = new ClassFieldCache(/* resolveTypes = */ false, - onlySerializePublicFields); + onlySerializePublicFields, reflectionUtils); return serializeFromField(containingObject, fieldName, indentWidth, onlySerializePublicFields, classFieldCache); } + + /** + * Recursively serialize the named field of an object, skipping transient and final fields. + * + * @param containingObject + * The object containing the field value to serialize. + * @param fieldName + * The name of the field to serialize. + * @param indentWidth + * If indentWidth == 0, no prettyprinting indentation is performed, otherwise this specifies the + * number of spaces to indent each level of JSON. + * @param onlySerializePublicFields + * If true, only serialize public fields. + * @return The object graph in JSON form. + * @throws IllegalArgumentException + * If anything goes wrong during serialization. + */ + public static String serializeFromField(final Object containingObject, final String fieldName, + final int indentWidth, final boolean onlySerializePublicFields) { + return serializeFromField(containingObject, fieldName, indentWidth, onlySerializePublicFields, + new ReflectionUtils()); + } } diff --git a/src/main/java/nonapi/io/github/classgraph/json/JSONUtils.java b/src/main/java/nonapi/io/github/classgraph/json/JSONUtils.java index a63fa94ff..beadd071a 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/JSONUtils.java +++ b/src/main/java/nonapi/io/github/classgraph/json/JSONUtils.java @@ -30,16 +30,98 @@ import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Collection; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.Callable; + +import nonapi.io.github.classgraph.reflection.ReflectionUtils; /** Utils for Java serialization and deserialization. */ public final class JSONUtils { + private static Method isAccessibleMethod; + private static Method setAccessibleMethod; + private static Method trySetAccessibleMethod; + + static { + // Find deprecated methods isAccessible/setAccessible, to remove compile-time warnings + // TODO Switch to using MethodHandles once this is fixed: + // https://github.com/mojohaus/animal-sniffer/issues/67 + try { + isAccessibleMethod = AccessibleObject.class.getDeclaredMethod("isAccessible"); + } catch (final Throwable t) { + // Ignore + } + try { + setAccessibleMethod = AccessibleObject.class.getDeclaredMethod("setAccessible", boolean.class); + } catch (final Throwable t) { + // Ignore + } + try { + trySetAccessibleMethod = AccessibleObject.class.getDeclaredMethod("trySetAccessible"); + } catch (final Throwable t) { + // Ignore + } + } + + private static boolean isAccessible(final AccessibleObject obj) { + if (isAccessibleMethod != null) { + // JDK 7/8: use isAccessible (deprecated in JDK 9+) + try { + if ((Boolean) isAccessibleMethod.invoke(obj)) { + return true; + } + } catch (final Throwable e) { + // Ignore + } + } + return false; + } + + private static boolean tryMakeAccessible(final AccessibleObject obj) { + if (setAccessibleMethod != null) { + try { + setAccessibleMethod.invoke(obj, true); + return true; + } catch (final Throwable e) { + // Ignore + } + } + if (trySetAccessibleMethod != null) { + try { + if ((Boolean) trySetAccessibleMethod.invoke(obj)) { + return true; + } + } catch (final Throwable e) { + // Ignore + } + } + return false; + } + + public static boolean makeAccessible(final AccessibleObject obj, final ReflectionUtils reflectionUtils) { + // This reflection code is duplicated from StandardReflectionDriver, because calling + // ReflectionUtils.reflectionDriver.makeAccessible(obj) does not work when called from here + // (private fields can't be accessed from outside this package even after calling setAccessible(true)) + if (isAccessible(obj) || tryMakeAccessible(obj)) { + return true; + } + try { + return reflectionUtils.doPrivileged(new Callable() { + @Override + public Boolean call() throws Exception { + return tryMakeAccessible(obj); + } + }); + } catch (final Throwable t) { + return false; + } + } + + // ------------------------------------------------------------------------------------------------------------- + /** * JSON object key name for objects that are linked to from more than one object. Key name is only used if the * class that a JSON object was serialized from does not have its own id field annotated with {@link Id}. @@ -54,6 +136,7 @@ public final class JSONUtils { /** JSON character-to-string escaping replacements -- see http://www.json.org/ under "string". */ private static final String[] JSON_CHAR_REPLACEMENTS = new String[256]; + static { for (int c = 0; c < 256; c++) { if (c == 32) { @@ -63,7 +146,7 @@ public final class JSONUtils { final char hexDigit1 = nibble1 <= 9 ? (char) ('0' + nibble1) : (char) ('A' + nibble1 - 10); final int nibble0 = c & 0xf; final char hexDigit0 = nibble0 <= 9 ? (char) ('0' + nibble0) : (char) ('A' + nibble0 - 10); - JSON_CHAR_REPLACEMENTS[c] = "\\u00" + Character.toString(hexDigit1) + Character.toString(hexDigit0); + JSON_CHAR_REPLACEMENTS[c] = "\\u00" + hexDigit1 + "" + hexDigit0; } JSON_CHAR_REPLACEMENTS['"'] = "\\\""; JSON_CHAR_REPLACEMENTS['\\'] = "\\\\"; @@ -194,6 +277,7 @@ static void indent(final int depth, final int indentWidth, final StringBuilder b */ static Object getFieldValue(final Object containingObj, final Field field) throws IllegalArgumentException, IllegalAccessException { + // return ReflectionUtils.getFieldVal(true, containingObj, field.getName()); final Class fieldType = field.getType(); if (fieldType == Integer.TYPE) { return field.getInt(containingObj); @@ -236,7 +320,8 @@ static boolean isBasicValueType(final Class cls) { || cls == Byte.class || cls == Byte.TYPE // || cls == Character.class || cls == Character.TYPE // || cls == Boolean.class || cls == Boolean.TYPE // - || cls.isEnum(); + || cls.isEnum() // + || cls == Class.class; } /** @@ -266,7 +351,8 @@ static boolean isBasicValueType(final Type type) { static boolean isBasicValueType(final Object obj) { return obj == null || obj instanceof String || obj instanceof Integer || obj instanceof Boolean || obj instanceof Long || obj instanceof Float || obj instanceof Double || obj instanceof Short - || obj instanceof Byte || obj instanceof Character || obj.getClass().isEnum(); + || obj instanceof Byte || obj instanceof Character || obj.getClass().isEnum() + || obj instanceof Class; } /** @@ -303,40 +389,6 @@ static Class getRawType(final Type type) { } } - /** - * Return true if the field is accessible, or can be made accessible (and make it accessible if so). - * - * @param fieldOrConstructor - * the field or constructor - * @return true if accessible - */ - static boolean isAccessibleOrMakeAccessible(final AccessibleObject fieldOrConstructor) { - // Make field accessible if needed - final AtomicBoolean isAccessible = new AtomicBoolean( - // Replace with (in JDK 9+): fieldOrConstructor.canAccess(instance); - fieldOrConstructor.isAccessible()); - if (!isAccessible.get()) { - try { - fieldOrConstructor.setAccessible(true); - isAccessible.set(true); - } catch (final RuntimeException e) { // JDK 9+: InaccessibleObjectException | SecurityException - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Void run() { - try { - fieldOrConstructor.setAccessible(true); - isAccessible.set(true); - } catch (final RuntimeException e) { // JDK 9+: InaccessibleObjectException | SecurityException - // Ignore - } - return null; - } - }); - } - } - return isAccessible.get(); - } - /** * Check if a field is serializable. Don't serialize transient, final, synthetic, or inaccessible fields. * @@ -350,11 +402,12 @@ public Void run() { * if true, only serialize public fields * @return true if the field is serializable */ - static boolean fieldIsSerializable(final Field field, final boolean onlySerializePublicFields) { + static boolean fieldIsSerializable(final Field field, final boolean onlySerializePublicFields, + final ReflectionUtils reflectionUtils) { final int modifiers = field.getModifiers(); if ((!onlySerializePublicFields || Modifier.isPublic(modifiers)) && !Modifier.isTransient(modifiers) && !Modifier.isFinal(modifiers) && ((modifiers & 0x1000 /* synthetic */) == 0)) { - return JSONUtils.isAccessibleOrMakeAccessible(field); + return makeAccessible(field, reflectionUtils); } return false; } diff --git a/src/main/java/nonapi/io/github/classgraph/json/ParameterizedTypeImpl.java b/src/main/java/nonapi/io/github/classgraph/json/ParameterizedTypeImpl.java index e2e12a4d7..9419ad082 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/ParameterizedTypeImpl.java +++ b/src/main/java/nonapi/io/github/classgraph/json/ParameterizedTypeImpl.java @@ -102,19 +102,14 @@ public Type getOwnerType() { * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(final Object o) { - if (this == o) { + public boolean equals(final Object obj) { + if (obj == this) { return true; - } - if (!(o instanceof ParameterizedType)) { + } else if (!(obj instanceof ParameterizedType)) { return false; } - final ParameterizedType other = (ParameterizedType) o; - - final Type otherOwnerType = other.getOwnerType(); - final Type otherRawType = other.getRawType(); - - return Objects.equals(ownerType, otherOwnerType) && Objects.equals(rawType, otherRawType) + final ParameterizedType other = (ParameterizedType) obj; + return Objects.equals(ownerType, other.getOwnerType()) && Objects.equals(rawType, other.getRawType()) && Arrays.equals(actualTypeArguments, other.getActualTypeArguments()); } @@ -138,7 +133,7 @@ public String toString() { if (ownerType instanceof Class) { buf.append(((Class) ownerType).getName()); } else { - buf.append(ownerType.toString()); + buf.append(ownerType); } buf.append('$'); if (ownerType instanceof ParameterizedTypeImpl) { diff --git a/src/main/java/nonapi/io/github/classgraph/json/ReferenceEqualityKey.java b/src/main/java/nonapi/io/github/classgraph/json/ReferenceEqualityKey.java index b24509dfd..4cd6aadeb 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/ReferenceEqualityKey.java +++ b/src/main/java/nonapi/io/github/classgraph/json/ReferenceEqualityKey.java @@ -35,7 +35,7 @@ * @param * the key type */ -class ReferenceEqualityKey { +public class ReferenceEqualityKey { /** The wrapped key. */ private final K wrappedKey; @@ -50,27 +50,62 @@ public ReferenceEqualityKey(final K wrappedKey) { this.wrappedKey = wrappedKey; } + /** + * Get the wrapped key. + * + * @return the wrapped key. + */ + public K get() { + return wrappedKey; + } + + /** + * Hash code. + * + * @return the int + */ /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { - return wrappedKey.hashCode(); + final K key = wrappedKey; + // Don't call key.hashCode(), because that can be an expensive (deep) hashing method, + // e.g. for ByteBuffer, it is based on the entire contents of the buffer + return key == null ? 0 : System.identityHashCode(key); } + /** + * Equals. + * + * @param obj + * the obj + * @return true, if successful + */ /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override - public boolean equals(final Object other) { - return other instanceof ReferenceEqualityKey && wrappedKey == ((ReferenceEqualityKey) other).wrappedKey; + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof ReferenceEqualityKey)) { + return false; + } + return wrappedKey == ((ReferenceEqualityKey) obj).wrappedKey; } + /** + * To string. + * + * @return the string + */ /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { - return wrappedKey.toString(); + final K key = wrappedKey; + return key == null ? "null" : key.toString(); } } \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/json/TypeResolutions.java b/src/main/java/nonapi/io/github/classgraph/json/TypeResolutions.java index 31fbbd091..71ee57671 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/TypeResolutions.java +++ b/src/main/java/nonapi/io/github/classgraph/json/TypeResolutions.java @@ -35,8 +35,6 @@ import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; -import io.github.classgraph.ClassGraphException; - /** A mapping from {@link TypeVariable} to resolved {@link Type}. */ class TypeResolutions { @@ -142,10 +140,10 @@ Type resolveTypeVariables(final Type type) { } else if (type instanceof WildcardType) { // TODO: Support WildcardType - throw ClassGraphException.newClassGraphException("WildcardType not yet supported: " + type); + throw new RuntimeException("WildcardType not yet supported: " + type); } else { - throw ClassGraphException.newClassGraphException("Got unexpected type: " + type); + throw new RuntimeException("Got unexpected type: " + type); } } diff --git a/src/main/java/nonapi/io/github/classgraph/recycler/RecycleOnClose.java b/src/main/java/nonapi/io/github/classgraph/recycler/RecycleOnClose.java index b3fa598dd..5bd265b39 100644 --- a/src/main/java/nonapi/io/github/classgraph/recycler/RecycleOnClose.java +++ b/src/main/java/nonapi/io/github/classgraph/recycler/RecycleOnClose.java @@ -39,7 +39,6 @@ * the exception type that may be thrown when a recyclable item is acquired. */ public class RecycleOnClose implements AutoCloseable { - /** The recycler. */ private final Recycler recycler; diff --git a/src/main/java/nonapi/io/github/classgraph/recycler/Recycler.java b/src/main/java/nonapi/io/github/classgraph/recycler/Recycler.java index aee38f5ff..eb1a73cc1 100644 --- a/src/main/java/nonapi/io/github/classgraph/recycler/Recycler.java +++ b/src/main/java/nonapi/io/github/classgraph/recycler/Recycler.java @@ -98,7 +98,7 @@ public T acquire() throws E { * If anything goes wrong when trying to allocate a new object instance. */ public RecycleOnClose acquireRecycleOnClose() throws E { - return new RecycleOnClose(this, acquire()); + return new RecycleOnClose<>(this, acquire()); } /** @@ -112,11 +112,15 @@ public RecycleOnClose acquireRecycleOnClose() throws E { */ public final void recycle(final T instance) { if (instance != null) { - usedInstances.remove(instance); + if (!usedInstances.remove(instance)) { + throw new IllegalArgumentException("Tried to recycle an instance that was not in use"); + } if (instance instanceof Resettable) { ((Resettable) instance).reset(); } - unusedInstances.add(instance); + if (!unusedInstances.add(instance)) { + throw new IllegalArgumentException("Tried to recycle an instance twice"); + } } } diff --git a/src/main/java/nonapi/io/github/classgraph/reflection/NarcissusReflectionDriver.java b/src/main/java/nonapi/io/github/classgraph/reflection/NarcissusReflectionDriver.java new file mode 100644 index 000000000..865cb0ed1 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/reflection/NarcissusReflectionDriver.java @@ -0,0 +1,137 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.reflection; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Narcissus reflection driver (uses the Narcissus library, + * if it is available, which allows access to non-public fields and methods, circumventing encapsulation and + * visibility controls via JNI). + */ +class NarcissusReflectionDriver extends ReflectionDriver { + private final Class narcissusClass; + private final Method getDeclaredMethods; + private final Method findClass; + private final Method getDeclaredConstructors; + private final Method getDeclaredFields; + private final Method getField; + private final Method setField; + private final Method getStaticField; + private final Method setStaticField; + private final Method invokeMethod; + private final Method invokeStaticMethod; + + NarcissusReflectionDriver() throws Exception { + // Load Narcissus class via reflection, so that there is no runtime dependency + final StandardReflectionDriver drv = new StandardReflectionDriver(); + narcissusClass = drv.findClass("io.github.toolfactory.narcissus.Narcissus"); + if (!(Boolean) drv.getStaticField(drv.findStaticField(narcissusClass, "libraryLoaded"))) { + throw new IllegalArgumentException("Could not load Narcissus native library"); + } + + // Look up needed methods + findClass = drv.findStaticMethod(narcissusClass, "findClass", String.class); + getDeclaredMethods = drv.findStaticMethod(narcissusClass, "getDeclaredMethods", Class.class); + getDeclaredConstructors = drv.findStaticMethod(narcissusClass, "getDeclaredConstructors", Class.class); + getDeclaredFields = drv.findStaticMethod(narcissusClass, "getDeclaredFields", Class.class); + getField = drv.findStaticMethod(narcissusClass, "getField", Object.class, Field.class); + setField = drv.findStaticMethod(narcissusClass, "setField", Object.class, Field.class, Object.class); + getStaticField = drv.findStaticMethod(narcissusClass, "getStaticField", Field.class); + setStaticField = drv.findStaticMethod(narcissusClass, "setStaticField", Field.class, Object.class); + invokeMethod = drv.findStaticMethod(narcissusClass, "invokeMethod", Object.class, Method.class, + Object[].class); + invokeStaticMethod = drv.findStaticMethod(narcissusClass, "invokeStaticMethod", Method.class, + Object[].class); + } + + @Override + public boolean isAccessible(final Object instance, final AccessibleObject obj) { + return true; + } + + @Override + public boolean makeAccessible(final Object instance, final AccessibleObject accessibleObject) { + return true; + } + + @Override + Class findClass(final String className) throws Exception { + return (Class) findClass.invoke(null, className); + } + + @Override + Method[] getDeclaredMethods(final Class cls) throws Exception { + return (Method[]) getDeclaredMethods.invoke(null, cls); + } + + @SuppressWarnings("unchecked") + @Override + Constructor[] getDeclaredConstructors(final Class cls) throws Exception { + return (Constructor[]) getDeclaredConstructors.invoke(null, cls); + } + + @Override + Field[] getDeclaredFields(final Class cls) throws Exception { + return (Field[]) getDeclaredFields.invoke(null, cls); + } + + @Override + Object getField(final Object object, final Field field) throws Exception { + return getField.invoke(null, object, field); + } + + @Override + void setField(final Object object, final Field field, final Object value) throws Exception { + setField.invoke(null, object, field, value); + } + + @Override + Object getStaticField(final Field field) throws Exception { + return getStaticField.invoke(null, field); + } + + @Override + void setStaticField(final Field field, final Object value) throws Exception { + setStaticField.invoke(null, field, value); + } + + @Override + Object invokeMethod(final Object object, final Method method, final Object... args) throws Exception { + return invokeMethod.invoke(null, object, method, args); + } + + @Override + Object invokeStaticMethod(final Method method, final Object... args) throws Exception { + return invokeStaticMethod.invoke(null, method, args); + } +} \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/reflection/ReflectionDriver.java b/src/main/java/nonapi/io/github/classgraph/reflection/ReflectionDriver.java new file mode 100644 index 000000000..9db34a084 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/reflection/ReflectionDriver.java @@ -0,0 +1,437 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.reflection; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import nonapi.io.github.classgraph.concurrency.SingletonMap; +import nonapi.io.github.classgraph.utils.LogNode; + +/** Reflection driver */ +abstract class ReflectionDriver { + private final SingletonMap, ClassMemberCache, Exception> classToClassMemberCache // + = new SingletonMap, ClassMemberCache, Exception>() { + @Override + public ClassMemberCache newInstance(final Class cls, final LogNode log) + throws Exception, InterruptedException { + return new ClassMemberCache(cls); + } + }; + + private static Method isAccessibleMethod; + private static Method canAccessMethod; + + static { + // Find deprecated methods to remove compile-time warnings + // TODO Switch to using MethodHandles once this is fixed: + // https://github.com/mojohaus/animal-sniffer/issues/67 + try { + isAccessibleMethod = AccessibleObject.class.getDeclaredMethod("isAccessible"); + } catch (final Throwable t) { + // Ignore + } + try { + canAccessMethod = AccessibleObject.class.getDeclaredMethod("canAccess", Object.class); + } catch (final Throwable t) { + // Ignore + } + } + + /** Caches class members. */ + public class ClassMemberCache { + private final Map> methodNameToMethods = new HashMap<>(); + private final Map fieldNameToField = new HashMap<>(); + + private ClassMemberCache(final Class cls) throws Exception { + // Iterate from class to its superclasses, and find initial interfaces to start traversing from + final Set> visited = new HashSet<>(); + final LinkedList> interfaceQueue = new LinkedList>(); + for (Class c = cls; c != null; c = c.getSuperclass()) { + try { + // Cache any declared methods and fields + for (final Method m : getDeclaredMethods(c)) { + cacheMethod(m); + } + for (final Field f : getDeclaredFields(c)) { + cacheField(f); + } + // Find interfaces and superinterfaces implemented by this class or its superclasses + if (c.isInterface() && visited.add(c)) { + interfaceQueue.add(c); + } + for (final Class iface : c.getInterfaces()) { + if (visited.add(iface)) { + interfaceQueue.add(iface); + } + } + } catch (final Exception e) { + // Skip + } + } + // Traverse through interfaces looking for default methods + while (!interfaceQueue.isEmpty()) { + final Class iface = interfaceQueue.remove(); + try { + for (final Method m : getDeclaredMethods(iface)) { + cacheMethod(m); + } + } catch (final Exception e) { + // Skip + } + for (final Class superIface : iface.getInterfaces()) { + if (visited.add(superIface)) { + interfaceQueue.add(superIface); + } + } + } + } + + private void cacheMethod(final Method method) { + List methodsForName = methodNameToMethods.get(method.getName()); + if (methodsForName == null) { + methodNameToMethods.put(method.getName(), methodsForName = new ArrayList<>()); + } + methodsForName.add(method); + } + + private void cacheField(final Field field) { + // Only put a field name to field mapping if it is absent, so that subclasses mask fields + // of the same name in superclasses + if (!fieldNameToField.containsKey(field.getName())) { + fieldNameToField.put(field.getName(), field); + } + } + } + + /** + * Find a class by name. + * + * @param className + * the class name + * @return the class reference + */ + abstract Class findClass(final String className) throws Exception; + + /** + * Get declared methods for class. + * + * @param cls + * the class + * @return the declared methods + */ + abstract Method[] getDeclaredMethods(Class cls) throws Exception; + + /** + * Get declared constructors for class. + * + * @param + * the generic type + * @param cls + * the class + * @return the declared constructors + */ + abstract Constructor[] getDeclaredConstructors(Class cls) throws Exception; + + /** + * Get declared fields for class. + * + * @param cls + * the class + * @return the declared fields + */ + abstract Field[] getDeclaredFields(Class cls) throws Exception; + + /** + * Get the value of a non-static field, boxing the value if necessary. + * + * @param object + * the object instance to get the field value from + * @param field + * the non-static field + * @return the value of the field + */ + abstract Object getField(final Object object, final Field field) throws Exception; + + /** + * Set the value of a non-static field, unboxing the value if necessary. + * + * @param object + * the object instance to get the field value from + * @param field + * the non-static field + * @param value + * the value to set + */ + abstract void setField(final Object object, final Field field, Object value) throws Exception; + + /** + * Get the value of a static field, boxing the value if necessary. + * + * @param field + * the static field + * @return the static field + */ + abstract Object getStaticField(final Field field) throws Exception; + + /** + * Set the value of a static field, unboxing the value if necessary. + * + * @param field + * the static field + * @param value + * the value to set + */ + abstract void setStaticField(final Field field, Object value) throws Exception; + + /** + * Invoke a non-static method, boxing the result if necessary. + * + * @param object + * the object instance to invoke the method on + * @param method + * the non-static method + * @param args + * the method arguments (or {@code new Object[0]} if there are no args) + * @return the return value (possibly a boxed value) + */ + abstract Object invokeMethod(final Object object, final Method method, final Object... args) throws Exception; + + /** + * Invoke a static method, boxing the result if necessary. + * + * @param method + * the static method + * @param args + * the method arguments (or {@code new Object[0]} if there are no args) + * @return the return value (possibly a boxed value) + */ + abstract Object invokeStaticMethod(final Method method, final Object... args) throws Exception; + + /** + * Make a field or method accessible. + * + * @param instance + * the object instance, or null if static. + * @param fieldOrMethod + * the field or method. + * + * @return true if successful. + */ + abstract boolean makeAccessible(final Object instance, final AccessibleObject fieldOrMethod); + + /** + * Check whether a field or method is accessible. + * + *

+ * N.B. this is overridden in Narcissus driver to just return true, since everything is accessible to JNI. + * + * @param instance + * the object instance, or null if static. + * @param fieldOrMethod + * the field or method. + * + * @return true if accessible. + */ + boolean isAccessible(final Object instance, final AccessibleObject fieldOrMethod) { + if (canAccessMethod != null) { + // JDK 9+: use canAccess + try { + return (Boolean) canAccessMethod.invoke(fieldOrMethod, instance); + } catch (final Throwable e) { + // Ignore + } + } + if (isAccessibleMethod != null) { + // JDK 7/8: use isAccessible (deprecated in JDK 9+) + try { + return (Boolean) isAccessibleMethod.invoke(fieldOrMethod); + } catch (final Throwable e) { + // Ignore + } + } + return false; + } + + /** + * Get the field of the class that has a given field name. + * + * @param cls + * the class. + * @param obj + * the object instance, or null for a static field. + * @param fieldName + * The name of the field. + * @return The {@link Field} object for the requested field name, or null if no such field was found in the + * class. + * @throws Exception + * if the field could not be found + */ + protected Field findField(final Class cls, final Object obj, final String fieldName) throws Exception { + final Field field = classToClassMemberCache.get(cls, /* log = */ null).fieldNameToField.get(fieldName); + if (field != null) { + if (!isAccessible(obj, field)) { + // If field was found but is not accessible, try making it accessible and then returning it + // (may result in a reflective access warning on stderr) + makeAccessible(obj, field); + } + return field; + } + throw new NoSuchFieldException("Could not find field " + cls.getName() + "." + fieldName); + } + + /** + * Get the static field of the class that has a given field name. + * + * @param cls + * the class. + * @param fieldName + * The name of the field. + * @return The {@link Field} object for the requested field name, or null if no such field was found in the + * class. + * @throws Exception + * if the field could not be found + */ + protected Field findStaticField(final Class cls, final String fieldName) throws Exception { + return findField(cls, null, fieldName); + } + + /** + * Get the non-static field of the class that has a given field name. + * + * @param obj + * the object instance, or null for a static field. + * @param fieldName + * The name of the field. + * @return The {@link Field} object for the requested field name, or null if no such field was found in the + * class. + * @throws Exception + * if the field could not be found + */ + protected Field findInstanceField(final Object obj, final String fieldName) throws Exception { + if (obj == null) { + throw new IllegalArgumentException("obj cannot be null"); + } + return findField(obj.getClass(), obj, fieldName); + } + + /** + * Get a method by name and parameter types. + * + * @param cls + * the class. + * @param obj + * the object instance, or null for a static method. + * @param methodName + * The name of the method. + * @param paramTypes + * The types of the parameters of the method. For primitive-typed parameters, use e.g. Integer.TYPE. + * @return The {@link Method} object for the matching method, or null if no such method was found in the class. + * @throws Exception + * if the method could not be found. + */ + protected Method findMethod(final Class cls, final Object obj, final String methodName, + final Class... paramTypes) throws Exception { + final List methodsForName = classToClassMemberCache.get(cls, null).methodNameToMethods + .get(methodName); + if (methodsForName != null) { + // Return the first method that matches the signature that is already accessible + boolean found = false; + for (final Method method : methodsForName) { + if (Arrays.equals(method.getParameterTypes(), paramTypes)) { + found = true; + if (isAccessible(obj, method)) { + return method; + } + } + } + // If method was found but is not accessible, try making it accessible and then returning it + // (may result in a reflective access warning on stderr) + if (found) { + for (final Method method : methodsForName) { + if (Arrays.equals(method.getParameterTypes(), paramTypes) && makeAccessible(obj, method)) { + return method; + } + } + } + throw new NoSuchMethodException( + "Could not make method accessible: " + cls.getName() + "." + methodName); + } + throw new NoSuchMethodException("Could not find method " + cls.getName() + "." + methodName); + } + + /** + * Get a static method by name and parameter types. + * + * @param cls + * the class. + * @param methodName + * The name of the method. + * @param paramTypes + * The types of the parameters of the method. For primitive-typed parameters, use e.g. Integer.TYPE. + * @return The {@link Method} object for the matching method, or null if no such method was found in the class. + * @throws Exception + * if the method could not be found. + */ + protected Method findStaticMethod(final Class cls, final String methodName, final Class... paramTypes) + throws Exception { + return findMethod(cls, null, methodName, paramTypes); + } + + /** + * Get a non-static method by name and parameter types. + * + * @param obj + * the object instance, or null for a static method. + * @param methodName + * The name of the method. + * @param paramTypes + * The types of the parameters of the method. For primitive-typed parameters, use e.g. Integer.TYPE. + * @return The {@link Method} object for the matching method, or null if no such method was found in the class. + * @throws Exception + * if the method could not be found. + */ + protected Method findInstanceMethod(final Object obj, final String methodName, final Class... paramTypes) + throws Exception { + if (obj == null) { + throw new IllegalArgumentException("obj cannot be null"); + } + return findMethod(obj.getClass(), obj, methodName, paramTypes); + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/reflection/ReflectionUtils.java b/src/main/java/nonapi/io/github/classgraph/reflection/ReflectionUtils.java new file mode 100644 index 000000000..2d0f974fa --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/reflection/ReflectionUtils.java @@ -0,0 +1,421 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.reflection; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.concurrent.Callable; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassGraph.CircumventEncapsulationMethod; + +/** Reflection utility methods that can be used by ClassLoaderHandlers. */ +public final class ReflectionUtils { + /** The reflection driver to use. */ + public ReflectionDriver reflectionDriver; + private Class accessControllerClass; + private Class privilegedActionClass; + private Method accessControllerDoPrivileged; + + /** Call this if you change the value of {@link ClassGraph#CIRCUMVENT_ENCAPSULATION}. */ + public ReflectionUtils() { + if (ClassGraph.CIRCUMVENT_ENCAPSULATION == CircumventEncapsulationMethod.NARCISSUS) { + try { + reflectionDriver = new NarcissusReflectionDriver(); + } catch (final Throwable t) { + System.err.println("Could not load Narcissus reflection driver: " + t); + // Fall back to standard reflection driver + } + } + if (reflectionDriver == null) { + reflectionDriver = new StandardReflectionDriver(); + } + try { + accessControllerClass = reflectionDriver.findClass("java.security.AccessController"); + privilegedActionClass = reflectionDriver.findClass("java.security.PrivilegedAction"); + accessControllerDoPrivileged = reflectionDriver.findMethod(accessControllerClass, null, "doPrivileged", + privilegedActionClass); + } catch (final Throwable t) { + // Ignore + } + } + + /** + * Get the value of the field in the class of the given object or any of its superclasses. If an exception is + * thrown while trying to read the field, and throwException is true, then IllegalArgumentException is thrown + * wrapping the cause, otherwise this will return null. If passed a null object, returns null unless + * throwException is true, then throws IllegalArgumentException. + * + * @param throwException + * If true, throw an exception if the field value could not be read. + * @param obj + * The object. + * @param field + * The field. + * + * @return The field value. + * @throws IllegalArgumentException + * If the field value could not be read. + */ + public Object getFieldVal(final boolean throwException, final Object obj, final Field field) + throws IllegalArgumentException { + if (reflectionDriver == null) { + throw new RuntimeException("Cannot use reflection after ScanResult has been closed"); + } + if (obj == null || field == null) { + if (throwException) { + throw new NullPointerException(); + } else { + return null; + } + } + try { + return reflectionDriver.getField(obj, field); + } catch (final Throwable e) { + if (throwException) { + throw new IllegalArgumentException( + "Can't read field " + obj.getClass().getName() + "." + field.getName(), e); + } + } + return null; + } + + /** + * Get the value of the named field in the class of the given object or any of its superclasses. If an exception + * is thrown while trying to read the field, and throwException is true, then IllegalArgumentException is thrown + * wrapping the cause, otherwise this will return null. If passed a null object, returns null unless + * throwException is true, then throws IllegalArgumentException. + * + * @param throwException + * If true, throw an exception if the field value could not be read. + * @param obj + * The object. + * @param fieldName + * The field name. + * + * @return The field value. + * @throws IllegalArgumentException + * If the field value could not be read. + */ + public Object getFieldVal(final boolean throwException, final Object obj, final String fieldName) + throws IllegalArgumentException { + if (reflectionDriver == null) { + throw new RuntimeException("Cannot use reflection after ScanResult has been closed"); + } + if (obj == null || fieldName == null) { + if (throwException) { + throw new NullPointerException(); + } else { + return null; + } + } + try { + return reflectionDriver.getField(obj, reflectionDriver.findInstanceField(obj, fieldName)); + } catch (final Throwable e) { + if (throwException) { + throw new IllegalArgumentException("Can't read field " + obj.getClass().getName() + "." + fieldName, + e); + } + } + return null; + } + + /** + * Get the value of the named field in the given class or any of its superclasses. If an exception is thrown + * while trying to read the field value, and throwException is true, then IllegalArgumentException is thrown + * wrapping the cause, otherwise this will return null. If passed a null class reference, returns null unless + * throwException is true, then throws IllegalArgumentException. + * + * @param throwException + * If true, throw an exception if the field value could not be read. + * @param cls + * The class. + * @param fieldName + * The field name. + * + * @return The field value. + * @throws IllegalArgumentException + * If the field value could not be read. + */ + public Object getStaticFieldVal(final boolean throwException, final Class cls, final String fieldName) + throws IllegalArgumentException { + if (reflectionDriver == null) { + throw new RuntimeException("Cannot use reflection after ScanResult has been closed"); + } + if (cls == null || fieldName == null) { + if (throwException) { + throw new NullPointerException(); + } else { + return null; + } + } + try { + return reflectionDriver.getStaticField(reflectionDriver.findStaticField(cls, fieldName)); + } catch (final Throwable e) { + if (throwException) { + throw new IllegalArgumentException("Can't read field " + cls.getName() + "." + fieldName, e); + } + } + return null; + } + + /** + * Invoke the named method in the given object or its superclasses. If an exception is thrown while trying to + * call the method, and throwException is true, then IllegalArgumentException is thrown wrapping the cause, + * otherwise this will return null. If passed a null object, returns null unless throwException is true, then + * throws IllegalArgumentException. + * + * @param throwException + * If true, throw an exception if the field value could not be read. + * @param obj + * The object. + * @param methodName + * The method name. + * + * @return The result of the method invocation. + * @throws IllegalArgumentException + * If the method could not be invoked. + */ + public Object invokeMethod(final boolean throwException, final Object obj, final String methodName) + throws IllegalArgumentException { + if (reflectionDriver == null) { + throw new RuntimeException("Cannot use reflection after ScanResult has been closed"); + } + if (obj == null || methodName == null) { + if (throwException) { + throw new IllegalArgumentException("Unexpected null argument"); + } else { + return null; + } + } + try { + return reflectionDriver.invokeMethod(obj, reflectionDriver.findInstanceMethod(obj, methodName)); + } catch (final Throwable e) { + if (throwException) { + throw new IllegalArgumentException("Method \"" + methodName + "\" could not be invoked", e); + } + return null; + } + } + + /** + * Invoke the named method in the given object or its superclasses. If an exception is thrown while trying to + * call the method, and throwException is true, then IllegalArgumentException is thrown wrapping the cause, + * otherwise this will return null. If passed a null object, returns null unless throwException is true, then + * throws IllegalArgumentException. + * + * @param throwException + * Whether to throw an exception on failure. + * @param obj + * The object. + * @param methodName + * The method name. + * @param argType + * The type of the method argument. + * @param param + * The parameter value to use when invoking the method. + * + * @return The result of the method invocation. + * @throws IllegalArgumentException + * If the method could not be invoked. + */ + public Object invokeMethod(final boolean throwException, final Object obj, final String methodName, + final Class argType, final Object param) throws IllegalArgumentException { + if (reflectionDriver == null) { + throw new RuntimeException("Cannot use reflection after ScanResult has been closed"); + } + if (obj == null || methodName == null || argType == null) { + if (throwException) { + throw new IllegalArgumentException("Unexpected null argument"); + } else { + return null; + } + } + try { + return reflectionDriver.invokeMethod(obj, reflectionDriver.findInstanceMethod(obj, methodName, argType), + param); + } catch (final Throwable e) { + if (throwException) { + throw new IllegalArgumentException("Method \"" + methodName + "\" could not be invoked", e); + } + return null; + } + } + + /** + * Invoke the named method. If an exception is thrown while trying to call the method, and throwException is + * true, then IllegalArgumentException is thrown wrapping the cause, otherwise this will return null. If passed + * a null class reference, returns null unless throwException is true, then throws IllegalArgumentException. + * + * @param throwException + * Whether to throw an exception on failure. + * @param cls + * The class. + * @param methodName + * The method name. + * + * @return The result of the method invocation. + * @throws IllegalArgumentException + * If the method could not be invoked. + */ + public Object invokeStaticMethod(final boolean throwException, final Class cls, final String methodName) + throws IllegalArgumentException { + if (reflectionDriver == null) { + throw new RuntimeException("Cannot use reflection after ScanResult has been closed"); + } + if (cls == null || methodName == null) { + if (throwException) { + throw new IllegalArgumentException("Unexpected null argument"); + } else { + return null; + } + } + try { + return reflectionDriver.invokeStaticMethod(reflectionDriver.findStaticMethod(cls, methodName)); + } catch (final Throwable e) { + if (throwException) { + throw new IllegalArgumentException("Method \"" + methodName + "\" could not be invoked", e); + } + return null; + } + } + + /** + * Invoke the named method. If an exception is thrown while trying to call the method, and throwException is + * true, then IllegalArgumentException is thrown wrapping the cause, otherwise this will return null. If passed + * a null class reference, returns null unless throwException is true, then throws IllegalArgumentException. + * + * @param throwException + * Whether to throw an exception on failure. + * @param cls + * The class. + * @param methodName + * The method name. + * @param argType + * The type of the method argument. + * @param param + * The parameter value to use when invoking the method. + * + * @return The result of the method invocation. + * @throws IllegalArgumentException + * If the method could not be invoked. + */ + public Object invokeStaticMethod(final boolean throwException, final Class cls, final String methodName, + final Class argType, final Object param) throws IllegalArgumentException { + if (reflectionDriver == null) { + throw new RuntimeException("Cannot use reflection after ScanResult has been closed"); + } + if (cls == null || methodName == null || argType == null) { + if (throwException) { + throw new IllegalArgumentException("Unexpected null argument"); + } else { + return null; + } + } + try { + return reflectionDriver.invokeStaticMethod(reflectionDriver.findStaticMethod(cls, methodName, argType), + param); + } catch (final Throwable e) { + if (throwException) { + throw new IllegalArgumentException("Fethod \"" + methodName + "\" could not be invoked", e); + } + return null; + } + } + + /** + * Call Class.forName(className), but return null if any exception is thrown. + * + * @param className + * The class name to load. + * @return The class of the requested name, or null if an exception was thrown while trying to load the class. + */ + public Class classForNameOrNull(final String className) { + if (reflectionDriver == null) { + throw new RuntimeException("Cannot use reflection after ScanResult has been closed"); + } + try { + return reflectionDriver.findClass(className); + } catch (final Throwable e) { + return null; + } + } + + /** + * Get a method by name, but return null if any exception is thrown. + * + * @param className + * The class name to load. + * @return The class of the requested name, or null if an exception was thrown while trying to load the class. + */ + public Method staticMethodForNameOrNull(final String className, final String staticMethodName) { + if (reflectionDriver == null) { + throw new RuntimeException("Cannot use reflection after ScanResult has been closed"); + } + try { + return reflectionDriver.findStaticMethod(reflectionDriver.findClass(className), staticMethodName); + } catch (final Throwable e) { + return null; + } + } + + // ------------------------------------------------------------------------------------------------------------- + + private class PrivilegedActionInvocationHandler implements InvocationHandler { + private final Callable callable; + + public PrivilegedActionInvocationHandler(final Callable callable) { + this.callable = callable; + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + return callable.call(); + } + } + + /** + * Call a method in the AccessController.doPrivileged(PrivilegedAction) context, using reflection, if possible + * (AccessController is deprecated in JDK 17). + */ + @SuppressWarnings("unchecked") + public T doPrivileged(final Callable callable) throws Throwable { + if (accessControllerDoPrivileged != null) { + final Object privilegedAction = Proxy.newProxyInstance(privilegedActionClass.getClassLoader(), + new Class[] { privilegedActionClass }, new PrivilegedActionInvocationHandler(callable)); + return (T) accessControllerDoPrivileged.invoke(null, privilegedAction); + } else { + // Fall back to invoking in a non-privileged context + return callable.call(); + } + } + +} diff --git a/src/main/java/nonapi/io/github/classgraph/reflection/StandardReflectionDriver.java b/src/main/java/nonapi/io/github/classgraph/reflection/StandardReflectionDriver.java new file mode 100644 index 000000000..410398700 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/reflection/StandardReflectionDriver.java @@ -0,0 +1,201 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.reflection; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.concurrent.Callable; + +/** + * Standard reflection driver (uses {@link AccessibleObject#setAccessible(boolean)} to access non-public fields if + * necessary). + */ +class StandardReflectionDriver extends ReflectionDriver { + private static Method setAccessibleMethod; + private static Method trySetAccessibleMethod; + private static Class accessControllerClass; + private static Class privilegedActionClass; + private static Method accessControllerDoPrivileged; + + static { + // Find deprecated methods to remove compile-time warnings + // TODO Switch to using MethodHandles once this is fixed: + // https://github.com/mojohaus/animal-sniffer/issues/67 + try { + setAccessibleMethod = AccessibleObject.class.getDeclaredMethod("setAccessible", boolean.class); + } catch (final Throwable t) { + // Ignore + } + try { + trySetAccessibleMethod = AccessibleObject.class.getDeclaredMethod("trySetAccessible"); + } catch (final Throwable t) { + // Ignore + } + try { + accessControllerClass = Class.forName("java.security.AccessController"); + privilegedActionClass = Class.forName("java.security.PrivilegedAction"); + accessControllerDoPrivileged = accessControllerClass.getMethod("doPrivileged", privilegedActionClass); + } catch (final Throwable t) { + // Ignore + } + } + + // ------------------------------------------------------------------------------------------------------------- + + private class PrivilegedActionInvocationHandler implements InvocationHandler { + private final Callable callable; + + public PrivilegedActionInvocationHandler(final Callable callable) { + this.callable = callable; + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + return callable.call(); + } + } + + /** + * Call a method in the AccessController.doPrivileged(PrivilegedAction) context, using reflection, if possible + * (AccessController is deprecated in JDK 17). + */ + @SuppressWarnings("unchecked") + private T doPrivileged(final Callable callable) throws Throwable { + if (accessControllerDoPrivileged != null) { + final Object privilegedAction = Proxy.newProxyInstance(privilegedActionClass.getClassLoader(), + new Class[] { privilegedActionClass }, new PrivilegedActionInvocationHandler(callable)); + return (T) accessControllerDoPrivileged.invoke(null, privilegedAction); + } else { + // Fall back to invoking in a non-privileged context + return callable.call(); + } + } + + // ------------------------------------------------------------------------------------------------------------- + + private static boolean tryMakeAccessible(final AccessibleObject obj) { + if (trySetAccessibleMethod != null) { + // JDK 9+ + try { + return (Boolean) trySetAccessibleMethod.invoke(obj); + } catch (final Throwable e) { + // Ignore + } + } + if (setAccessibleMethod != null) { + // JDK 7/8 + try { + setAccessibleMethod.invoke(obj, true); + return true; + } catch (final Throwable e) { + // Ignore + } + } + return false; + } + + @Override + public boolean makeAccessible(final Object instance, final AccessibleObject obj) { + if (isAccessible(instance, obj)) { + return true; + } + try { + return doPrivileged(new Callable() { + @Override + public Boolean call() throws Exception { + return tryMakeAccessible(obj); + } + }); + } catch (final Throwable t) { + // Fall through + return tryMakeAccessible(obj); + } + } + + @Override + Class findClass(final String className) throws Exception { + return Class.forName(className); + } + + @Override + Method[] getDeclaredMethods(final Class cls) throws Exception { + return cls.getDeclaredMethods(); + } + + @SuppressWarnings("unchecked") + @Override + Constructor[] getDeclaredConstructors(final Class cls) throws Exception { + return (Constructor[]) cls.getDeclaredConstructors(); + } + + @Override + Field[] getDeclaredFields(final Class cls) throws Exception { + return cls.getDeclaredFields(); + } + + @Override + Object getField(final Object object, final Field field) throws Exception { + makeAccessible(object, field); + return field.get(object); + } + + @Override + void setField(final Object object, final Field field, final Object value) throws Exception { + makeAccessible(object, field); + field.set(object, value); + } + + @Override + Object getStaticField(final Field field) throws Exception { + makeAccessible(null, field); + return field.get(null); + } + + @Override + void setStaticField(final Field field, final Object value) throws Exception { + makeAccessible(null, field); + field.set(null, value); + } + + @Override + Object invokeMethod(final Object object, final Method method, final Object... args) throws Exception { + makeAccessible(object, method); + return method.invoke(object, args); + } + + @Override + Object invokeStaticMethod(final Method method, final Object... args) throws Exception { + makeAccessible(null, method); + return method.invoke(null, args); + } +} \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/scanspec/AcceptReject.java b/src/main/java/nonapi/io/github/classgraph/scanspec/AcceptReject.java new file mode 100644 index 000000000..f7b098b75 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/scanspec/AcceptReject.java @@ -0,0 +1,759 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.scanspec; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import nonapi.io.github.classgraph.utils.CollectionUtils; +import nonapi.io.github.classgraph.utils.FastPathResolver; +import nonapi.io.github.classgraph.utils.FileUtils; +import nonapi.io.github.classgraph.utils.JarUtils; + +/** A class storing accept or reject criteria. */ +public abstract class AcceptReject { + /** Accepted items (whole-string match). */ + protected Set accept; + /** Rejected items (whole-string match). */ + protected Set reject; + /** Accepted items (prefix match), as a set. */ + protected Set acceptPrefixesSet; + /** Accepted items (prefix match), as a sorted list. */ + protected List acceptPrefixes; + /** Rejected items (prefix match). */ + protected List rejectPrefixes; + /** Accept glob strings. (Serialized to JSON, for logging purposes.) */ + protected Set acceptGlobs; + /** Reject glob strings. (Serialized to JSON, for logging purposes.) */ + protected Set rejectGlobs; + /** Accept regexp patterns. (Not serialized to JSON.) */ + protected transient List acceptPatterns; + /** Reject regexp patterns. (Not serialized to JSON.) */ + protected transient List rejectPatterns; + /** The separator character. */ + protected char separatorChar; + + /** Deserialization constructor. */ + public AcceptReject() { + } + + /** + * Constructor for deserialization. + * + * @param separatorChar + * the separator char + */ + public AcceptReject(final char separatorChar) { + this.separatorChar = separatorChar; + } + + /** Accept/reject for prefix strings. */ + public static class AcceptRejectPrefix extends AcceptReject { + /** Deserialization constructor. */ + public AcceptRejectPrefix() { + super(); + } + + /** + * Instantiate a new accept/reject for prefix strings. + * + * @param separatorChar + * the separator char + */ + public AcceptRejectPrefix(final char separatorChar) { + super(separatorChar); + } + + /** + * Add to the accept. + * + * @param str + * the string to accept + */ + @Override + public void addToAccept(final String str) { + if (str.contains("*")) { + throw new IllegalArgumentException("Cannot use a glob wildcard here: " + str); + } + if (this.acceptPrefixesSet == null) { + this.acceptPrefixesSet = new HashSet<>(); + } + this.acceptPrefixesSet.add(str); + } + + /** + * Add to the reject. + * + * @param str + * the string to reject + */ + @Override + public void addToReject(final String str) { + if (str.contains("*")) { + throw new IllegalArgumentException("Cannot use a glob wildcard here: " + str); + } + if (this.rejectPrefixes == null) { + this.rejectPrefixes = new ArrayList<>(); + } + this.rejectPrefixes.add(str); + } + + /** + * Check if the requested string has an accepted/non-rejected prefix. + * + * @param str + * the string to test + * @return true if string is accepted and not rejected + */ + @Override + public boolean isAcceptedAndNotRejected(final String str) { + boolean isAccepted = acceptPrefixes == null; + if (!isAccepted) { + for (final String prefix : acceptPrefixes) { + if (str.startsWith(prefix)) { + isAccepted = true; + break; + } + } + } + if (!isAccepted) { + return false; + } + if (rejectPrefixes != null) { + for (final String prefix : rejectPrefixes) { + if (str.startsWith(prefix)) { + return false; + } + } + } + return true; + } + + /** + * Check if the requested string has an accepted prefix. + * + * @param str + * the string to test + * @return true if string is accepted + */ + @Override + public boolean isAccepted(final String str) { + boolean isAccepted = acceptPrefixes == null; + if (!isAccepted) { + for (final String prefix : acceptPrefixes) { + if (str.startsWith(prefix)) { + isAccepted = true; + break; + } + } + } + return isAccepted; + } + + /** + * Prefix-of-prefix is invalid -- throws {@link IllegalArgumentException}. + * + * @param str + * the string to test + * @return (does not return, throws exception) + * @throws IllegalArgumentException + * always + */ + @Override + public boolean acceptHasPrefix(final String str) { + throw new IllegalArgumentException("Can only find prefixes of whole strings"); + } + + /** + * Check if the requested string has a rejected prefix. + * + * @param str + * the string to test + * @return true if the string has a rejected prefix + */ + @Override + public boolean isRejected(final String str) { + if (rejectPrefixes != null) { + for (final String prefix : rejectPrefixes) { + if (str.startsWith(prefix)) { + return true; + } + } + } + return false; + } + } + + /** Accept/reject for whole-strings matches. */ + public static class AcceptRejectWholeString extends AcceptReject { + /** Deserialization constructor. */ + public AcceptRejectWholeString() { + super(); + } + + /** + * Instantiate a new accept/reject for whole-string matches. + * + * @param separatorChar + * the separator char + */ + public AcceptRejectWholeString(final char separatorChar) { + super(separatorChar); + } + + /** + * Add to the accept. + * + * @param str + * the string to accept + */ + @Override + public void addToAccept(final String str) { + if (str.contains("*")) { + if (this.acceptGlobs == null) { + this.acceptGlobs = new HashSet<>(); + this.acceptPatterns = new ArrayList<>(); + } + this.acceptGlobs.add(str); + this.acceptPatterns.add(globToPattern(str, /* simpleGlob = */ true)); + } else { + if (this.accept == null) { + this.accept = new HashSet<>(); + } + this.accept.add(str); + } + + // For AcceptRejectWholeString, which doesn't perform prefix matches like AcceptRejectPrefix, + // use acceptPrefixes to store all parent prefixes of an accepted path, so that + // acceptHasPrefix() can operate efficiently on very large accepts (#338), + // in particular where the size of the accept is much larger than the maximum path depth. + if (this.acceptPrefixesSet == null) { + this.acceptPrefixesSet = new HashSet<>(); + acceptPrefixesSet.add(""); + acceptPrefixesSet.add("/"); + } + final String separator = Character.toString(separatorChar); + String prefix = str; + if (prefix.contains("*")) { + // Stop performing prefix search at first '*' -- this means prefix matching will + // break if there is more than one '*' in the path + prefix = prefix.substring(0, prefix.indexOf('*')); + // /path/to/wildcard*.jar -> /path/to + // /path/to/*.jar -> /path/to + final int sepIdx = prefix.lastIndexOf(separatorChar); + if (sepIdx < 0) { + prefix = ""; + } else { + prefix = prefix.substring(0, prefix.lastIndexOf(separatorChar)); + } + } + // Strip off any final separator + while (prefix.endsWith(separator)) { + prefix = prefix.substring(0, prefix.length() - 1); + } + // Add str itself as a prefix (this will only match a parent dir for + for (; !prefix.isEmpty(); prefix = FileUtils.getParentDirPath(prefix, separatorChar)) { + acceptPrefixesSet.add(prefix + separatorChar); + } + } + + /** + * Add to the reject. + * + * @param str + * the string to reject + */ + @Override + public void addToReject(final String str) { + if (str.contains("*")) { + if (this.rejectGlobs == null) { + this.rejectGlobs = new HashSet<>(); + this.rejectPatterns = new ArrayList<>(); + } + this.rejectGlobs.add(str); + this.rejectPatterns.add(globToPattern(str, /* simpleGlob = */ true)); + } else { + if (this.reject == null) { + this.reject = new HashSet<>(); + } + this.reject.add(str); + } + } + + /** + * Check if the requested string is accepted and not rejected. + * + * @param str + * the string to test + * @return true if the string is accepted and not rejected + */ + @Override + public boolean isAcceptedAndNotRejected(final String str) { + return isAccepted(str) && !isRejected(str); + } + + /** + * Check if the requested string is accepted. + * + * @param str + * the string to test + * @return true if the string is accepted + */ + @Override + public boolean isAccepted(final String str) { + return (accept == null && acceptPatterns == null) || (accept != null && accept.contains(str)) + || matchesPatternList(str, acceptPatterns); + } + + /** + * Check if the requested string is a prefix of an accepted string. + * + * @param str + * the string to test + * @return true if the string is a prefix of an accepted string + */ + @Override + public boolean acceptHasPrefix(final String str) { + if (acceptPrefixesSet == null) { + return false; + } + return acceptPrefixesSet.contains(str); + } + + /** + * Check if the requested string is rejected. + * + * @param str + * the string to test + * @return true if the string is rejected + */ + @Override + public boolean isRejected(final String str) { + return (reject != null && reject.contains(str)) || matchesPatternList(str, rejectPatterns); + } + } + + /** Accept/reject for leaf matches. */ + public static class AcceptRejectLeafname extends AcceptRejectWholeString { + /** Deserialization constructor. */ + public AcceptRejectLeafname() { + super(); + } + + /** + * Instantiates a new accept/reject for leaf matches. + * + * @param separatorChar + * the separator char + */ + public AcceptRejectLeafname(final char separatorChar) { + super(separatorChar); + } + + /** + * Add to the accept. + * + * @param str + * the string to accept + */ + @Override + public void addToAccept(final String str) { + super.addToAccept(JarUtils.leafName(str)); + } + + /** + * Add to the reject. + * + * @param str + * the string to reject + */ + @Override + public void addToReject(final String str) { + super.addToReject(JarUtils.leafName(str)); + } + + /** + * Check if the requested string is accepted and not rejected. + * + * @param str + * the string to test + * @return true if the string is accepted and not rejected + */ + @Override + public boolean isAcceptedAndNotRejected(final String str) { + return super.isAcceptedAndNotRejected(JarUtils.leafName(str)); + } + + /** + * Check if the requested string is accepted. + * + * @param str + * the string to test + * @return true if the string is accepted + */ + @Override + public boolean isAccepted(final String str) { + return super.isAccepted(JarUtils.leafName(str)); + } + + /** + * Prefix tests are invalid for jar leafnames -- throws {@link IllegalArgumentException}. + * + * @param str + * the string to test + * @return (does not return, throws exception) + * @throws IllegalArgumentException + * always + */ + @Override + public boolean acceptHasPrefix(final String str) { + throw new IllegalArgumentException("Can only find prefixes of whole strings"); + } + + /** + * Check if the requested string is rejected. + * + * @param str + * the string to test + * @return true if the string is rejected + */ + @Override + public boolean isRejected(final String str) { + return super.isRejected(JarUtils.leafName(str)); + } + } + + /** + * Add to the accept. + * + * @param str + * The string to accept. + */ + public abstract void addToAccept(final String str); + + /** + * Add to the reject. + * + * @param str + * The string to reject. + */ + public abstract void addToReject(final String str); + + /** + * Check if a string is accepted and not rejected. + * + * @param str + * The string to test. + * @return true if the string is accepted and not rejected. + */ + public abstract boolean isAcceptedAndNotRejected(final String str); + + /** + * Check if a string is accepted. + * + * @param str + * The string to test. + * @return true if the string is accepted. + */ + public abstract boolean isAccepted(final String str); + + /** + * Check if a string is a prefix of an accepted string. + * + * @param str + * The string to test. + * @return true if the string is a prefix of an accepted string. + */ + public abstract boolean acceptHasPrefix(final String str); + + /** + * Check if a string is rejected. + * + * @param str + * The string to test. + * @return true if the string is rejected. + */ + public abstract boolean isRejected(final String str); + + /** + * Remove initial and final '/' characters, if any. + * + * @param path + * The path to normalize. + * @return The normalized path. + */ + public static String normalizePath(final String path) { + String pathResolved = FastPathResolver.resolve(path); + while (pathResolved.startsWith("/")) { + pathResolved = pathResolved.substring(1); + } + return pathResolved; + } + + /** + * Remove initial and final '.' characters, if any. + * + * @param packageOrClassName + * The package or class name. + * @return The normalized package or class name. + */ + public static String normalizePackageOrClassName(final String packageOrClassName) { + return normalizePath(packageOrClassName.replace('.', '/')).replace('/', '.'); + } + + /** + * Convert a path to a package name. + * + * @param path + * The path. + * @return The package name. + */ + public static String pathToPackageName(final String path) { + return path.replace('/', '.'); + } + + /** + * Convert a package name to a path. + * + * @param packageName + * The package name. + * @return The path. + */ + public static String packageNameToPath(final String packageName) { + return packageName.replace('.', '/'); + } + + /** + * Convert a class name to a classfile path. + * + * @param className + * The class name. + * @return The classfile path (including a ".class" suffix). + */ + public static String classNameToClassfilePath(final String className) { + return JarUtils.classNameToClassfilePath(className); + } + + /** + * Convert a spec with a '*' glob character into a regular expression. + * + * @param glob + * The glob string. + * @param simpleGlob + * if true, handles simple globs: "*" matches zero or more characters (replaces "." with "\\.", "*" + * with ".*", then compiles a regular expression). If false, handles filesystem-style globs: "**" + * matches zero or more characters, "*" matches zero or more characters other than "/", "?" matches + * one character (replaces "." with "\\.", "**" with ".*", "*" with "[^/]*", and "?" with ".", then + * compiles a regular expression). + * @return The Pattern created from the glob string. + */ + public static Pattern globToPattern(final String glob, final boolean simpleGlob) { + // TODO: when API is next changed, make all glob behavior consistent between accept/reject criteria + // and resource filtering (i.e. enforce simpleGlob == false, at least for accept/reject criteria for + // paths, although packages/classes would need different handling because ** should work across + // packages of any depth, rather than paths of any number of segments) + return Pattern.compile("^" // + + (simpleGlob // + ? glob.replace(".", "\\.") // + .replace("*", ".*") // + : glob.replace(".", "\\.") // + .replace("*", "[^/]*") // + .replace("[^/]*[^/]*", ".*") // + .replace('?', '.') // + ) // + + "$"); + } + + /** + * Check if a string matches one of the patterns in the provided list. + * + * @param str + * the string to test + * @param patterns + * the patterns + * @return true, if successful + */ + private static boolean matchesPatternList(final String str, final List patterns) { + if (patterns != null) { + for (final Pattern pattern : patterns) { + if (pattern.matcher(str).matches()) { + return true; + } + } + } + return false; + } + + /** + * Check if the accept is empty. + * + * @return true if there were no accept criteria added. + */ + public boolean acceptIsEmpty() { + return accept == null && acceptPrefixes == null && acceptGlobs == null; + } + + /** + * Check if the reject is empty. + * + * @return true if there were no reject criteria added. + */ + public boolean rejectIsEmpty() { + return reject == null && rejectPrefixes == null && rejectGlobs == null; + } + + /** + * Check if the accept and reject are empty. + * + * @return true if there were no accept or reject criteria added. + */ + public boolean acceptAndRejectAreEmpty() { + return acceptIsEmpty() && rejectIsEmpty(); + } + + /** + * Check if a string is specifically accepted and not rejected. + * + * @param str + * The string to test. + * @return true if the requested string is specifically accepted and not rejected, i.e. will not return + * true if the accept is empty, or if the string is rejected. + */ + public boolean isSpecificallyAcceptedAndNotRejected(final String str) { + return !acceptIsEmpty() && isAcceptedAndNotRejected(str); + } + + /** + * Check if a string is specifically accepted. + * + * @param str + * The string to test. + * @return true if the requested string is specifically accepted, i.e. will not return true if the accept + * is empty. + */ + public boolean isSpecificallyAccepted(final String str) { + return !acceptIsEmpty() && isAccepted(str); + } + + /** Need to sort prefixes to ensure correct accept/reject evaluation (see Issue #167). */ + void sortPrefixes() { + if (acceptPrefixesSet != null) { + acceptPrefixes = new ArrayList<>(acceptPrefixesSet); + } + if (acceptPrefixes != null) { + CollectionUtils.sortIfNotEmpty(acceptPrefixes); + } + if (rejectPrefixes != null) { + CollectionUtils.sortIfNotEmpty(rejectPrefixes); + } + } + + /** + * Quote list. + * + * @param coll + * the coll + * @param buf + * the buf + */ + private static void quoteList(final Collection coll, final StringBuilder buf) { + buf.append('['); + boolean first = true; + for (final String item : coll) { + if (first) { + first = false; + } else { + buf.append(", "); + } + buf.append('"'); + for (int i = 0; i < item.length(); i++) { + final char c = item.charAt(i); + if (c == '"') { + buf.append("\\\""); + } else { + buf.append(c); + } + } + buf.append('"'); + } + buf.append(']'); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + if (accept != null) { + buf.append("accept: "); + quoteList(accept, buf); + } + if (acceptPrefixes != null) { + if (buf.length() > 0) { + buf.append("; "); + } + buf.append("acceptPrefixes: "); + quoteList(acceptPrefixes, buf); + } + if (acceptGlobs != null) { + if (buf.length() > 0) { + buf.append("; "); + } + buf.append("acceptGlobs: "); + quoteList(acceptGlobs, buf); + } + if (reject != null) { + if (buf.length() > 0) { + buf.append("; "); + } + buf.append("reject: "); + quoteList(reject, buf); + } + if (rejectPrefixes != null) { + if (buf.length() > 0) { + buf.append("; "); + } + buf.append("rejectPrefixes: "); + quoteList(rejectPrefixes, buf); + } + if (rejectGlobs != null) { + if (buf.length() > 0) { + buf.append("; "); + } + buf.append("rejectGlobs: "); + quoteList(rejectGlobs, buf); + } + return buf.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/nonapi/io/github/classgraph/ScanSpec.java b/src/main/java/nonapi/io/github/classgraph/scanspec/ScanSpec.java similarity index 54% rename from src/main/java/nonapi/io/github/classgraph/ScanSpec.java rename to src/main/java/nonapi/io/github/classgraph/scanspec/ScanSpec.java index c8be41e36..39e06b28c 100644 --- a/src/main/java/nonapi/io/github/classgraph/ScanSpec.java +++ b/src/main/java/nonapi/io/github/classgraph/scanspec/ScanSpec.java @@ -26,69 +26,74 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE * OR OTHER DEALINGS IN THE SOFTWARE. */ -package nonapi.io.github.classgraph; +package nonapi.io.github.classgraph.scanspec; +import java.io.InputStream; import java.lang.reflect.Field; +import java.net.URI; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import io.github.classgraph.ClassGraph.ClasspathElementFilter; -import io.github.classgraph.ClassGraphException; +import io.github.classgraph.ClassGraph.ClasspathElementURLFilter; import io.github.classgraph.ClassInfo; import io.github.classgraph.ModulePathInfo; import io.github.classgraph.ScanResult; -import nonapi.io.github.classgraph.WhiteBlackList.WhiteBlackListLeafname; -import nonapi.io.github.classgraph.WhiteBlackList.WhiteBlackListPrefix; -import nonapi.io.github.classgraph.WhiteBlackList.WhiteBlackListWholeString; +import nonapi.io.github.classgraph.scanspec.AcceptReject.AcceptRejectLeafname; +import nonapi.io.github.classgraph.scanspec.AcceptReject.AcceptRejectPrefix; +import nonapi.io.github.classgraph.scanspec.AcceptReject.AcceptRejectWholeString; import nonapi.io.github.classgraph.utils.LogNode; /** * The scanning specification. */ public class ScanSpec { - /** Package white/blacklist (with separator '.'). */ - public WhiteBlackListWholeString packageWhiteBlackList = new WhiteBlackListWholeString(); + /** Package accept/reject criteria (with separator '.'). */ + public AcceptRejectWholeString packageAcceptReject = new AcceptRejectWholeString('.'); - /** Package prefix white/blacklist, for recursive scanning (with separator '.', ending in '.'). */ - public WhiteBlackListPrefix packagePrefixWhiteBlackList = new WhiteBlackListPrefix(); + /** Package prefix accept/reject criteria, for recursive scanning (with separator '.', ending in '.'). */ + public AcceptRejectPrefix packagePrefixAcceptReject = new AcceptRejectPrefix('.'); - /** Path white/blacklist (with separator '/'). */ - public WhiteBlackListWholeString pathWhiteBlackList = new WhiteBlackListWholeString(); + /** Path accept/reject criteria (with separator '/'). */ + public AcceptRejectWholeString pathAcceptReject = new AcceptRejectWholeString('/'); - /** Path prefix white/blacklist, for recursive scanning (with separator '/', ending in '/'). */ - public WhiteBlackListPrefix pathPrefixWhiteBlackList = new WhiteBlackListPrefix(); + /** Path prefix accept/reject criteria, for recursive scanning (with separator '/', ending in '/'). */ + public AcceptRejectPrefix pathPrefixAcceptReject = new AcceptRejectPrefix('/'); - /** Class white/blacklist (fully-qualified class names, with separator '.'). */ - public WhiteBlackListWholeString classWhiteBlackList = new WhiteBlackListWholeString(); + /** Class accept/reject criteria (fully-qualified class names, with separator '.'). */ + public AcceptRejectWholeString classAcceptReject = new AcceptRejectWholeString('.'); - /** Classfile white/blacklist (path to classfiles, with separator '/', ending in ".class"). */ - public WhiteBlackListWholeString classfilePathWhiteBlackList = new WhiteBlackListWholeString(); + /** Classfile accept/reject criteria (path to classfiles, with separator '/', ending in ".class"). */ + public AcceptRejectWholeString classfilePathAcceptReject = new AcceptRejectWholeString('/'); - /** Package containing white/blacklisted classes (with separator '.'). */ - public WhiteBlackListWholeString classPackageWhiteBlackList = new WhiteBlackListWholeString(); + /** Package containing accept/reject criteriaed classes (with separator '.'). */ + public AcceptRejectWholeString classPackageAcceptReject = new AcceptRejectWholeString('.'); - /** Path to white/blacklisted classes (with separator '/'). */ - public WhiteBlackListWholeString classPackagePathWhiteBlackList = new WhiteBlackListWholeString(); + /** Path to accept/reject criteriaed classes (with separator '/'). */ + public AcceptRejectWholeString classPackagePathAcceptReject = new AcceptRejectWholeString('/'); - /** Module white/blacklist (with separator '.'). */ - public WhiteBlackListWholeString moduleWhiteBlackList = new WhiteBlackListWholeString(); + /** Module accept/reject criteria (with separator '.'). */ + public AcceptRejectWholeString moduleAcceptReject = new AcceptRejectWholeString('.'); - /** Jar white/blacklist (leafname only, ending in ".jar"). */ - public WhiteBlackListLeafname jarWhiteBlackList = new WhiteBlackListLeafname(); + /** Jar accept/reject criteria (leafname only, ending in ".jar"). */ + public AcceptRejectLeafname jarAcceptReject = new AcceptRejectLeafname('/'); - /** Classpath element resource path white/blacklist. */ - public WhiteBlackListWholeString classpathElementResourcePathWhiteBlackList = // - new WhiteBlackListWholeString(); + /** Classpath element resource path accept/reject criteria. */ + public AcceptRejectWholeString classpathElementResourcePathAcceptReject = // + new AcceptRejectWholeString('/'); - /** lib/ext jar white/blacklist (leafname only, ending in ".jar"). */ - public WhiteBlackListLeafname libOrExtJarWhiteBlackList = new WhiteBlackListLeafname(); + /** lib/ext jar accept/reject criteria (leafname only, ending in ".jar"). */ + public AcceptRejectLeafname libOrExtJarAcceptReject = new AcceptRejectLeafname('/'); // ------------------------------------------------------------------------------------------------------------- - /** If true, performing a scan. If false, only fetching the classpath. */ - public boolean performScan = true; - /** If true, scan jarfiles. */ public boolean scanJars = true; @@ -130,9 +135,9 @@ public class ScanSpec { public boolean enableInterClassDependencies; /** - * If true, allow external classes (classes outside of whitelisted packages) to be returned in the ScanResult, - * if they are directly referred to by a whitelisted class, as a superclass, implemented interface or - * annotation. Disabled by default. + * If true, allow external classes (classes outside of accepted packages) to be returned in the ScanResult, if + * they are directly referred to by an accepted class, as a superclass, implemented interface or annotation. + * Disabled by default. */ public boolean enableExternalClasses; @@ -169,11 +174,10 @@ public class ScanSpec { public boolean extendScanningUpwardsToExternalClasses = true; /** - * If true, enable http(s) classpath elements to be fetched to local temporary files and scanned. Disabled by - * default as this may present a security vulnerability, since classes from downloaded jars can be subsequently - * loaded using {@link ClassInfo#loadClass}. + * URL schemes that are allowed in classpath elements (not counting the optional "jar:" prefix and/or "file:", + * which are automatically allowed). */ - public boolean enableRemoteJarScanning; + public Set allowedURLSchemes; // ------------------------------------------------------------------------------------------------------------- @@ -198,11 +202,14 @@ public class ScanSpec { */ public transient List overrideModuleLayers; - /** If non-null, specifies a classpath to override the default one. */ - public String overrideClasspath; + /** + * If non-null, specifies a list of classpath elements (String, {@link URL} or {@link URI} to use to override + * the default classpath. + */ + public List overrideClasspath; /** If non-null, a list of filter operations to apply to classpath elements. */ - public transient List classpathElementFilters; + public transient List classpathElementFilters; /** Whether to initialize classes when loading them. */ public boolean initializeLoadedClasses; @@ -225,6 +232,37 @@ public class ScanSpec { // ------------------------------------------------------------------------------------------------------------- + /** + * The maximum size of an inner (nested) jar that has been deflated (i.e. compressed, not stored) within an + * outer jar, before it has to be spilled to disk rather than stored in a RAM-backed {@link ByteBuffer} when it + * is deflated, in order for the inner jar's entries to be read. (Note that this situation of having to deflate + * a nested jar to RAM or disk in order to read it is rare, because normally adding a jarfile to another jarfile + * will store the inner jar, rather than deflate it, because deflating a jarfile does not usually produce any + * further compression gains. If an inner jar is stored, not deflated, then its zip entries can be read directly + * using ClassGraph's own zipfile central directory parser, which can use file slicing to extract entries + * directly from stored nested jars.) + * + *

+ * This is also the maximum size of a jar downloaded from an {@code http://} or {@code https://} classpath + * {@link URL} to RAM. Once this many bytes have been read from the {@link URL}'s {@link InputStream}, then the + * RAM contents are spilled over to a temporary file on disk, and the rest of the content is downloaded to the + * temporary file. (This is also rare, because normally there are no {@code http://} or {@code https://} + * classpath entries.) + * + *

+ * Default: 64MB (i.e. writing to disk is avoided wherever possible). Setting a lower max RAM size value will + * decrease ClassGraph's memory usage if either of the above rare situations occurs. + */ + public int maxBufferedJarRAMSize = 64 * 1024 * 1024; + + /** If true, use a {@link MappedByteBuffer} rather than the {@link FileChannel} API to access file content. */ + public boolean enableMemoryMapping; + + /** If true, all multi-release versions of a resource are found. */ + public boolean enableMultiReleaseVersions; + + // ------------------------------------------------------------------------------------------------------------- + /** Constructor for deserialization. */ public ScanSpec() { // Intentionally empty @@ -232,14 +270,14 @@ public ScanSpec() { // ------------------------------------------------------------------------------------------------------------- - /** Sort prefixes to ensure correct whitelist/blacklist evaluation (see Issue #167). */ + /** Sort prefixes to ensure correct accept/reject evaluation (see Issue #167). */ public void sortPrefixes() { for (final Field field : ScanSpec.class.getDeclaredFields()) { - if (WhiteBlackList.class.isAssignableFrom(field.getType())) { + if (AcceptReject.class.isAssignableFrom(field.getType())) { try { - ((WhiteBlackList) field.get(this)).sortPrefixes(); + ((AcceptReject) field.get(this)).sortPrefixes(); } catch (final ReflectiveOperationException e) { - throw ClassGraphException.newClassGraphException("Field is not accessible: " + field, e); + throw new RuntimeException("Field is not accessible: " + field, e); } } } @@ -248,30 +286,46 @@ public void sortPrefixes() { // ------------------------------------------------------------------------------------------------------------- /** - * Override the automatically-detected classpath with a custom search path. You can specify multiple elements, - * separated by File.pathSeparatorChar. If this method is called, nothing but the provided classpath will be - * scanned, i.e. causes ClassLoaders to be ignored, as well as the java.class.path system property. + * Override the automatically-detected classpath with a custom path. You can specify multiple elements in + * separate calls, and if this method is called even once, the default classpath will be overridden, such that + * nothing but the provided classpath will be scanned, i.e. causes ClassLoaders to be ignored, as well as the + * java.class.path system property. * - * @param overrideClasspath - * The classpath to scan. + * @param overrideClasspathElement + * The classpath element to add as an override to the default classpath. */ - public void overrideClasspath(final String overrideClasspath) { - this.overrideClasspath = overrideClasspath; + public void addClasspathOverride(final Object overrideClasspathElement) { + if (this.overrideClasspath == null) { + this.overrideClasspath = new ArrayList<>(); + } + if (overrideClasspathElement instanceof ClassLoader) { + throw new IllegalArgumentException( + "Need to pass ClassLoader instances to overrideClassLoaders, not overrideClasspath"); + } + this.overrideClasspath + .add(overrideClasspathElement instanceof String || overrideClasspathElement instanceof URL + || overrideClasspathElement instanceof URI ? overrideClasspathElement + : overrideClasspathElement.toString()); } /** - * Add a classpath element filter. The provided ClasspathElementFilter should return true if the path string - * passed to it is a path you want to scan. + * Add a classpath element filter. The provided {@link ClasspathElementFilter} or + * {@link ClasspathElementURLFilter} should return true if the path string or {@link URL} passed to it is a path + * that should be scanned. * - * @param classpathElementFilter + * @param filterLambda * The classpath element filter to apply to all discovered classpath elements, to decide which should * be scanned. */ - public void filterClasspathElements(final ClasspathElementFilter classpathElementFilter) { + public void filterClasspathElements(final Object filterLambda) { + if (!(filterLambda instanceof ClasspathElementFilter + || filterLambda instanceof ClasspathElementURLFilter)) { + throw new IllegalArgumentException(); + } if (this.classpathElementFilters == null) { this.classpathElementFilters = new ArrayList<>(2); } - this.classpathElementFilters.add(classpathElementFilter); + this.classpathElementFilters.add(filterLambda); } /** @@ -290,6 +344,22 @@ public void addClassLoader(final ClassLoader classLoader) { } } + /** + * Allow a specified URL scheme in classpath elements. + * + * @param scheme + * the scheme, e.g. "http". + */ + public void enableURLScheme(final String scheme) { + if (scheme == null || scheme.length() < 2) { + throw new IllegalArgumentException("URL schemes must contain at least two characters"); + } + if (allowedURLSchemes == null) { + allowedURLSchemes = new HashSet<>(); + } + allowedURLSchemes.add(scheme.toLowerCase()); + } + /** * Completely override the list of ClassLoaders to scan. (This only works if overrideClasspath() is not called.) * Causes the java.class.path system property to be ignored. @@ -382,104 +452,97 @@ public void overrideModuleLayers(final Object... overrideModuleLayers) { // ------------------------------------------------------------------------------------------------------------- /** - * Whether a path is a descendant of a blacklisted path, or an ancestor or descendant of a whitelisted path. + * Whether a path is a descendant of a rejected path, or an ancestor or descendant of an accepted path. */ public enum ScanSpecPathMatch { - /** Path starts with (or is) a blacklisted path prefix. */ - HAS_BLACKLISTED_PATH_PREFIX, - /** Path starts with a whitelisted path prefix. */ - HAS_WHITELISTED_PATH_PREFIX, - /** Path is whitelisted. */ - AT_WHITELISTED_PATH, - /** Path is an ancestor of a whitelisted path. */ - ANCESTOR_OF_WHITELISTED_PATH, - /** Path is the package of a specifically-whitelisted class. */ - AT_WHITELISTED_CLASS_PACKAGE, - /** Path is not whitelisted and not blacklisted. */ - NOT_WITHIN_WHITELISTED_PATH + /** Path starts with (or is) a rejected path prefix. */ + HAS_REJECTED_PATH_PREFIX, + /** Path starts with an accepted path prefix. */ + HAS_ACCEPTED_PATH_PREFIX, + /** Path is accepted. */ + AT_ACCEPTED_PATH, + /** Path is an ancestor of an accepted path. */ + ANCESTOR_OF_ACCEPTED_PATH, + /** Path is the package of a specifically-accepted class. */ + AT_ACCEPTED_CLASS_PACKAGE, + /** Path is not accepted and not rejected. */ + NOT_WITHIN_ACCEPTED_PATH } /** - * Returns true if the given directory path is a descendant of a blacklisted path, or an ancestor or descendant - * of a whitelisted path. The path should end in "/". + * Returns true if the given directory path is a descendant of a rejected path, or an ancestor or descendant of + * an accepted path. The path should end in "/". * * @param relativePath * the relative path * @return the {@link ScanSpecPathMatch} */ - public ScanSpecPathMatch dirWhitelistMatchStatus(final String relativePath) { - // In blacklisted path - if (pathWhiteBlackList.isBlacklisted(relativePath)) { - // The directory is blacklisted. - return ScanSpecPathMatch.HAS_BLACKLISTED_PATH_PREFIX; - } - if (pathPrefixWhiteBlackList.isBlacklisted(relativePath)) { - // An prefix of this path is blacklisted. - return ScanSpecPathMatch.HAS_BLACKLISTED_PATH_PREFIX; + public ScanSpecPathMatch dirAcceptMatchStatus(final String relativePath) { + // In rejected path + if (pathAcceptReject.isRejected(relativePath) || pathPrefixAcceptReject.isRejected(relativePath)) { + // An prefix of this path is rejected. + return ScanSpecPathMatch.HAS_REJECTED_PATH_PREFIX; } - if (pathWhiteBlackList.whitelistIsEmpty() && classPackagePathWhiteBlackList.whitelistIsEmpty()) { - // If there are no whitelisted packages, the root package is whitelisted - return relativePath.isEmpty() || relativePath.equals("/") ? ScanSpecPathMatch.AT_WHITELISTED_PATH - : ScanSpecPathMatch.HAS_WHITELISTED_PATH_PREFIX; + if (pathAcceptReject.acceptIsEmpty() && classPackagePathAcceptReject.acceptIsEmpty()) { + // If there are no accepted packages, the root package is accepted + return relativePath.isEmpty() || relativePath.equals("/") ? ScanSpecPathMatch.AT_ACCEPTED_PATH + : ScanSpecPathMatch.HAS_ACCEPTED_PATH_PREFIX; } - // At whitelisted path - if (pathWhiteBlackList.isSpecificallyWhitelistedAndNotBlacklisted(relativePath)) { - // Reached a whitelisted path - return ScanSpecPathMatch.AT_WHITELISTED_PATH; + // At accepted path + if (pathAcceptReject.isSpecificallyAcceptedAndNotRejected(relativePath)) { + // Reached an accepted path + return ScanSpecPathMatch.AT_ACCEPTED_PATH; } - if (classPackagePathWhiteBlackList.isSpecificallyWhitelistedAndNotBlacklisted(relativePath)) { - // Reached a package containing a specifically-whitelisted class - return ScanSpecPathMatch.AT_WHITELISTED_CLASS_PACKAGE; + if (classPackagePathAcceptReject.isSpecificallyAcceptedAndNotRejected(relativePath)) { + // Reached a package containing a specifically-accepted class + return ScanSpecPathMatch.AT_ACCEPTED_CLASS_PACKAGE; } - // Descendant of whitelisted path - if (pathPrefixWhiteBlackList.isSpecificallyWhitelisted(relativePath)) { - // Path prefix matches one in the whitelist - return ScanSpecPathMatch.HAS_WHITELISTED_PATH_PREFIX; + // Descendant of accepted path + if (pathPrefixAcceptReject.isSpecificallyAccepted(relativePath)) { + // Path prefix matches one in the accept + return ScanSpecPathMatch.HAS_ACCEPTED_PATH_PREFIX; } - // Ancestor of whitelisted path - if (relativePath.equals("/")) { - // The default package is always the ancestor of whitelisted paths (need to keep recursing) - return ScanSpecPathMatch.ANCESTOR_OF_WHITELISTED_PATH; - } - if (pathWhiteBlackList.whitelistHasPrefix(relativePath)) { - // relativePath is an ancestor (prefix) of a whitelisted path - return ScanSpecPathMatch.ANCESTOR_OF_WHITELISTED_PATH; - } - if (classfilePathWhiteBlackList.whitelistHasPrefix(relativePath)) { - // relativePath is an ancestor (prefix) of a whitelisted class' parent directory - return ScanSpecPathMatch.ANCESTOR_OF_WHITELISTED_PATH; + // Ancestor of accepted path + if ( + // The default package is always the ancestor of accepted paths (need to keep recursing) + relativePath.equals("/") + // relativePath is an ancestor (prefix) of an accepted path + || pathAcceptReject.acceptHasPrefix(relativePath) + // relativePath is an ancestor (prefix) of an accepted class' parent directory + || classfilePathAcceptReject.acceptHasPrefix(relativePath)) { + return ScanSpecPathMatch.ANCESTOR_OF_ACCEPTED_PATH; } - // Not in whitelisted path - return ScanSpecPathMatch.NOT_WITHIN_WHITELISTED_PATH; + // Not in accepted path + return ScanSpecPathMatch.NOT_WITHIN_ACCEPTED_PATH; } /** * Returns true if the given relative path (for a classfile name, including ".class") matches a - * specifically-whitelisted (and non-blacklisted) classfile's relative path. + * specifically-accepted (and non-rejected) classfile's relative path. * * @param relativePath * the relative path * @return true if the given relative path (for a classfile name, including ".class") matches a - * specifically-whitelisted (and non-blacklisted) classfile's relative path. + * specifically-accepted (and non-rejected) classfile's relative path. */ - public boolean classfileIsSpecificallyWhitelisted(final String relativePath) { - return classfilePathWhiteBlackList.isSpecificallyWhitelistedAndNotBlacklisted(relativePath); + public boolean classfileIsSpecificallyAccepted(final String relativePath) { + return classfilePathAcceptReject.isSpecificallyAcceptedAndNotRejected(relativePath); } /** - * Returns true if the class is specifically blacklisted, or is within a blacklisted package. + * Returns true if the class is specifically rejected, or is within a rejected package. * * @param className * the class name - * @return true if the class is specifically blacklisted, or is within a blacklisted package. + * @return true if the class is specifically rejected, or is within a rejected package. */ - public boolean classOrPackageIsBlacklisted(final String className) { - return classWhiteBlackList.isBlacklisted(className) || packagePrefixWhiteBlackList.isBlacklisted(className); + public boolean classOrPackageIsRejected(final String className) { + return classAcceptReject.isRejected(className) || packagePrefixAcceptReject.isRejected(className); } // ------------------------------------------------------------------------------------------------------------- diff --git a/src/main/java/nonapi/io/github/classgraph/types/TypeUtils.java b/src/main/java/nonapi/io/github/classgraph/types/TypeUtils.java index 34869fde8..7dd48db8a 100644 --- a/src/main/java/nonapi/io/github/classgraph/types/TypeUtils.java +++ b/src/main/java/nonapi/io/github/classgraph/types/TypeUtils.java @@ -45,27 +45,27 @@ private TypeUtils() { } /** - * Parse a Java identifier with the given separator ('.' or '/'). Potentially replaces the separator with a - * different character. Appends the identifier to the token buffer in the parser. + * Parse a Java identifier, replacing '/' with '.'. Appends the identifier to the token buffer in the parser. * * @param parser * The parser. - * @param separator - * The separator character. - * @param separatorReplace - * The character to replace the separator with. + * @param stopAtDollarSign + * If true, stop parsing when the first '$' is hit. + * @param stopAtDot + * If true, stop parsing when the first '.' is hit. * @return true if at least one identifier character was parsed. */ - public static boolean getIdentifierToken(final Parser parser, final char separator, - final char separatorReplace) { + public static boolean getIdentifierToken(final Parser parser, final boolean stopAtDollarSign, + final boolean stopAtDot) { boolean consumedChar = false; while (parser.hasMore()) { final char c = parser.peek(); - if (c == separator) { - parser.appendToToken(separatorReplace); + if (c == '/') { + parser.appendToToken('.'); parser.next(); consumedChar = true; - } else if (c != ';' && c != '[' && c != '<' && c != '>' && c != ':' && c != '/' && c != '.') { + } else if (c != ';' && c != '[' && c != '<' && c != '>' && c != ':' && (!stopAtDollarSign || c != '$') + && (!stopAtDot || c != '.')) { parser.appendToToken(c); parser.next(); consumedChar = true; @@ -76,20 +76,6 @@ public static boolean getIdentifierToken(final Parser parser, final char separat return consumedChar; } - /** - * Parse a Java identifier part (between separators and other non-alphanumeric characters). Appends the - * identifier to the token buffer in the parser. - * - * @param parser - * The parser. - * @return true if at least one identifier character was parsed. - * @throws ParseException - * If the parser ran out of input. - */ - public static boolean getIdentifierToken(final Parser parser) throws ParseException { - return getIdentifierToken(parser, '\0', '\0'); - } - /** The origin of the modifier bits. */ public enum ModifierType { /** The modifier bits apply to a class. */ @@ -97,7 +83,7 @@ public enum ModifierType { /** The modifier bits apply to a method. */ METHOD, /** The modifier bits apply to a field. */ - FIELD; + FIELD } /** diff --git a/src/main/java/nonapi/io/github/classgraph/utils/Assert.java b/src/main/java/nonapi/io/github/classgraph/utils/Assert.java new file mode 100644 index 000000000..936786b9d --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/utils/Assert.java @@ -0,0 +1,32 @@ +package nonapi.io.github.classgraph.utils; + +/** Assertions. */ +public final class Assert { + /** + * Throw {@link IllegalArgumentException} if the class is not an annotation. + * + * @param clazz + * the class. + * @throws IllegalArgumentException + * if the class is not an annotation. + */ + public static void isAnnotation(final Class clazz) { + if (!clazz.isAnnotation()) { + throw new IllegalArgumentException(clazz + " is not an annotation"); + } + } + + /** + * Throw {@link IllegalArgumentException} if the class is not an interface. + * + * @param clazz + * the class. + * @throws IllegalArgumentException + * if the class is not an interface. + */ + public static void isInterface(final Class clazz) { + if (!clazz.isInterface()) { + throw new IllegalArgumentException(clazz + " is not an interface"); + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/utils/CollectionUtils.java b/src/main/java/nonapi/io/github/classgraph/utils/CollectionUtils.java new file mode 100644 index 000000000..c3767c0f5 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/utils/CollectionUtils.java @@ -0,0 +1,95 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.List; + +/** + * Collection utilities. + */ +public final class CollectionUtils { + /** Class can't be constructed. */ + private CollectionUtils() { + // Empty + } + + /** + * Sort a collection if it is not empty (to prevent {@link ConcurrentModificationException} if an immutable + * empty list that has been returned more than once is being sorted in one thread and iterated through in + * another thread -- #334). + * + * @param + * the element type + * @param list + * the list + */ + public static > void sortIfNotEmpty(final List list) { + if (list.size() > 1) { + Collections.sort(list); + } + } + + /** + * Sort a collection if it is not empty (to prevent {@link ConcurrentModificationException} if an immutable + * empty list that has been returned more than once is being sorted in one thread and iterated through in + * another thread -- #334). + * + * @param + * the element type + * @param list + * the list + * @param comparator + * the comparator + */ + public static void sortIfNotEmpty(final List list, final Comparator comparator) { + if (list.size() > 1) { + Collections.sort(list, comparator); + } + } + + /** + * Copy and sort a collection. + * + * @param elts + * the collection to copy and sort + * @return a sorted copy of the collection + */ + public static > List sortCopy(final Collection elts) { + final List sortedCopy = new ArrayList<>(elts); + if (sortedCopy.size() > 1) { + Collections.sort(sortedCopy); + } + return sortedCopy; + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/utils/FastPathResolver.java b/src/main/java/nonapi/io/github/classgraph/utils/FastPathResolver.java index 6c2c5fb1d..6bed8a4f6 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/FastPathResolver.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/FastPathResolver.java @@ -28,11 +28,12 @@ */ package nonapi.io.github.classgraph.utils; -import java.io.File; import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; +import nonapi.io.github.classgraph.utils.VersionFinder.OperatingSystem; + /** * Resolve relative paths and URLs/URIs against a base path in a way that is faster than Java's URL/URI parser (and * much faster than Path), while aiming for cross-platform compatibility, and hopefully in particular being robust @@ -42,8 +43,8 @@ public final class FastPathResolver { /** Match %-encoded characters in URLs. */ private static final Pattern percentMatcher = Pattern.compile("([%][0-9a-fA-F][0-9a-fA-F])+"); - /** True if we're running on Windows. */ - private static final boolean WINDOWS = File.separatorChar == '\\'; + /** Match custom URLs that are followed by one or two slashes. */ + private static final Pattern schemeOneOrTwoSlashMatcher = Pattern.compile("^[a-zA-Z+\\-.]+:/{1,2}"); /** * Constructor. @@ -194,64 +195,73 @@ public static String resolve(final String resolveBasePath, final String relative boolean isAbsolutePath = false; boolean isFileOrJarURL = false; int startIdx = 0; - if (relativePath.regionMatches(true, startIdx, "jar:", 0, 4)) { - // "jar:" prefix can be stripped - startIdx += 4; - isFileOrJarURL = true; - } - if (relativePath.regionMatches(true, startIdx, "http://", 0, 7)) { - // Detect http:// - startIdx += 7; - // Force protocol name to lowercase - prefix = "http://"; - // Treat the part after the protocol as an absolute path, so the domain is not treated as a directory - // relative to the current directory. - isAbsolutePath = true; - // Don't un-escape percent encoding etc. - } else if (relativePath.regionMatches(true, startIdx, "https://", 0, 8)) { - // Detect https:// - startIdx += 8; - prefix = "https://"; - isAbsolutePath = true; - } else if (relativePath.regionMatches(true, startIdx, "jrt:", 0, 5)) { - // Detect jrt: - startIdx += 4; - prefix = "jrt:"; - isAbsolutePath = true; - } else if (relativePath.regionMatches(true, startIdx, "file:", 0, 5)) { - // Strip off any "file:" prefix from relative path - startIdx += 5; - if (WINDOWS) { - if (relativePath.startsWith("\\\\\\\\", startIdx) || relativePath.startsWith("////", startIdx)) { - // Windows UNC URL - startIdx += 4; - prefix = "//"; + boolean matchedPrefix; + do { + matchedPrefix = false; + if (relativePath.regionMatches(true, startIdx, "jar:", 0, 4)) { + // "jar:" prefix can be stripped + matchedPrefix = true; + startIdx = 4; + isFileOrJarURL = true; + } else if (relativePath.regionMatches(true, startIdx, "http://", 0, 7)) { + // Detect http:// + matchedPrefix = true; + startIdx += 7; + // Force protocol name to lowercase + prefix += "http://"; + // Treat the part after the protocol as an absolute path, so the domain is not treated as a directory + // relative to the current directory. + isAbsolutePath = true; + // Don't un-escape percent encoding etc. + } else if (relativePath.regionMatches(true, startIdx, "https://", 0, 8)) { + // Detect https:// + matchedPrefix = true; + startIdx += 8; + prefix += "https://"; + isAbsolutePath = true; + } else if (relativePath.regionMatches(true, startIdx, "jrt:", 0, 5)) { + // Detect jrt: + matchedPrefix = true; + startIdx += 4; + prefix += "jrt:"; + isAbsolutePath = true; + } else if (relativePath.regionMatches(true, startIdx, "file:", 0, 5)) { + // Strip off "file:" prefix from relative path + matchedPrefix = true; + startIdx += 5; + isFileOrJarURL = true; + } else { + // Preserve the number of slashes on custom URL schemes (#420) + final String relPath = startIdx == 0 ? relativePath : relativePath.substring(startIdx); + final Matcher matcher = schemeOneOrTwoSlashMatcher.matcher(relPath); + if (matcher.find()) { + matchedPrefix = true; + final String match = matcher.group(); + startIdx += match.length(); + prefix += match; + // Treat the part after the protocol as an absolute path, so the rest of the URL is not treated + // as a directory relative to the current directory. isAbsolutePath = true; - } else { - if (relativePath.startsWith("\\\\", startIdx)) { - startIdx += 2; - } } } - if (relativePath.startsWith("//", startIdx)) { - startIdx += 2; - } - isFileOrJarURL = true; - } else if (WINDOWS && (relativePath.startsWith("//") || relativePath.startsWith("\\\\"))) { - // Windows UNC path - startIdx += 2; - prefix = "//"; - isAbsolutePath = true; - } + } while (matchedPrefix); + // Handle Windows paths starting with a drive designation as an absolute path - if (WINDOWS) { - if (relativePath.length() - startIdx > 2 && Character.isLetter(relativePath.charAt(startIdx)) + if (VersionFinder.OS == OperatingSystem.Windows) { + if (relativePath.startsWith("//", startIdx) || relativePath.startsWith("\\\\", startIdx)) { + // Windows UNC path + startIdx += 2; + prefix += "//"; + isAbsolutePath = true; + } else if (relativePath.length() - startIdx > 2 && Character.isLetter(relativePath.charAt(startIdx)) && relativePath.charAt(startIdx + 1) == ':') { + // Path like "C:/xyz" isAbsolutePath = true; } else if (relativePath.length() - startIdx > 3 && (relativePath.charAt(startIdx) == '/' || relativePath.charAt(startIdx) == '\\') && Character.isLetter(relativePath.charAt(startIdx + 1)) && relativePath.charAt(startIdx + 2) == ':') { + // Path like "/C:/xyz" isAbsolutePath = true; startIdx++; } @@ -281,19 +291,18 @@ public static String resolve(final String resolveBasePath, final String relative } } - final String resolveBasePathSanitized = isAbsolutePath || resolveBasePath == null - || resolveBasePath.isEmpty() ? null - : FileUtils.sanitizeEntryPath(resolveBasePath, /* removeInitialSlash = */ false); - final String pathStrSanitized = FileUtils.sanitizeEntryPath(pathStr, /* removeInitialSlash = */ false); + // Sanitize path (resolve ".." sections, collapse "//" double separators, etc.) String pathResolved; - if (resolveBasePathSanitized == null || resolveBasePathSanitized.isEmpty()) { + if (isAbsolutePath || resolveBasePath == null || resolveBasePath.isEmpty()) { // There is no base path to resolve against, or path is an absolute path or http(s):// URL // (ignore the base path) - pathResolved = pathStrSanitized; + pathResolved = FileUtils.sanitizeEntryPath(pathStr, /* removeInitialSlash = */ false, + /* removeFinalSlash = */ true); } else { // Path is a relative path -- resolve it relative to the base path - pathResolved = resolveBasePath - + (resolveBasePath.endsWith("/") || pathStrSanitized.isEmpty() ? "" : "/") + pathStrSanitized; + pathResolved = FileUtils.sanitizeEntryPath( + resolveBasePath + (resolveBasePath.endsWith("/") ? "" : "/") + pathStr, + /* removeInitialSlash = */ false, /* removeFinalSlash = */ true); } // Add any prefix back, e.g. "https://" diff --git a/src/main/java/nonapi/io/github/classgraph/utils/FileUtils.java b/src/main/java/nonapi/io/github/classgraph/utils/FileUtils.java index adbdbf755..395ac3ca7 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/FileUtils.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/FileUtils.java @@ -29,32 +29,46 @@ package nonapi.io.github.classgraph.utils; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.LinkOption; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.AbstractMap.SimpleEntry; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; -import io.github.classgraph.ClassGraphException; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.utils.VersionFinder.OperatingSystem; /** * File utilities. */ public final class FileUtils { + /** The DirectByteBuffer.cleaner() method. */ + private static Method directByteBufferCleanerMethod; - /** The clean() method. */ - private static Method cleanMethod; + /** The Cleaner.clean() method. */ + private static Method cleanerCleanMethod; + + // /** The jdk.incubator.foreign.MemorySegment class (JDK14+). */ + // private static Class memorySegmentClass; + // + // /** The jdk.incubator.foreign.MemorySegment.ofByteBuffer method (JDK14+). */ + // private static Method memorySegmentOfByteBufferMethod; + // + // /** The jdk.incubator.foreign.MemorySegment.ofByteBuffer method (JDK14+). */ + // private static Method memorySegmentCloseMethod; /** The attachment() method. */ private static Method attachmentMethod; @@ -62,23 +76,14 @@ public final class FileUtils { /** The Unsafe object. */ private static Object theUnsafe; - /** - * The minimum filesize at which it becomes more efficient to read a file with a memory-mapped file channel - * rather than an InputStream. Based on benchmark testing using the following benchmark, averaged over three - * separate runs, then plotted as a speedup curve for 1, 2, 4 and 8 concurrent threads: - * - * https://github.com/lukehutch/FileReadingBenchmark - */ - public static final int FILECHANNEL_FILE_SIZE_THRESHOLD; + /** True if class' static fields have been initialized. */ + private static AtomicBoolean initialized = new AtomicBoolean(); /** * The current directory path (only reads the current directory once, the first time this field is accessed, so * will not reflect subsequent changes to the current directory). */ - public static final String CURR_DIR_PATH; - - /** The default size of a file buffer. */ - private static final int DEFAULT_BUFFER_SIZE = 16384; + private static String currDirPath; /** * The maximum size of a file buffer array. Eight bytes smaller than {@link Integer#MAX_VALUE}, since some VMs @@ -86,9 +91,6 @@ public final class FileUtils { */ public static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8; - /** The maximum initial buffer size. */ - private static final int MAX_INITIAL_BUFFER_SIZE = 16 * 1024 * 1024; - // ------------------------------------------------------------------------------------------------------------- /** @@ -98,188 +100,42 @@ private FileUtils() { // Cannot be constructed } - static { - String currDirPathStr = ""; - try { - // The result is moved to currDirPathStr after each step, so we can provide fine-grained debug info and - // a best guess at the path, if the current dir doesn't exist (#109), or something goes wrong while - // trying to get the current dir path. - Path currDirPath = Paths.get("").toAbsolutePath(); - currDirPathStr = currDirPath.toString(); - currDirPath = currDirPath.normalize(); - currDirPathStr = currDirPath.toString(); - currDirPath = currDirPath.toRealPath(LinkOption.NOFOLLOW_LINKS); - currDirPathStr = currDirPath.toString(); - currDirPathStr = FastPathResolver.resolve(currDirPathStr); - } catch (final IOException e) { - throw ClassGraphException - .newClassGraphException("Could not resolve current directory: " + currDirPathStr, e); - } - CURR_DIR_PATH = currDirPathStr; - } - - // ------------------------------------------------------------------------------------------------------------- - - static { - switch (VersionFinder.OS) { - case Linux: - // On Linux, FileChannel is more efficient once file sizes are larger than 16kb, - // and the speedup increases superlinearly, reaching 1.5-3x for a filesize of 1MB - // (and the performance increase does not level off at 1MB either -- that is as - // far as this was benchmarked). - case MacOSX: - // On older/slower Mac OS X machines, FileChannel is always 10-20% slower than InputStream, - // except for very large files (>1MB), and only for single-threaded reading. - // But on newer/faster Mac OS X machines, you get a 10-20% speedup between 16kB and 128kB, - // then a much larger speedup for files larger than 128kb (topping out at about 2.5x speedup). - // It's probably worth setting the threshold to 16kB to get the 10-20% speedup for files - // larger than 16kB in size on modern machines. - case Solaris: - case BSD: - case Unix: - // No testing has been performed yet on the other unices, so just pick the same val as MacOSX and Linux - FILECHANNEL_FILE_SIZE_THRESHOLD = 16384; - break; - - case Windows: - // Windows is always 10-20% faster with FileChannel than with InputStream, even for small files. - FILECHANNEL_FILE_SIZE_THRESHOLD = -1; - break; - - case Unknown: - // For any other operating system - default: - FILECHANNEL_FILE_SIZE_THRESHOLD = 16384; - break; - } - } - // ------------------------------------------------------------------------------------------------------------- /** - * Read all the bytes in an {@link InputStream}. + * Get the current directory (only looks at the current directory the first time it is called, then caches this + * value for future reads). * - * @param inputStream - * The {@link InputStream}. - * @param fileSizeHint - * The file size, if known, otherwise -1L. - * @return The contents of the {@link InputStream} as an Entry consisting of the byte array and number of bytes - * used in the array. - * @throws IOException - * If the contents could not be read. + * @return The current directory, as a string */ - private static SimpleEntry readAllBytes(final InputStream inputStream, final long fileSizeHint) - throws IOException { - if (fileSizeHint > MAX_BUFFER_SIZE) { - throw new IOException("InputStream is too large to read"); - } - final int bufferSize = fileSizeHint < 1L - // If fileSizeHint is unknown, use default buffer size - ? DEFAULT_BUFFER_SIZE - // fileSizeHint is just a hint -- limit the max allocated buffer size, so that invalid ZipEntry - // lengths do not become a memory allocation attack vector - : Math.min((int) fileSizeHint, MAX_INITIAL_BUFFER_SIZE); - byte[] buf = new byte[bufferSize]; - - int bufLength = buf.length; - int totBytesRead = 0; - for (int bytesRead;;) { - // Fill buffer -- may fill more or fewer bytes than buffer size - while ((bytesRead = inputStream.read(buf, totBytesRead, bufLength - totBytesRead)) > 0) { - totBytesRead += bytesRead; - } - if (bytesRead < 0) { - // Reached end of stream - break; - } - // bytesRead == 0 => grow buffer, avoiding overflow - if (bufLength <= MAX_BUFFER_SIZE - bufLength) { - bufLength = bufLength << 1; - } else { - if (bufLength == MAX_BUFFER_SIZE) { - throw new IOException("InputStream too large to read"); + public static String currDirPath() { + if (currDirPath == null) { + // user.dir should be the current directory at the time the JVM is started, which is + // where classpath elements should be resolved relative to + Path path = null; + final String currDirPathStr = System.getProperty("user.dir"); + if (currDirPathStr != null) { + try { + path = Paths.get(currDirPathStr); + } catch (final InvalidPathException e) { + // Fall through } - bufLength = MAX_BUFFER_SIZE; } - buf = Arrays.copyOf(buf, bufLength); - } - // Return buffer and number of bytes read - return new SimpleEntry<>((bufLength == totBytesRead) ? buf : Arrays.copyOf(buf, totBytesRead), - totBytesRead); - } - - /** - * Read all the bytes in an {@link InputStream} as a byte array. - * - * @param inputStream - * The {@link InputStream}. - * @param fileSize - * The file size, if known, otherwise -1L. - * @return The contents of the {@link InputStream} as a byte array. - * @throws IOException - * If the contents could not be read. - */ - public static byte[] readAllBytesAsArray(final InputStream inputStream, final long fileSize) - throws IOException { - final SimpleEntry ent = readAllBytes(inputStream, fileSize); - final byte[] buf = ent.getKey(); - final int bufBytesUsed = ent.getValue(); - return (buf.length == bufBytesUsed) ? buf : Arrays.copyOf(buf, bufBytesUsed); - } - - /** - * Read all the bytes in an {@link InputStream} as a String. - * - * @param inputStream - * The {@link InputStream}. - * @param fileSize - * The file size, if known, otherwise -1L. - * @return The contents of the {@link InputStream} as a String. - * @throws IOException - * If the contents could not be read. - */ - public static String readAllBytesAsString(final InputStream inputStream, final long fileSize) - throws IOException { - final SimpleEntry ent = readAllBytes(inputStream, fileSize); - final byte[] buf = ent.getKey(); - final int bufBytesUsed = ent.getValue(); - return new String(buf, 0, bufBytesUsed, StandardCharsets.UTF_8); - } - - // ------------------------------------------------------------------------------------------------------------- - - /** - * Produce an {@link InputStream} that is able to read from a {@link ByteBuffer}. - * - * @param byteBuffer - * The {@link ByteBuffer}. - * @return An {@link InputStream} that reads from the {@link ByteBuffer}. - */ - public static InputStream byteBufferToInputStream(final ByteBuffer byteBuffer) { - // https://stackoverflow.com/questions/4332264/wrapping-a-bytebuffer-with-an-inputstream/6603018#6603018 - return new InputStream() { - /** The intermediate buffer. */ - final ByteBuffer buf = byteBuffer; - - @Override - public int read() { - if (!buf.hasRemaining()) { - return -1; + if (path == null) { + // user.dir should probably always be set. But just in case it is not, try reading the + // actual current directory at the time ClassGraph is first invoked. + try { + path = Paths.get(""); + } catch (final InvalidPathException e) { + // Fall through } - return buf.get() & 0xFF; } - @Override - public int read(final byte[] bytes, final int off, final int len) { - if (!buf.hasRemaining()) { - return -1; - } - - final int bytesRead = Math.min(len, buf.remaining()); - buf.get(bytes, off, bytesRead); - return bytesRead; - } - }; + // Normalize current directory the same way all other paths are normalized in ClassGraph, + // for consistency + currDirPath = FastPathResolver.resolve(path == null ? "" : path.toString()); + } + return currDirPath; } // ------------------------------------------------------------------------------------------------------------- @@ -292,30 +148,36 @@ public int read(final byte[] bytes, final int off, final int len) { * @param path * The path to sanitize. * @param removeInitialSlash - * If true, additionally removes any "/" character(s) from the beginning of the returned path. + * If true, remove any '/' character(s) from the beginning of the returned path. + * @param removeFinalSlash + * If true, remove any '/' character(s) from the end of the returned path. * @return The sanitized path. */ - public static String sanitizeEntryPath(final String path, final boolean removeInitialSlash) { + public static String sanitizeEntryPath(final String path, final boolean removeInitialSlash, + final boolean removeFinalSlash) { if (path.isEmpty()) { return ""; } // Find all '/' and '!' character positions, which split a path into segments boolean foundSegmentToSanitize = false; + final int pathLen = path.length(); + final char[] pathChars = new char[pathLen]; + path.getChars(0, pathLen, pathChars, 0); { int lastSepIdx = -1; char prevC = '\0'; - for (int i = 0; i < path.length() + 1; i++) { - final char c = i == path.length() ? '\0' : path.charAt(i); + for (int i = 0, ii = pathLen + 1; i < ii; i++) { + final char c = i == pathLen ? '\0' : pathChars[i]; if (c == '/' || c == '!' || c == '\0') { final int segmentLength = i - (lastSepIdx + 1); if ( // Found empty segment "//" or "!!" (segmentLength == 0 && prevC == c) // Found segment "." - || (segmentLength == 1 && path.charAt(i - 1) == '.') + || (segmentLength == 1 && pathChars[i - 1] == '.') // Found segment ".." - || (segmentLength == 2 && path.charAt(i - 2) == '.' && path.charAt(i - 1) == '.')) { + || (segmentLength == 2 && pathChars[i - 2] == '.' && pathChars[i - 1] == '.')) { foundSegmentToSanitize = true; } lastSepIdx = i; @@ -325,27 +187,31 @@ public static String sanitizeEntryPath(final String path, final boolean removeIn } // Handle "..", "." and empty path segments, if any were found - String pathSanitized = path; + final boolean pathHasInitialSlash = pathChars[0] == '/'; + final boolean pathHasInitialSlashSlash = pathHasInitialSlash && pathLen > 1 && pathChars[1] == '/'; + final StringBuilder pathSanitized = new StringBuilder(pathLen + 16); if (foundSegmentToSanitize) { // Sanitize between "!" section markers separately (".." should not apply past preceding "!") - final List> allSectionSegments = new ArrayList<>(); - List currSectionSegments = new ArrayList<>(); + final List> allSectionSegments = new ArrayList<>(); + List currSectionSegments = new ArrayList<>(); allSectionSegments.add(currSectionSegments); int lastSepIdx = -1; - for (int i = 0; i < path.length() + 1; i++) { - final char c = i == path.length() ? '\0' : path.charAt(i); + for (int i = 0; i < pathLen + 1; i++) { + final char c = i == pathLen ? '\0' : pathChars[i]; if (c == '/' || c == '!' || c == '\0') { - final String segment = path.substring(lastSepIdx + 1, i); - if (segment.equals(".") || segment.isEmpty()) { - // Ignore "/./" or empty segment "//" - } else if (segment.equals("..")) { + final int segmentStartIdx = lastSepIdx + 1; + final int segmentLen = i - segmentStartIdx; + if (segmentLen == 0 || (segmentLen == 1 && pathChars[segmentStartIdx] == '.')) { + // Ignore empty segment "//" or idempotent segment "/./" + } else if (segmentLen == 2 && pathChars[segmentStartIdx] == '.' + && pathChars[segmentStartIdx + 1] == '.') { // Remove one segment if ".." encountered, but do not allow ".." above top of hierarchy if (!currSectionSegments.isEmpty()) { currSectionSegments.remove(currSectionSegments.size() - 1); } } else { // Encountered normal path segment - currSectionSegments.add(segment); + currSectionSegments.add(path.subSequence(segmentStartIdx, segmentStartIdx + segmentLen)); } if (c == '!' && !currSectionSegments.isEmpty()) { // Begin new section @@ -356,34 +222,47 @@ public static String sanitizeEntryPath(final String path, final boolean removeIn } } // Turn sections and segments back into path string - final StringBuilder buf = new StringBuilder(); - for (final List sectionSegments : allSectionSegments) { + for (final List sectionSegments : allSectionSegments) { if (!sectionSegments.isEmpty()) { // Delineate segments with "!" - if (buf.length() > 0) { - buf.append('!'); + if (pathSanitized.length() > 0) { + pathSanitized.append('!'); } - for (final String sectionSegment : sectionSegments) { - buf.append('/'); - buf.append(sectionSegment); + for (final CharSequence sectionSegment : sectionSegments) { + pathSanitized.append('/'); + pathSanitized.append(sectionSegment); } } } - pathSanitized = buf.toString(); - if (pathSanitized.isEmpty() && path.startsWith("/")) { - pathSanitized = "/"; + if (pathSanitized.length() == 0 && pathHasInitialSlash) { + pathSanitized.append('/'); } + } else { + pathSanitized.append(path); } - if (removeInitialSlash || !path.startsWith("/")) { + // Intended to preserve the double slash at the start of UNC paths (#736). + // e.g. //server/file/path + if (VersionFinder.OS == OperatingSystem.Windows && pathHasInitialSlashSlash) { + pathSanitized.insert(0, '/'); + } + + int startIdx = 0; + if (removeInitialSlash || !pathHasInitialSlash) { // Strip off leading "/" if it needs to be removed, or if it wasn't present in the original path // (the string-building code above prepends "/" to every segment). Note that "/" is always added // after "!", since "jar:" URLs expect this. - while (pathSanitized.startsWith("/")) { - pathSanitized = pathSanitized.substring(1); + while (startIdx < pathSanitized.length() && pathSanitized.charAt(startIdx) == '/') { + startIdx++; + } + } + if (removeFinalSlash) { + while (pathSanitized.length() > 0 && pathSanitized.charAt(pathSanitized.length() - 1) == '/') { + pathSanitized.setLength(pathSanitized.length() - 1); } } - return pathSanitized; + + return pathSanitized.substring(startIdx); } // ------------------------------------------------------------------------------------------------------------- @@ -403,7 +282,7 @@ public static boolean isClassfile(final String path) { // ------------------------------------------------------------------------------------------------------------- /** - * Check if the file exists and can be read. + * Check if a {@link File} exists and can be read. * * @param file * A {@link File}. @@ -417,6 +296,223 @@ public static boolean canRead(final File file) { } } + /** + * Check if a {@link Path} exists and can be read. + * + * @param path + * A {@link Path}. + * @return true if the file exists and can be read. + */ + public static boolean canRead(final Path path) { + try { + return canRead(path.toFile()); + } catch (final UnsupportedOperationException ignored) { + } + try { + return Files.isReadable(path); + } catch (final SecurityException e) { + return false; + } + } + + /** + * Check if a {@link File} exists, is a regular file, and can be read. + * + * @param file + * A {@link File}. + * @return true if the file exists, is a regular file, and can be read. + */ + public static boolean canReadAndIsFile(final File file) { + try { + if (!file.canRead()) { + return false; + } + } catch (final SecurityException e) { + return false; + } + return file.isFile(); + } + + /** + * Check if a {@link Path} exists, is a regular file, and can be read. + * + * @param path + * A {@link Path}. + * @return true if the file exists, is a regular file, and can be read. + */ + public static boolean canReadAndIsFile(final Path path) { + try { + return canReadAndIsFile(path.toFile()); + } catch (final UnsupportedOperationException ignored) { + } + try { + if (!Files.isReadable(path)) { + return false; + } + } catch (final SecurityException e) { + return false; + } + return Files.isRegularFile(path); + } + + public static boolean isFile(final Path path) { + try { + return path.toFile().isFile(); + } catch (final UnsupportedOperationException e) { + return Files.isRegularFile(path); + } catch (final SecurityException e) { + return false; + } + } + + /** + * Check if a {@link File} exists, is a regular file, and can be read. + * + * @param file + * A {@link File}. + * @throws IOException + * if the file does not exist, is not a regular file, or cannot be read. + */ + public static void checkCanReadAndIsFile(final File file) throws IOException { + try { + if (!file.canRead()) { + throw new FileNotFoundException("File does not exist or cannot be read: " + file); + } + } catch (final SecurityException e) { + throw new FileNotFoundException("File " + file + " cannot be accessed: " + e); + } + if (!file.isFile()) { + throw new IOException("Not a regular file: " + file); + } + } + + /** + * Check if a {@link Path} exists, is a regular file, and can be read. + * + * @param path + * A {@link Path}. + * @throws IOException + * if the path does not exist, is not a regular file, or cannot be read. + */ + public static void checkCanReadAndIsFile(final Path path) throws IOException { + try { + checkCanReadAndIsFile(path.toFile()); + return; + } catch (final UnsupportedOperationException ignored) { + } + try { + if (!Files.isReadable(path)) { + throw new FileNotFoundException("Path does not exist or cannot be read: " + path); + } + } catch (final SecurityException e) { + throw new FileNotFoundException("Path " + path + " cannot be accessed: " + e); + } + if (!Files.isRegularFile(path)) { + throw new IOException("Not a regular file: " + path); + } + } + + /** + * Check if a {@link File} exists, is a directory, and can be read. + * + * @param file + * A {@link File}. + * @return true if the file exists, is a directory, and can be read. + */ + public static boolean canReadAndIsDir(final File file) { + try { + if (!file.canRead()) { + return false; + } + } catch (final SecurityException e) { + return false; + } + return file.isDirectory(); + } + + /** + * Check if a {@link Path} exists, is a directory, and can be read. + * + * @param path + * A {@link Path}. + * @return true if the file exists, is a directory, and can be read. + */ + public static boolean canReadAndIsDir(final Path path) { + try { + return canReadAndIsDir(path.toFile()); + } catch (final UnsupportedOperationException ignored) { + } + try { + if (!Files.isReadable(path)) { + return false; + } + } catch (final SecurityException e) { + return false; + } + return Files.isDirectory(path); + } + + public static boolean isDir(final Path path) { + try { + return path.toFile().isDirectory(); + } catch (final UnsupportedOperationException e) { + return Files.isDirectory(path); + } catch (final SecurityException e) { + return false; + } + } + + /** + * Check if a {@link File} exists, is a directory, and can be read. + * + * @param file + * A {@link File}. + * @throws IOException + * if the file does not exist, is not a directory, or cannot be read. + */ + public static void checkCanReadAndIsDir(final File file) throws IOException { + try { + if (!file.canRead()) { + throw new FileNotFoundException("Directory does not exist or cannot be read: " + file); + } + } catch (final SecurityException e) { + throw new FileNotFoundException("File " + file + " cannot be accessed: " + e); + } + if (!file.isDirectory()) { + throw new IOException("Not a directory: " + file); + } + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get the parent dir path. + * + * @param path + * the path + * @param separator + * the separator + * @return the parent dir path + */ + public static String getParentDirPath(final String path, final char separator) { + final int lastSlashIdx = path.lastIndexOf(separator); + if (lastSlashIdx <= 0) { + return ""; + } + return path.substring(0, lastSlashIdx); + } + + /** + * Get the parent dir path. + * + * @param path + * the path + * @return the parent dir path + */ + public static String getParentDirPath(final String path) { + return getParentDirPath(path, '/'); + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -425,55 +521,52 @@ public static boolean canRead(final File file) { private static void lookupCleanMethodPrivileged() { if (VersionFinder.JAVA_MAJOR_VERSION < 9) { try { - // See: https://stackoverflow.com/a/19447758/3950982 - cleanMethod = Class.forName("sun.misc.Cleaner").getMethod("clean"); - cleanMethod.setAccessible(true); + // See: + // https://stackoverflow.com/a/19447758/3950982 + cleanerCleanMethod = Class.forName("sun.misc.Cleaner").getDeclaredMethod("clean"); + cleanerCleanMethod.setAccessible(true); final Class directByteBufferClass = Class.forName("sun.nio.ch.DirectBuffer"); + directByteBufferCleanerMethod = directByteBufferClass.getDeclaredMethod("cleaner"); attachmentMethod = directByteBufferClass.getMethod("attachment"); attachmentMethod.setAccessible(true); } catch (final SecurityException e) { - throw ClassGraphException.newClassGraphException( + throw new RuntimeException( "You need to grant classgraph RuntimePermission(\"accessClassInPackage.sun.misc\") " - + "and ReflectPermission(\"suppressAccessChecks\")"); - } catch (final ReflectiveOperationException | LinkageError ex) { + + "and ReflectPermission(\"suppressAccessChecks\")", + e); + } catch (final ReflectiveOperationException | LinkageError e) { // Ignore } - } else { + } else if (VersionFinder.JAVA_MAJOR_VERSION < 24) { + // JDK 24+ reports: "A terminally deprecated method in sun.misc.Unsafe has been called" + // if Unsafe::invokeCleaner is used, and we don't actually need the cleaner method unless + // direct memory mapping is used rather than FileChannel (ClassGraph#enableMemoryMapping + // disables this now for JDK 24+). + // See: https://github.com/classgraph/classgraph/issues/899 try { Class unsafeClass; try { unsafeClass = Class.forName("sun.misc.Unsafe"); } catch (final ReflectiveOperationException | LinkageError e) { - // jdk.internal.misc.Unsafe doesn't yet have an invokeCleaner() method, - // but that method should be added if sun.misc.Unsafe is removed. - unsafeClass = Class.forName("jdk.internal.misc.Unsafe"); + throw new RuntimeException("Could not get class sun.misc.Unsafe", e); } - cleanMethod = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class); - cleanMethod.setAccessible(true); final Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe"); theUnsafeField.setAccessible(true); theUnsafe = theUnsafeField.get(null); + cleanerCleanMethod = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class); + cleanerCleanMethod.setAccessible(true); } catch (final SecurityException e) { - throw ClassGraphException.newClassGraphException( - "You need to grant classgraph RuntimePermission(\"accessClassInPackage.sun.misc\"), " - + "RuntimePermission(\"accessClassInPackage.jdk.internal.misc\") " - + "and ReflectPermission(\"suppressAccessChecks\")"); + throw new RuntimeException( + "You need to grant classgraph RuntimePermission(\"accessClassInPackage.sun.misc\") " + + "and ReflectPermission(\"suppressAccessChecks\")", + e); } catch (final ReflectiveOperationException | LinkageError ex) { // Ignore } + //} } } - static { - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Object run() { - lookupCleanMethodPrivileged(); - return null; - } - }); - } - /** * Close a direct byte buffer (run in doPrivileged). * @@ -484,13 +577,11 @@ public Object run() { * @return true if successful */ private static boolean closeDirectByteBufferPrivileged(final ByteBuffer byteBuffer, final LogNode log) { + if (!byteBuffer.isDirect()) { + // Nothing to do + return true; + } try { - if (cleanMethod == null) { - if (log != null) { - log.log("Could not unmap ByteBuffer, cleanMethod == null"); - } - return false; - } if (VersionFinder.JAVA_MAJOR_VERSION < 9) { if (attachmentMethod == null) { if (log != null) { @@ -507,27 +598,76 @@ private static boolean closeDirectByteBufferPrivileged(final ByteBuffer byteBuff return false; } // Invoke ((DirectBuffer) byteBuffer).cleaner().clean() - final Method cleaner = byteBuffer.getClass().getMethod("cleaner"); - cleaner.setAccessible(true); - cleanMethod.invoke(cleaner.invoke(byteBuffer)); - return true; - } else { + if (directByteBufferCleanerMethod == null) { + if (log != null) { + log.log("Could not unmap ByteBuffer, cleanerMethod == null"); + } + return false; + } + try { + directByteBufferCleanerMethod.setAccessible(true); + } catch (final Exception e) { + if (log != null) { + log.log("Could not unmap ByteBuffer, cleanerMethod.setAccessible(true) failed"); + } + return false; + } + final Object cleanerInstance = directByteBufferCleanerMethod.invoke(byteBuffer); + if (cleanerInstance == null) { + if (log != null) { + log.log("Could not unmap ByteBuffer, cleaner == null"); + } + return false; + } + if (cleanerCleanMethod == null) { + if (log != null) { + log.log("Could not unmap ByteBuffer, cleanMethod == null"); + } + return false; + } + try { + cleanerCleanMethod.invoke(cleanerInstance); + return true; + } catch (final Exception e) { + if (log != null) { + log.log("Could not unmap ByteBuffer, cleanMethod.invoke(cleaner) failed: " + e); + } + return false; + } + // } else if (memorySegmentOfByteBufferMethod != null) { + // // JDK 14+ + // final Object memorySegment = memorySegmentOfByteBufferMethod.invoke(null, byteBuffer); + // if (memorySegment == null) { + // if (log != null) { + // log.log("Got null MemorySegment, could not unmap ByteBuffer"); + // } + // return false; + // } + // memorySegmentCloseMethod.invoke(memorySegment); + // return true; + } else if (VersionFinder.JAVA_MAJOR_VERSION < 24) { if (theUnsafe == null) { if (log != null) { log.log("Could not unmap ByteBuffer, theUnsafe == null"); } return false; } - // In JDK9+, calling the above code gives a reflection warning on stderr, - // need to call Unsafe.theUnsafe.invokeCleaner(byteBuffer) , which makes - // the same call, but does not print the reflection warning. + if (cleanerCleanMethod == null) { + if (log != null) { + log.log("Could not unmap ByteBuffer, cleanMethod == null"); + } + return false; + } try { - cleanMethod.invoke(theUnsafe, byteBuffer); + cleanerCleanMethod.invoke(theUnsafe, byteBuffer); return true; } catch (final IllegalArgumentException e) { // Buffer is a duplicate or slice return false; } + } else { + // TODO: on JDK 24+, use Arena -- see FileSlice + return false; } } catch (final ReflectiveOperationException | SecurityException e) { if (log != null) { @@ -546,17 +686,108 @@ private static boolean closeDirectByteBufferPrivileged(final ByteBuffer byteBuff * The log. * @return True if the byteBuffer was closed/unmapped. */ - public static boolean closeDirectByteBuffer(final ByteBuffer byteBuffer, final LogNode log) { + public static boolean closeDirectByteBuffer(final ByteBuffer byteBuffer, final ReflectionUtils reflectionUtils, + final LogNode log) { if (byteBuffer != null && byteBuffer.isDirect()) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Boolean run() { - return closeDirectByteBufferPrivileged(byteBuffer, log); + if (!initialized.get()) { + try { + reflectionUtils.doPrivileged(new Callable() { + @Override + public Void call() throws Exception { + lookupCleanMethodPrivileged(); + return null; + } + }); + } catch (final Throwable e) { + throw new RuntimeException("Cannot get buffer cleaner method", e); } - }); + initialized.set(true); + } + try { + return reflectionUtils.doPrivileged(new Callable() { + @Override + public Boolean call() throws Exception { + return closeDirectByteBufferPrivileged(byteBuffer, log); + } + }); + } catch (final Throwable t) { + return false; + } } else { // Nothing to unmap return false; } } + + public static FileAttributesGetter createCachedAttributesGetter() { + final Map cache = new HashMap<>(); + return new FileAttributesGetter() { + @Override + public BasicFileAttributes get(final Path path) { + BasicFileAttributes attributes = cache.get(path); + if (attributes == null) { + attributes = readAttributes(path); + cache.put(path, attributes); + } + return attributes; + } + }; + } + + public static BasicFileAttributes readAttributes(final Path path) { + try { + return Files.readAttributes(path, BasicFileAttributes.class); + } catch (final IOException e) { + return new BasicFileAttributes() { + @Override + public FileTime lastModifiedTime() { + return FileTime.fromMillis(path.toFile().lastModified()); + } + + @Override + public FileTime lastAccessTime() { + throw new UnsupportedOperationException(); + } + + @Override + public FileTime creationTime() { + return FileTime.fromMillis(0); + } + + @Override + public boolean isRegularFile() { + return FileUtils.isFile(path); + } + + @Override + public boolean isDirectory() { + return FileUtils.isDir(path); + } + + @Override + public boolean isSymbolicLink() { + return false; + } + + @Override + public boolean isOther() { + return !isRegularFile() && !isDirectory(); + } + + @Override + public long size() { + return path.toFile().length(); + } + + @Override + public Object fileKey() { + throw new UnsupportedOperationException(); + } + }; + } + } + + public interface FileAttributesGetter { + BasicFileAttributes get(Path path); + } } diff --git a/src/main/java/nonapi/io/github/classgraph/utils/InputStreamOrByteBufferAdapter.java b/src/main/java/nonapi/io/github/classgraph/utils/InputStreamOrByteBufferAdapter.java deleted file mode 100644 index 048affd4b..000000000 --- a/src/main/java/nonapi/io/github/classgraph/utils/InputStreamOrByteBufferAdapter.java +++ /dev/null @@ -1,438 +0,0 @@ -/* - * This file is part of ClassGraph. - * - * Author: Luke Hutchison - * - * Hosted at: https://github.com/classgraph/classgraph - * - * -- - * - * The MIT License (MIT) - * - * Copyright (c) 2019 Luke Hutchison - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT - * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO - * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE - * OR OTHER DEALINGS IN THE SOFTWARE. - */ -package nonapi.io.github.classgraph.utils; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; -import java.util.Arrays; - -/** Buffer class that can wrap either an InputStream or a ByteBuffer, depending on which is available. */ -public class InputStreamOrByteBufferAdapter implements AutoCloseable { - /** - * Buffer size for initial read. We can save some time by reading most of the classfile header in a single read - * at the beginning of the scan. - * - *

- * (If chunk sizes are too small, significant overhead is expended in refilling the buffer. If they are too - * large, significant overhead is expended in decompressing more of the classfile header than is needed. Testing - * on a large classpath indicates that the defaults are reasonably optimal.) - */ - private static final int INITIAL_BUFFER_CHUNK_SIZE = 16384; - - /** Buffer size for classfile reader. */ - private static final int SUBSEQUENT_BUFFER_CHUNK_SIZE = 4096; - - /** The InputStream, if applicable. */ - private InputStream inputStream; - - /** The ByteBuffer, if applicable. */ - private ByteBuffer byteBuffer; - - /** - * Bytes read from the beginning of the classfile, for a memory-backed ByteBuffer. This array is reused across - * calls. - */ - public byte[] buf; - - /** - * - * /** The current position in the buffer. - */ - public int curr; - - /** Bytes used in the buffer. */ - private int used; - - /** - * Create an {@link InputStreamOrByteBufferAdapter} from an {@link InputStream}. - * - * @param inputStream - * the input stream - */ - public InputStreamOrByteBufferAdapter(final InputStream inputStream) { - this.inputStream = inputStream; - this.buf = new byte[INITIAL_BUFFER_CHUNK_SIZE]; - } - - /** - * Create an {@link InputStreamOrByteBufferAdapter} from an {@link InputStream}. - * - * @param byteBuffer - * the byte buffer - */ - public InputStreamOrByteBufferAdapter(final ByteBuffer byteBuffer) { - if (byteBuffer.hasArray()) { - // Just use the array behind the buffer as the input buffer - this.buf = byteBuffer.array(); - } else { - this.byteBuffer = byteBuffer; - this.buf = new byte[INITIAL_BUFFER_CHUNK_SIZE]; - } - } - - /** - * Copy up to len bytes into buf, starting at the given offset. - * - * @param off - * The start index for the copy. - * @param len - * The maximum number of bytes to copy. - * @return The number of bytes actually copied. - * @throws IOException - * If the file content could not be read. - */ - private int read(final int off, final int len) throws IOException { - if (len == 0) { - return 0; - } - if (inputStream != null) { - // Wrapped InputStream - return inputStream.read(buf, off, len); - } else { - // Wrapped ByteBuffer - final int bytesRemainingInBuf = byteBuffer != null ? byteBuffer.remaining() : buf.length - off; - final int bytesRead = Math.max(0, Math.min(len, bytesRemainingInBuf)); - if (bytesRead == 0) { - // Return -1, as per InputStream#read() contract - return -1; - } - if (byteBuffer != null) { - // Copy from the ByteBuffer into the byte array - final int byteBufPositionBefore = byteBuffer.position(); - try { - byteBuffer.get(buf, off, bytesRead); - } catch (final BufferUnderflowException e) { - // Should not happen - throw new IOException("Buffer underflow", e); - } - return byteBuffer.position() - byteBufPositionBefore; - } else { - // Nothing to read, since ByteBuffer is backed with an array - return bytesRead; - } - } - } - - /** - * Read another chunk of from the InputStream or ByteBuffer. - * - * @param bytesRequired - * the number of bytes to read - * @throws IOException - * If an I/O exception occurs. - */ - private void readMore(final int bytesRequired) throws IOException { - if ((long) used + (long) bytesRequired > FileUtils.MAX_BUFFER_SIZE) { - // Since buf is an array, we're limited to reading 2GB per file - throw new IOException("File is larger than 2GB, cannot read it"); - } - // Read INITIAL_BUFFER_CHUNK_SIZE for first chunk, or SUBSEQUENT_BUFFER_CHUNK_SIZE for subsequent chunks, - // but don't try to read past 2GB limit - final int targetReadSize = Math.max(bytesRequired, // - used == 0 ? INITIAL_BUFFER_CHUNK_SIZE : SUBSEQUENT_BUFFER_CHUNK_SIZE); - // Calculate number of bytes to read, based on the target read size, handling integer overflow - final int maxNewUsed = (int) Math.min((long) used + (long) targetReadSize, FileUtils.MAX_BUFFER_SIZE); - final int bytesToRead = maxNewUsed - used; - if (maxNewUsed > buf.length) { - // Ran out of space, need to increase the size of the buffer - long newBufLen = buf.length; - while (newBufLen < maxNewUsed) { - newBufLen <<= 1; - } - buf = Arrays.copyOf(buf, (int) Math.min(newBufLen, FileUtils.MAX_BUFFER_SIZE)); - } - int extraBytesStillNotRead = bytesToRead; - int totBytesRead = 0; - while (extraBytesStillNotRead > 0) { - final int bytesRead = read(used, extraBytesStillNotRead); - if (bytesRead > 0) { - used += bytesRead; - totBytesRead += bytesRead; - extraBytesStillNotRead -= bytesRead; - } else { - // EOF - break; - } - } - if (totBytesRead < bytesRequired) { - throw new IOException("Premature EOF while reading classfile"); - } - } - - /** - * Read an unsigned byte from the buffer. - * - * @return The next unsigned byte in the buffer. - * @throws IOException - * If there was an exception while reading. - */ - public int readUnsignedByte() throws IOException { - final int val = readUnsignedByte(curr); - curr++; - return val; - } - - /** - * Read an unsigned byte at a specific offset (without changing the current read point). - * - * @param offset - * The buffer offset to read from. - * @return The unsigned byte at the buffer offset. - * @throws IOException - * If there was an exception while reading. - */ - public int readUnsignedByte(final int offset) throws IOException { - final int bytesToRead = Math.max(0, offset + 1 - used); - if (bytesToRead > 0) { - readMore(bytesToRead); - } - return buf[offset] & 0xff; - } - - /** - * Read the next unsigned short. - * - * @return The next unsigned short in the buffer. - * @throws IOException - * If there was an exception while reading. - */ - public int readUnsignedShort() throws IOException { - final int val = readUnsignedShort(curr); - curr += 2; - return val; - } - - /** - * Read an unsigned short at a specific offset (without changing the current read point). - * - * @param offset - * The buffer offset to read from. - * @return The unsigned short at the buffer offset. - * @throws IOException - * If there was an exception while reading. - */ - public int readUnsignedShort(final int offset) throws IOException { - final int bytesToRead = Math.max(0, offset + 2 - used); - if (bytesToRead > 0) { - readMore(bytesToRead); - } - return ((buf[offset] & 0xff) << 8) | (buf[offset + 1] & 0xff); - } - - /** - * Read the next int. - * - * @return The next int in the buffer. - * @throws IOException - * If there was an exception while reading. - */ - public int readInt() throws IOException { - final int val = readInt(curr); - curr += 4; - return val; - } - - /** - * Read an int at a specific offset (without changing the current read point). - * - * @param offset - * The buffer offset to read from. - * @return The int at the buffer offset. - * @throws IOException - * If there was an exception while reading. - */ - public int readInt(final int offset) throws IOException { - final int bytesToRead = Math.max(0, offset + 4 - used); - if (bytesToRead > 0) { - readMore(bytesToRead); - } - return ((buf[offset] & 0xff) << 24) | ((buf[offset + 1] & 0xff) << 16) | ((buf[offset + 2] & 0xff) << 8) - | (buf[offset + 3] & 0xff); - } - - /** - * Read the next long. - * - * @return The next long in the buffer. - * @throws IOException - * If there was an exception while reading. - */ - public long readLong() throws IOException { - final long val = readLong(curr); - curr += 8; - return val; - } - - /** - * Read a long at a specific offset (without changing the current read point). - * - * @param offset - * The buffer offset to read from. - * @return The long at the buffer offset. - * @throws IOException - * If there was an exception while reading. - */ - public long readLong(final int offset) throws IOException { - final int bytesToRead = Math.max(0, offset + 8 - used); - if (bytesToRead > 0) { - readMore(bytesToRead); - } - return (((long) (((buf[offset] & 0xff) << 24) | ((buf[offset + 1] & 0xff) << 16) - | ((buf[offset + 2] & 0xff) << 8) | (buf[offset + 3] & 0xff))) << 32) - | ((buf[offset + 4] & 0xff) << 24) | ((buf[offset + 5] & 0xff) << 16) - | ((buf[offset + 6] & 0xff) << 8) | (buf[offset + 7] & 0xff); - } - - /** - * Skip the given number of bytes. - * - * @param bytesToSkip - * The number of bytes to skip. - * @throws IOException - * If there was an exception while reading. - */ - public void skip(final int bytesToSkip) throws IOException { - final int bytesToRead = Math.max(0, curr + bytesToSkip - used); - if (bytesToRead > 0) { - readMore(bytesToRead); - } - curr += bytesToSkip; - } - - /** - * Reads the "modified UTF8" format defined in the Java classfile spec, optionally replacing '/' with '.', and - * optionally removing the prefix "L" and the suffix ";". - * - * @param strStart - * The start index of the string. - * @param replaceSlashWithDot - * If true, replace '/' with '.'. - * @param stripLSemicolon - * If true, string final ';' character. - * @return The string. - * @throws IOException - * If an I/O exception occurs. - */ - public String readString(final int strStart, final boolean replaceSlashWithDot, final boolean stripLSemicolon) - throws IOException { - final int utfLen = readUnsignedShort(strStart); - final int utfStart = strStart + 2; - final int bufferUnderrunBytes = Math.max(0, utfStart + utfLen - used); - if (bufferUnderrunBytes > 0) { - readMore(bufferUnderrunBytes); - } - final char[] chars = new char[utfLen]; - int c, c2, c3, c4; - int byteIdx = 0; - int charIdx = 0; - for (; byteIdx < utfLen; byteIdx++) { - c = buf[utfStart + byteIdx] & 0xff; - if (c > 127) { - break; - } - chars[charIdx++] = (char) (replaceSlashWithDot && c == '/' ? '.' : c); - } - while (byteIdx < utfLen) { - c = buf[utfStart + byteIdx] & 0xff; - switch (c >> 4) { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - byteIdx++; - chars[charIdx++] = (char) (replaceSlashWithDot && c == '/' ? '.' : c); - break; - case 12: - case 13: - byteIdx += 2; - if (byteIdx > utfLen) { - throw new IOException("Bad modified UTF8"); - } - c2 = buf[utfStart + byteIdx - 1]; - if ((c2 & 0xc0) != 0x80) { - throw new IOException("Bad modified UTF8"); - } - c4 = ((c & 0x1f) << 6) | (c2 & 0x3f); - chars[charIdx++] = (char) (replaceSlashWithDot && c4 == '/' ? '.' : c4); - break; - case 14: - byteIdx += 3; - if (byteIdx > utfLen) { - throw new IOException("Bad modified UTF8"); - } - c2 = buf[utfStart + byteIdx - 2]; - c3 = buf[utfStart + byteIdx - 1]; - if ((c2 & 0xc0) != 0x80 || (c3 & 0xc0) != 0x80) { - throw new IOException("Bad modified UTF8"); - } - c4 = ((c & 0x0f) << 12) | ((c2 & 0x3f) << 6) | (c3 & 0x3f); - chars[charIdx++] = (char) (replaceSlashWithDot && c4 == '/' ? '.' : c4); - break; - default: - throw new IOException("Bad modified UTF8"); - } - } - if (charIdx == utfLen && !stripLSemicolon) { - return new String(chars); - } else { - if (stripLSemicolon) { - if (charIdx < 2 || chars[0] != 'L' || chars[charIdx - 1] != ';') { - throw new IOException("Expected string to start with 'L' and end with ';', got \"" - + new String(chars) + "\""); - } - return new String(chars, 1, charIdx - 2); - } else { - return new String(chars, 0, charIdx); - } - } - } - - /* (non-Javadoc) - * @see java.lang.AutoCloseable#close() - */ - @Override - public void close() { - if (this.inputStream != null) { - try { - this.inputStream.close(); - } catch (final IOException e) { - // Ignore - } - this.inputStream = null; - } - this.byteBuffer = null; - this.buf = null; - } -} diff --git a/src/main/java/nonapi/io/github/classgraph/utils/JarUtils.java b/src/main/java/nonapi/io/github/classgraph/utils/JarUtils.java index 74e1619e3..10b7dc382 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/JarUtils.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/JarUtils.java @@ -30,20 +30,24 @@ import java.io.File; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import io.github.classgraph.ClassGraphException; import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; +import nonapi.io.github.classgraph.scanspec.ScanSpec; /** * Jarfile utilities. */ public final class JarUtils { + /** + * Check if a path has a URL scheme at the beginning. Require at least 2 chars in a URL scheme, so that Windows + * drive designations don't get treated as URL schemes. + */ + public static final Pattern URL_SCHEME_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9+-.]+[:].*"); /** The Constant DASH_VERSION. */ private static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))"); @@ -60,6 +64,9 @@ public final class JarUtils { /** The Constant TRAILING_DOTS. */ private static final Pattern TRAILING_DOTS = Pattern.compile("\\.$"); + /** The Constant DOUBLE_BACKSHLASH_WITH_COLON. */ + private static final Pattern DOUBLE_BACKSHLASH_WITH_COLON = Pattern.compile("\\\\:"); + /** * On everything but Windows, where the path separator is ':', need to treat the colon in these substrings as * non-separators, when at the beginning of the string or following a ':'. @@ -82,8 +89,7 @@ public final class JarUtils { for (int i = 0; i < UNIX_NON_PATH_SEPARATORS.length; i++) { UNIX_NON_PATH_SEPARATOR_COLON_POSITIONS[i] = UNIX_NON_PATH_SEPARATORS[i].indexOf(':'); if (UNIX_NON_PATH_SEPARATOR_COLON_POSITIONS[i] < 0) { - throw ClassGraphException - .newClassGraphException("Could not find ':' in \"" + UNIX_NON_PATH_SEPARATORS[i] + "\""); + throw new RuntimeException("Could not find ':' in \"" + UNIX_NON_PATH_SEPARATORS[i] + "\""); } } } @@ -98,26 +104,30 @@ private JarUtils() { /** * Split a path on File.pathSeparator (':' on Linux, ';' on Windows), but also allow for the use of URLs with * protocol specifiers, e.g. "http://domain/jar1.jar:http://domain/jar2.jar". - * + * * @param pathStr * The path to split. + * @param scanSpec + * the scan spec * @return The path element substrings. */ - public static String[] smartPathSplit(final String pathStr) { - return smartPathSplit(pathStr, File.pathSeparatorChar); + public static String[] smartPathSplit(final String pathStr, final ScanSpec scanSpec) { + return smartPathSplit(pathStr, File.pathSeparatorChar, scanSpec); } /** * Split a path on the given separator char. If the separator char is ':', also allow for the use of URLs with * protocol specifiers, e.g. "http://domain/jar1.jar:http://domain/jar2.jar". - * + * * @param pathStr * The path to split. * @param separatorChar * The separator char to use. + * @param scanSpec + * the scan spec * @return The path element substrings. */ - public static String[] smartPathSplit(final String pathStr, final char separatorChar) { + public static String[] smartPathSplit(final String pathStr, final char separatorChar, final ScanSpec scanSpec) { if (pathStr == null || pathStr.isEmpty()) { return new String[0]; } @@ -149,6 +159,23 @@ public static String[] smartPathSplit(final String pathStr, final char separator break; } } + if (!foundNonPathSeparator && scanSpec != null && scanSpec.allowedURLSchemes != null + && !scanSpec.allowedURLSchemes.isEmpty()) { + // If custom URL schemes have been registered, allow those to be used as delimiters too + for (final String scheme : scanSpec.allowedURLSchemes) { + // Skip schemes already handled by the faster matching code above + if (!scheme.equals("http") && !scheme.equals("https") && !scheme.equals("jar") + && !scheme.equals("file")) { + final int schemeLen = scheme.length(); + final int startIdx = i - schemeLen; + if (pathStr.regionMatches(true, startIdx, scheme, 0, schemeLen) + && (startIdx == 0 || pathStr.charAt(startIdx - 1) == ':')) { + foundNonPathSeparator = true; + break; + } + } + } + } if (!foundNonPathSeparator) { // The ':' character is a valid path separator splitPoints.add(i); @@ -162,14 +189,14 @@ public static String[] smartPathSplit(final String pathStr, final char separator } } final List splitPointsSorted = new ArrayList<>(splitPoints); - Collections.sort(splitPointsSorted); + CollectionUtils.sortIfNotEmpty(splitPointsSorted); final List parts = new ArrayList<>(); for (int i = 1; i < splitPointsSorted.size(); i++) { final int idx0 = splitPointsSorted.get(i - 1); final int idx1 = splitPointsSorted.get(i); // Trim, and unescape "\\:" String part = pathStr.substring(idx0 + 1, idx1).trim(); - part = part.replaceAll("\\\\:", ":"); + part = DOUBLE_BACKSHLASH_WITH_COLON.matcher(part).replaceAll( ":"); // Remove empty path components if (!part.isEmpty()) { parts.add(part); diff --git a/src/main/java/nonapi/io/github/classgraph/utils/Join.java b/src/main/java/nonapi/io/github/classgraph/utils/Join.java deleted file mode 100644 index 8e3583797..000000000 --- a/src/main/java/nonapi/io/github/classgraph/utils/Join.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This file is part of ClassGraph. - * - * Author: Luke Hutchison - * - * Hosted at: https://github.com/classgraph/classgraph - * - * -- - * - * The MIT License (MIT) - * - * Copyright (c) 2019 Luke Hutchison - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT - * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO - * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE - * OR OTHER DEALINGS IN THE SOFTWARE. - */ -package nonapi.io.github.classgraph.utils; - -/** A replacement for Java 8's String.join() that will allow compilation on Java 7. */ -public final class Join { - - /** - * Constructor. - */ - private Join() { - // Cannot be constructed - } - - /** - * A replacement for Java 8's String.join(). - * - * @param buf - * The buffer to append to. - * @param addAtBeginning - * The token to add at the beginning of the string. - * @param sep - * The separator string. - * @param addAtEnd - * The token to add at the end of the string. - * @param iterable - * The {@link Iterable} to join. - */ - public static void join(final StringBuilder buf, final String addAtBeginning, final String sep, - final String addAtEnd, final Iterable iterable) { - if (!addAtBeginning.isEmpty()) { - buf.append(addAtBeginning); - } - boolean first = true; - for (final Object item : iterable) { - if (first) { - first = false; - } else { - buf.append(sep); - } - buf.append(item.toString()); - } - if (!addAtEnd.isEmpty()) { - buf.append(addAtEnd); - } - } - - /** - * A replacement for Java 8's String.join(). - * - * @param sep - * The separator string. - * @param iterable - * The {@link Iterable} to join. - * @return The string representation of the joined elements. - */ - public static String join(final String sep, final Iterable iterable) { - final StringBuilder buf = new StringBuilder(); - join(buf, "", sep, "", iterable); - return buf.toString(); - } - - /** - * A replacement for Java 8's String.join(). - * - * @param sep - * The separator string. - * @param items - * The items to join. - * @return The string representation of the joined items. - */ - public static String join(final String sep, final Object... items) { - final StringBuilder buf = new StringBuilder(); - boolean first = true; - for (final Object item : items) { - if (first) { - first = false; - } else { - buf.append(sep); - } - buf.append(item.toString()); - } - return buf.toString(); - } -} diff --git a/src/main/java/nonapi/io/github/classgraph/utils/LogNode.java b/src/main/java/nonapi/io/github/classgraph/utils/LogNode.java index 954f0a23e..9004567a1 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/LogNode.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/LogNode.java @@ -49,8 +49,13 @@ * retain a sane order. The order may also be made deterministic by specifying a sort key for log entries. */ public final class LogNode { + // Mitigate log4j2 vulnerability (CVE-2021-44228), in case log4j is added to the classpath as the logger + // https://blog.cloudflare.com/inside-the-log4j2-vulnerability-cve-2021-44228/ + static { + System.getProperties().setProperty("log4j2.formatMsgNoLookups", "true"); + } - /** The Constant log. */ + /** The logger. */ private static final Logger log = Logger.getLogger(ClassGraph.class.getName()); /** diff --git a/src/main/java/nonapi/io/github/classgraph/utils/ProxyingInputStream.java b/src/main/java/nonapi/io/github/classgraph/utils/ProxyingInputStream.java new file mode 100644 index 000000000..0e24abe67 --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/utils/ProxyingInputStream.java @@ -0,0 +1,205 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; + +/** + * A proxying {@link InputStream} implementation that compiles for JDK 7 but can support the methods added in JDK 8 + * by reflection. + */ +public class ProxyingInputStream extends InputStream { + private InputStream inputStream; + + private static Method readAllBytes; + private static Method readNBytes1; + private static Method readNBytes3; + private static Method skipNBytes; + private static Method transferTo; + + static { + // Use reflection for InputStream methods not present in JDK 7. + // TODO Switch to direct method calls once JDK 8 is required, and add back missing @Override annotations + try { + readAllBytes = InputStream.class.getDeclaredMethod("readAllBytes"); + } catch (NoSuchMethodException | SecurityException e1) { + // Ignore + } + try { + readNBytes1 = InputStream.class.getDeclaredMethod("readNBytes", int.class); + } catch (NoSuchMethodException | SecurityException e1) { + // Ignore + } + try { + readNBytes3 = InputStream.class.getDeclaredMethod("readNBytes", byte[].class, int.class, int.class); + } catch (NoSuchMethodException | SecurityException e1) { + // Ignore + } + try { + skipNBytes = InputStream.class.getDeclaredMethod("skipNBytes", long.class); + } catch (NoSuchMethodException | SecurityException e1) { + // Ignore + } + try { + transferTo = InputStream.class.getDeclaredMethod("transferTo", OutputStream.class); + } catch (NoSuchMethodException | SecurityException e1) { + // Ignore + } + } + + /** + * A proxying {@link InputStream} implementation that compiles for JDK 7 but can support the methods added in + * JDK 8 by reflection. + * + * @param inputStream + * the {@link InputStream} to wrap. + */ + public ProxyingInputStream(final InputStream inputStream) { + this.inputStream = inputStream; + } + + @Override + public int read() throws IOException { + return inputStream.read(); + } + + @Override + public int read(final byte[] b) throws IOException { + return inputStream.read(b); + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + return inputStream.read(b, off, len); + } + + // No @Override, since this method is not present in JDK 7 + public byte[] readAllBytes() throws IOException { + if (readAllBytes == null) { + throw new UnsupportedOperationException(); + } + try { + return (byte[]) readAllBytes.invoke(inputStream); + } catch (final Exception e) { + throw new IOException(e); + } + } + + // No @Override, since this method is not present in JDK 7 + public byte[] readNBytes(final int len) throws IOException { + if (readNBytes1 == null) { + throw new UnsupportedOperationException(); + } + try { + return (byte[]) readNBytes1.invoke(inputStream, len); + } catch (final Exception e) { + throw new IOException(e); + } + } + + // No @Override, since this method is not present in JDK 7 + public int readNBytes(final byte[] b, final int off, final int len) throws IOException { + if (readNBytes3 == null) { + throw new UnsupportedOperationException(); + } + try { + return (int) readNBytes3.invoke(inputStream, b, off, len); + } catch (final Exception e) { + throw new IOException(e); + } + } + + @Override + public int available() throws IOException { + return inputStream.available(); + } + + @Override + public boolean markSupported() { + return inputStream.markSupported(); + } + + @Override + public synchronized void mark(final int readlimit) { + inputStream.mark(readlimit); + } + + @Override + public synchronized void reset() throws IOException { + inputStream.reset(); + } + + @Override + public long skip(final long n) throws IOException { + return inputStream.skip(n); + } + + // No @Override, since this method is not present in JDK 7 + public void skipNBytes(final long n) throws IOException { + if (skipNBytes == null) { + throw new UnsupportedOperationException(); + } + try { + skipNBytes.invoke(inputStream, n); + } catch (final Exception e) { + throw new IOException(e); + } + } + + // No @Override, since this method is not present in JDK 7 + public long transferTo(final OutputStream out) throws IOException { + if (transferTo == null) { + throw new UnsupportedOperationException(); + } + try { + return (long) transferTo.invoke(inputStream, out); + } catch (final Exception e) { + throw new IOException(e); + } + } + + @Override + public String toString() { + return inputStream.toString(); + } + + @Override + public void close() throws IOException { + if (inputStream != null) { + try { + inputStream.close(); + } finally { + inputStream = null; + } + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/utils/ReflectionUtils.java b/src/main/java/nonapi/io/github/classgraph/utils/ReflectionUtils.java deleted file mode 100644 index 2a92dbe83..000000000 --- a/src/main/java/nonapi/io/github/classgraph/utils/ReflectionUtils.java +++ /dev/null @@ -1,411 +0,0 @@ -/* - * This file is part of ClassGraph. - * - * Author: Luke Hutchison - * - * Hosted at: https://github.com/classgraph/classgraph - * - * -- - * - * The MIT License (MIT) - * - * Copyright (c) 2019 Luke Hutchison - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT - * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO - * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE - * OR OTHER DEALINGS IN THE SOFTWARE. - */ -package nonapi.io.github.classgraph.utils; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** Reflection utility methods that can be used by ClassLoaderHandlers. */ -public final class ReflectionUtils { - - // In JDK 9+, could use MethodHandles.privateLookupIn - // And then use getter lookup to get fields (which works even if there is no getter function defined): - // https://stackoverflow.com/q/19135218/3950982 - - /** - * Constructor. - */ - private ReflectionUtils() { - // Cannot be constructed - } - - /** - * Get the value of the named field in the class of the given object or any of its superclasses. If an exception - * is thrown while trying to read the field, and throwException is true, then IllegalArgumentException is thrown - * wrapping the cause, otherwise this will return null. If passed a null object, returns null unless - * throwException is true, then throws IllegalArgumentException. - * - * @param cls - * The class. - * @param obj - * The object, or null to get the value of a static field. - * @param fieldName - * The field name. - * @param throwException - * If true, throw an exception if the field value could not be read. - * @return The field value. - * @throws IllegalArgumentException - * If the field value could not be read. - */ - private static Object getFieldVal(final Class cls, final Object obj, final String fieldName, - final boolean throwException) throws IllegalArgumentException { - Field field = null; - for (Class currClass = cls; currClass != null; currClass = currClass.getSuperclass()) { - try { - field = currClass.getDeclaredField(fieldName); - // Field found - break; - } catch (final ReflectiveOperationException | SecurityException e) { - // Try parent - } - } - if (field == null) { - if (throwException) { - throw new IllegalArgumentException((obj == null ? "Static field " : "Field ") + "\"" + fieldName - + "\" not found or not accessible"); - } - } else { - try { - field.setAccessible(true); - } catch (final RuntimeException e) { // JDK 9+: InaccessibleObjectException | SecurityException - // Ignore - } - try { - return field.get(obj); - } catch (final IllegalAccessException e) { - if (throwException) { - throw new IllegalArgumentException( - "Can't read " + (obj == null ? "static " : "") + " field \"" + fieldName + "\": " + e); - } - } - } - return null; - } - - /** - * Get the value of the named field in the class of the given object or any of its superclasses. If an exception - * is thrown while trying to read the field, and throwException is true, then IllegalArgumentException is thrown - * wrapping the cause, otherwise this will return null. If passed a null object, returns null unless - * throwException is true, then throws IllegalArgumentException. - * - * @param obj - * The object. - * @param fieldName - * The field name. - * @param throwException - * If true, throw an exception if the field value could not be read. - * @return The field value. - * @throws IllegalArgumentException - * If the field value could not be read. - */ - public static Object getFieldVal(final Object obj, final String fieldName, final boolean throwException) - throws IllegalArgumentException { - if (obj == null || fieldName == null) { - if (throwException) { - throw new NullPointerException(); - } else { - return null; - } - } - return getFieldVal(obj.getClass(), obj, fieldName, throwException); - } - - /** - * Get the value of the named static field in the given class or any of its superclasses. If an exception is - * thrown while trying to read the field value, and throwException is true, then IllegalArgumentException is - * thrown wrapping the cause, otherwise this will return null. If passed a null class reference, returns null - * unless throwException is true, then throws IllegalArgumentException. - * - * @param cls - * The class. - * @param fieldName - * The field name. - * @param throwException - * If true, throw an exception if the field value could not be read. - * @return The field value. - * @throws IllegalArgumentException - * If the field value could not be read. - */ - public static Object getStaticFieldVal(final Class cls, final String fieldName, final boolean throwException) - throws IllegalArgumentException { - if (cls == null || fieldName == null) { - if (throwException) { - throw new NullPointerException(); - } else { - return null; - } - } - return getFieldVal(cls, null, fieldName, throwException); - } - - /** - * Iterate through implemented interfaces, top-down, then superclass to subclasses, top-down (since higher-up - * superclasses and superinterfaces have the highest chance of being visible). - * - * @param cls - * the class - * @return the reverse of the order in which method calls would be attempted by the JRE. - */ - private static List> getReverseMethodAttemptOrder(final Class cls) { - final List> reverseAttemptOrder = new ArrayList<>(); - - // Iterate from class to its superclasses - for (Class c = cls; c != null && c != Object.class; c = c.getSuperclass()) { - reverseAttemptOrder.add(c); - } - - // Find interfaces and superinterfaces implemented by this class or its superclasses - final Set> addedIfaces = new HashSet<>(); - final LinkedList> ifaceQueue = new LinkedList<>(); - for (Class c = cls; c != null; c = c.getSuperclass()) { - if (c.isInterface() && addedIfaces.add(c)) { - ifaceQueue.add(c); - } - for (final Class iface : c.getInterfaces()) { - if (addedIfaces.add(iface)) { - ifaceQueue.add(iface); - } - } - } - while (!ifaceQueue.isEmpty()) { - final Class iface = ifaceQueue.remove(); - reverseAttemptOrder.add(iface); - final Class[] superIfaces = iface.getInterfaces(); - if (superIfaces.length > 0) { - for (final Class superIface : superIfaces) { - if (addedIfaces.add(superIface)) { - ifaceQueue.add(superIface); - } - } - } - } - return reverseAttemptOrder; - } - - /** - * Invoke the named method in the given object or its superclasses. If an exception is thrown while trying to - * call the method, and throwException is true, then IllegalArgumentException is thrown wrapping the cause, - * otherwise this will return null. If passed a null object, returns null unless throwException is true, then - * throws IllegalArgumentException. - * - * @param cls - * The class. - * @param obj - * The object, or null to invoke a static method. - * @param methodName - * The method name. - * @param oneArg - * If true, look for a method with one argument of type argType. If false, look for method with no - * arguments. - * @param argType - * The type of the first argument to the method. - * @param param - * The value of the first parameter to invoke the method with. - * @param throwException - * If true, throw an exception if the field value could not be read. - * @return The field value. - * @throws IllegalArgumentException - * If the field value could not be read. - */ - private static Object invokeMethod(final Class cls, final Object obj, final String methodName, - final boolean oneArg, final Class argType, final Object param, final boolean throwException) - throws IllegalArgumentException { - Method method = null; - final List> reverseAttemptOrder = getReverseMethodAttemptOrder(cls); - for (int i = reverseAttemptOrder.size() - 1; i >= 0; i--) { - final Class classOrInterface = reverseAttemptOrder.get(i); - try { - method = oneArg ? classOrInterface.getDeclaredMethod(methodName, argType) - : classOrInterface.getDeclaredMethod(methodName); - // Method found - break; - } catch (final ReflectiveOperationException | SecurityException e) { - // Try next interface or superclass - } - } - if (method == null) { - if (throwException) { - throw new IllegalArgumentException((obj == null ? "Static method " : "Method ") + "\"" + methodName - + "\" not found or not accesible"); - } - } else { - try { - method.setAccessible(true); - } catch (final RuntimeException e) { // JDK 9+: InaccessibleObjectException | SecurityException - // Ignore - } - try { - return oneArg ? method.invoke(obj, param) : method.invoke(obj); - } catch (final IllegalAccessException e) { - if (throwException) { - throw new IllegalArgumentException( - "Can't call " + (obj == null ? "static " : "") + "method \"" + methodName + "\": " + e); - } - } catch (final InvocationTargetException e) { - if (throwException) { - throw new IllegalArgumentException("Exception while invoking " + (obj == null ? "static " : "") - + "method \"" + methodName + "\"", e); - } - } - } - return null; - } - - /** - * Invoke the named method in the given object or its superclasses. If an exception is thrown while trying to - * call the method, and throwException is true, then IllegalArgumentException is thrown wrapping the cause, - * otherwise this will return null. If passed a null object, returns null unless throwException is true, then - * throws IllegalArgumentException. - * - * @param obj - * The object. - * @param methodName - * The method name. - * @param throwException - * If true, throw an exception if the field value could not be read. - * @return The field value. - * @throws IllegalArgumentException - * If the field value could not be read. - */ - public static Object invokeMethod(final Object obj, final String methodName, final boolean throwException) - throws IllegalArgumentException { - if (obj == null || methodName == null) { - if (throwException) { - throw new NullPointerException(); - } else { - return null; - } - } - return invokeMethod(obj.getClass(), obj, methodName, false, null, null, throwException); - } - - /** - * Invoke the named method in the given object or its superclasses. If an exception is thrown while trying to - * call the method, and throwException is true, then IllegalArgumentException is thrown wrapping the cause, - * otherwise this will return null. If passed a null object, returns null unless throwException is true, then - * throws IllegalArgumentException. - * - * @param obj - * The object. - * @param methodName - * The method name. - * @param argType - * The type of the method argument. - * @param param - * The parameter value to use when invoking the method. - * @param throwException - * Whether to throw an exception on failure. - * @return The result of the method invocation. - * @throws IllegalArgumentException - * If the method could not be invoked. - */ - public static Object invokeMethod(final Object obj, final String methodName, final Class argType, - final Object param, final boolean throwException) throws IllegalArgumentException { - if (obj == null || methodName == null) { - if (throwException) { - throw new NullPointerException(); - } else { - return null; - } - } - return invokeMethod(obj.getClass(), obj, methodName, true, argType, param, throwException); - } - - /** - * Invoke the named static method. If an exception is thrown while trying to call the method, and throwException - * is true, then IllegalArgumentException is thrown wrapping the cause, otherwise this will return null. If - * passed a null class reference, returns null unless throwException is true, then throws - * IllegalArgumentException. - * - * @param cls - * The class. - * @param methodName - * The method name. - * @param throwException - * Whether to throw an exception on failure. - * @return The result of the method invocation. - * @throws IllegalArgumentException - * If the method could not be invoked. - */ - public static Object invokeStaticMethod(final Class cls, final String methodName, - final boolean throwException) throws IllegalArgumentException { - if (cls == null || methodName == null) { - if (throwException) { - throw new NullPointerException(); - } else { - return null; - } - } - return invokeMethod(cls, null, methodName, false, null, null, throwException); - } - - /** - * Invoke the named static method. If an exception is thrown while trying to call the method, and throwException - * is true, then IllegalArgumentException is thrown wrapping the cause, otherwise this will return null. If - * passed a null class reference, returns null unless throwException is true, then throws - * IllegalArgumentException. - * - * @param cls - * The class. - * @param methodName - * The method name. - * @param argType - * The type of the method argument. - * @param param - * The parameter value to use when invoking the method. - * @param throwException - * Whether to throw an exception on failure. - * @return The result of the method invocation. - * @throws IllegalArgumentException - * If the method could not be invoked. - */ - public static Object invokeStaticMethod(final Class cls, final String methodName, final Class argType, - final Object param, final boolean throwException) throws IllegalArgumentException { - if (cls == null || methodName == null) { - if (throwException) { - throw new NullPointerException(); - } else { - return null; - } - } - return invokeMethod(cls, null, methodName, true, argType, param, throwException); - } - - /** - * Call Class.forName(className), but return null if any exception is thrown. - * - * @param className - * The class name to load. - * @return The class of the requested name, or null if an exception was thrown while trying to load the class. - */ - public static Class classForNameOrNull(final String className) { - try { - return Class.forName(className); - } catch (final ReflectiveOperationException | LinkageError e) { - return null; - } - } - -} diff --git a/src/main/java/nonapi/io/github/classgraph/utils/StringUtils.java b/src/main/java/nonapi/io/github/classgraph/utils/StringUtils.java new file mode 100644 index 000000000..f601be8fd --- /dev/null +++ b/src/main/java/nonapi/io/github/classgraph/utils/StringUtils.java @@ -0,0 +1,208 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package nonapi.io.github.classgraph.utils; + +/** + * File utilities. + */ +public final class StringUtils { + /** + * Constructor. + */ + private StringUtils() { + // Cannot be constructed + } + + /** + * Reads the "modified UTF8" format defined in the Java classfile spec, optionally replacing '/' with '.', and + * optionally removing the prefix "L" and the suffix ";". + * + * @param arr + * the array to read the string from + * @param startOffset + * The start offset of the string within the array. + * @param numBytes + * The number of bytes of the UTF8 encoding of the string. + * @param replaceSlashWithDot + * If true, replace '/' with '.'. + * @param stripLSemicolon + * If true, string final ';' character. + * @return The string. + * @throws IllegalArgumentException + * If string could not be parsed. + */ + public static String readString(final byte[] arr, final int startOffset, final int numBytes, + final boolean replaceSlashWithDot, final boolean stripLSemicolon) throws IllegalArgumentException { + if (startOffset < 0L || numBytes < 0 || startOffset + numBytes > arr.length) { + throw new IllegalArgumentException("offset or numBytes out of range"); + } + final char[] chars = new char[numBytes]; + int byteIdx = 0; + int charIdx = 0; + for (; byteIdx < numBytes; byteIdx++) { + final int c = arr[startOffset + byteIdx] & 0xff; + if (c > 127) { + break; + } + chars[charIdx++] = (char) (replaceSlashWithDot && c == '/' ? '.' : c); + } + while (byteIdx < numBytes) { + final int c = arr[startOffset + byteIdx] & 0xff; + switch (c >> 4) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: { + byteIdx++; + chars[charIdx++] = (char) (replaceSlashWithDot && c == '/' ? '.' : c); + break; + } + case 12: + case 13: { + byteIdx += 2; + if (byteIdx > numBytes) { + throw new IllegalArgumentException("Bad modified UTF8"); + } + final int c2 = arr[startOffset + byteIdx - 1]; + if ((c2 & 0xc0) != 0x80) { + throw new IllegalArgumentException("Bad modified UTF8"); + } + final int c3 = ((c & 0x1f) << 6) | (c2 & 0x3f); + chars[charIdx++] = (char) (replaceSlashWithDot && c3 == '/' ? '.' : c3); + break; + } + case 14: { + byteIdx += 3; + if (byteIdx > numBytes) { + throw new IllegalArgumentException("Bad modified UTF8"); + } + final int c2 = arr[startOffset + byteIdx - 2]; + final int c3 = arr[startOffset + byteIdx - 1]; + if ((c2 & 0xc0) != 0x80 || (c3 & 0xc0) != 0x80) { + throw new IllegalArgumentException("Bad modified UTF8"); + } + final int c4 = ((c & 0x0f) << 12) | ((c2 & 0x3f) << 6) | (c3 & 0x3f); + chars[charIdx++] = (char) (replaceSlashWithDot && c4 == '/' ? '.' : c4); + break; + } + default: + throw new IllegalArgumentException("Bad modified UTF8"); + } + } + if (charIdx == numBytes && !stripLSemicolon) { + return new String(chars); + } else { + if (stripLSemicolon) { + if (charIdx < 2 || chars[0] != 'L' || chars[charIdx - 1] != ';') { + throw new IllegalArgumentException("Expected string to start with 'L' and end with ';', got \"" + + new String(chars) + "\""); + } + return new String(chars, 1, charIdx - 2); + } else { + return new String(chars, 0, charIdx); + } + } + } + + /** + * A replacement for Java 8's String.join(). + * + * @param buf + * The buffer to append to. + * @param addAtBeginning + * The token to add at the beginning of the string. + * @param sep + * The separator string. + * @param addAtEnd + * The token to add at the end of the string. + * @param iterable + * The {@link Iterable} to join. + */ + public static void join(final StringBuilder buf, final String addAtBeginning, final String sep, + final String addAtEnd, final Iterable iterable) { + if (!addAtBeginning.isEmpty()) { + buf.append(addAtBeginning); + } + boolean first = true; + for (final Object item : iterable) { + if (first) { + first = false; + } else { + buf.append(sep); + } + buf.append(item == null ? "null" : item.toString()); + } + if (!addAtEnd.isEmpty()) { + buf.append(addAtEnd); + } + } + + /** + * A replacement for Java 8's String.join(). + * + * @param sep + * The separator string. + * @param iterable + * The {@link Iterable} to join. + * @return The string representation of the joined elements. + */ + public static String join(final String sep, final Iterable iterable) { + final StringBuilder buf = new StringBuilder(); + join(buf, "", sep, "", iterable); + return buf.toString(); + } + + /** + * A replacement for Java 8's String.join(). + * + * @param sep + * The separator string. + * @param items + * The items to join. + * @return The string representation of the joined items. + */ + public static String join(final String sep, final Object... items) { + final StringBuilder buf = new StringBuilder(); + boolean first = true; + for (final Object item : items) { + if (first) { + first = false; + } else { + buf.append(sep); + } + buf.append(item.toString()); + } + return buf.toString(); + } + +} diff --git a/src/main/java/nonapi/io/github/classgraph/utils/URLPathEncoder.java b/src/main/java/nonapi/io/github/classgraph/utils/URLPathEncoder.java index a1502528f..28ed7bb8f 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/URLPathEncoder.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/URLPathEncoder.java @@ -28,8 +28,12 @@ */ package nonapi.io.github.classgraph.utils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import nonapi.io.github.classgraph.utils.VersionFinder.OperatingSystem; + /** A simple URL path encoder. */ public final class URLPathEncoder { @@ -52,6 +56,8 @@ public final class URLPathEncoder { safe['!'] = safe['*'] = safe['\''] = safe['('] = safe[')'] = safe[','] = true; // Only include "/" from "fsegment" and "hsegment" rules (exclude ':', '@', '&' and '=' for safety) safe['/'] = true; + // Also allow '+' characters (#468) + //safe['+'] = true; } /** Hexadecimal digits. */ @@ -68,6 +74,67 @@ private URLPathEncoder() { // Cannot be constructed } + /** Unescape chars in a URL. URLDecoder.decode is broken: https://bugs.openjdk.java.net/browse/JDK-8179507 */ + private static void unescapeChars(final String str, final boolean isQuery, final ByteArrayOutputStream buf) { + if (str.isEmpty()) { + return; + } + for (int chrIdx = 0, len = str.length(); chrIdx < len; chrIdx++) { + final char c = str.charAt(chrIdx); + if (c == '%') { + // Decode %-escaped char sequence, e.g. %5D + if (chrIdx > len - 3) { + // Ignore truncated %-seq at end of string + } else { + final char c1 = str.charAt(++chrIdx); + final int digit1 = c1 >= '0' && c1 <= '9' ? (c1 - '0') + : c1 >= 'a' && c1 <= 'f' ? (c1 - 'a' + 10) + : c1 >= 'A' && c1 <= 'F' ? (c1 - 'A' + 10) : -1; + final char c2 = str.charAt(++chrIdx); + final int digit2 = c2 >= '0' && c2 <= '9' ? (c2 - '0') + : c2 >= 'a' && c2 <= 'f' ? (c2 - 'a' + 10) + : c2 >= 'A' && c2 <= 'F' ? (c2 - 'A' + 10) : -1; + if (digit1 < 0 || digit2 < 0) { + try { + buf.write(str.substring(chrIdx - 2, chrIdx + 1).getBytes(StandardCharsets.UTF_8)); + } catch (final IOException e) { + // Ignore + } + } else { + buf.write((byte) ((digit1 << 4) | digit2)); + } + } + } else if (isQuery && c == '+') { + buf.write((byte) ' '); + } else if (c <= 0x7f) { + buf.write((byte) c); + } else { + try { + buf.write(Character.toString(c).getBytes(StandardCharsets.UTF_8)); + } catch (final IOException e) { + // Ignore + } + } + } + } + + /** + * Unescape a URL segment, and turn it from UTF-8 bytes into a Java string. + * + * @param str + * the str + * @return the string + */ + public static String decodePath(final String str) { + final int queryIdx = str.indexOf('?'); + final String partBeforeQuery = queryIdx < 0 ? str : str.substring(0, queryIdx); + final String partFromQuery = queryIdx < 0 ? "" : str.substring(queryIdx); + final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + unescapeChars(partBeforeQuery, /* isQuery = */ false, buf); + unescapeChars(partFromQuery, /* isQuery = */ true, buf); + return new String(buf.toByteArray(), StandardCharsets.UTF_8); + } + /** * Encode a URL path using percent-encoding. '/' is not encoded. * @@ -75,7 +142,7 @@ private URLPathEncoder() { * The path to encode. * @return The encoded path. */ - private static String encodePath(final String path) { + public static String encodePath(final String path) { // Accept ':' if it is part of a scheme prefix int validColonPrefixLen = 0; for (final String scheme : SCHEME_PREFIXES) { @@ -84,6 +151,18 @@ private static String encodePath(final String path) { break; } } + // Also accept ':' after a Windows drive letter + if (VersionFinder.OS == OperatingSystem.Windows) { + int i = validColonPrefixLen; + if (i < path.length() && path.startsWith("///", i)) { + i += "///".length(); + } + if (i < path.length() - 1 && Character.isLetter(path.charAt(i)) && path.charAt(i + 1) == ':') { + validColonPrefixLen = i + 2; + } + } + + // Apply URL encoding rules to rest of path final byte[] pathBytes = path.getBytes(StandardCharsets.UTF_8); final StringBuilder encodedPath = new StringBuilder(pathBytes.length * 3); for (int i = 0; i < pathBytes.length; i++) { @@ -111,12 +190,51 @@ public static String normalizeURLPath(final String urlPath) { String urlPathNormalized = urlPath; if (!urlPathNormalized.startsWith("jrt:") && !urlPathNormalized.startsWith("http://") && !urlPathNormalized.startsWith("https://")) { - // Any URL with the "jar:" prefix must have "/" after any "!" + + // Strip "jar:" and/or "file:", if already present + if (urlPathNormalized.startsWith("jar:")) { + urlPathNormalized = urlPathNormalized.substring(4); + } + if (urlPathNormalized.startsWith("file:")) { + urlPathNormalized = urlPathNormalized.substring(4); + } + + // On Windows, remove drive prefix from path, if present (otherwise the ':' after the drive + // letter will be escaped as %3A) + String windowsDrivePrefix = ""; + if (VersionFinder.OS == OperatingSystem.Windows) { + if (urlPathNormalized.length() >= 2 && Character.isLetter(urlPathNormalized.charAt(0)) + && urlPathNormalized.charAt(1) == ':') { + // Path of form "C:/xyz" + windowsDrivePrefix = urlPathNormalized.substring(0, 2); + urlPathNormalized = urlPathNormalized.substring(2); + } else if (urlPathNormalized.length() >= 3 && urlPathNormalized.charAt(0) == '/' + && Character.isLetter(urlPathNormalized.charAt(1)) && urlPathNormalized.charAt(2) == ':') { + // Path of form "/C:/xyz" + windowsDrivePrefix = urlPathNormalized.substring(1, 3); + urlPathNormalized = urlPathNormalized.substring(3); + } + } + + // Any URL containing "!" segments must have "/" after "!" for the "jar:" URL scheme to work urlPathNormalized = urlPathNormalized.replace("/!", "!").replace("!/", "!").replace("!", "!/"); - // Prepend "jar:file:" - if (!urlPathNormalized.startsWith("file:") && !urlPathNormalized.startsWith("jar:")) { - urlPathNormalized = "file:" + urlPathNormalized; + + // Prepend "file:///" to absolute paths and "file:" to relative paths + if (windowsDrivePrefix.isEmpty()) { + // There is no Windows drive + if (urlPathNormalized.startsWith("/")) { + // Absolute path: file:///xyz + urlPathNormalized = "file://" + urlPathNormalized; + } else { + // Relative path: file:xyz + urlPathNormalized = "file:" + urlPathNormalized; + } + } else { + // There is a Windows drive, path must be absolute + urlPathNormalized = "file:///" + windowsDrivePrefix + urlPathNormalized; } + + // Prepend "jar:" if path contains a "!" segment if (urlPathNormalized.contains("!") && !urlPathNormalized.startsWith("jar:")) { urlPathNormalized = "jar:" + urlPathNormalized; } diff --git a/src/main/java/nonapi/io/github/classgraph/utils/VersionFinder.java b/src/main/java/nonapi/io/github/classgraph/utils/VersionFinder.java index a5a9e752b..8ee55625c 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/VersionFinder.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/VersionFinder.java @@ -28,18 +28,24 @@ */ package nonapi.io.github.classgraph.utils; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.Properties; +import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathFactoryConfigurationException; import org.w3c.dom.Document; @@ -58,22 +64,52 @@ public final class VersionFinder { public static final OperatingSystem OS; /** Java version string. */ - private static final String JAVA_VERSION = getProperty("java.version"); + public static final String JAVA_VERSION = getProperty("java.version"); /** Java major version -- 7 for "1.7", 8 for "1.8.0_244", 9 for "9", 11 for "11-ea", etc. */ public static final int JAVA_MAJOR_VERSION; + /** Java minor version -- 0 for "11.0.4" */ + public static final int JAVA_MINOR_VERSION; + + /** Java minor version -- 4 for "11.0.4" */ + public static final int JAVA_SUB_VERSION; + + /** Java is EA release -- true for "11-ea", etc. */ + public static final boolean JAVA_IS_EA_VERSION; + static { int javaMajorVersion = 0; + int javaMinorVersion = 0; + int javaSubVersion = 0; + final List versionParts = new ArrayList<>(); if (JAVA_VERSION != null) { for (final String versionPart : JAVA_VERSION.split("[^0-9]+")) { - if (!versionPart.isEmpty() && !versionPart.equals("1")) { - javaMajorVersion = Integer.parseInt(versionPart); - break; + try { + versionParts.add(Integer.parseInt(versionPart)); + } catch (final NumberFormatException e) { + // Skip } } + if (!versionParts.isEmpty() && versionParts.get(0) == 1) { + // 1.7 or 1.8 -> 7 or 8 + versionParts.remove(0); + } + if (versionParts.isEmpty()) { + throw new RuntimeException("Could not determine Java version: " + JAVA_VERSION); + } + javaMajorVersion = versionParts.get(0); + if (versionParts.size() > 1) { + javaMinorVersion = versionParts.get(1); + } + if (versionParts.size() > 2) { + javaSubVersion = versionParts.get(2); + } } JAVA_MAJOR_VERSION = javaMajorVersion; + JAVA_MINOR_VERSION = javaMinorVersion; + JAVA_SUB_VERSION = javaSubVersion; + JAVA_IS_EA_VERSION = JAVA_VERSION != null && JAVA_VERSION.endsWith("-ea"); } /** The operating system type. */ @@ -102,12 +138,14 @@ public enum OperatingSystem { static { final String osName = getProperty("os.name", "unknown").toLowerCase(Locale.ENGLISH); - if (osName == null) { + if (File.separatorChar == '\\') { + OS = OperatingSystem.Windows; + } else if (osName == null) { OS = OperatingSystem.Unknown; - } else if (osName.contains("mac") || osName.contains("darwin")) { - OS = OperatingSystem.MacOSX; } else if (osName.contains("win")) { OS = OperatingSystem.Windows; + } else if (osName.contains("mac") || osName.contains("darwin")) { + OS = OperatingSystem.MacOSX; } else if (osName.contains("nux")) { OS = OperatingSystem.Linux; } else if (osName.contains("sunos") || osName.contains("solaris")) { @@ -189,9 +227,9 @@ public static synchronized String getVersion() { for (int i = 0; i < 3 && path != null; i++, path = path.getParent()) { final Path pom = path.resolve("pom.xml"); try (InputStream is = Files.newInputStream(pom)) { - final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is); + final Document doc = getSecureDocumentBuilderFactory().newDocumentBuilder().parse(is); doc.getDocumentElement().normalize(); - String version = (String) XPathFactory.newInstance().newXPath().compile("/project/version") + String version = (String) getSecureXPathFactory().newXPath().compile("/project/version") .evaluate(doc, XPathConstants.STRING); if (version != null) { version = version.trim(); @@ -244,4 +282,43 @@ public static synchronized String getVersion() { } return "unknown"; } + + /** + * Helper method to provide a XXE secured DocumentBuilder Factory. + * + * reference - https://gist.github.com/AlainODea/1779a7c6a26a5c135280bc9b3b71868f + * + * reference - https://rules.sonarsource.com/java/tag/owasp/RSPEC-2755 + * + * @return DocumentBuilderFactory + * @throws ParserConfigurationException + */ + private static DocumentBuilderFactory getSecureDocumentBuilderFactory() throws ParserConfigurationException { + final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setXIncludeAware(false); + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + dbf.setExpandEntityReferences(false); + dbf.setNamespaceAware(true); + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + return dbf; + } + + /** + * Helper method to provide a XXE secured XPathFactory Factory. + * + * reference - https://rules.sonarsource.com/java/tag/owasp/RSPEC-2755 + * + * @return XPathFactory + * @throws XPathFactoryConfigurationException + */ + private static XPathFactory getSecureXPathFactory() throws XPathFactoryConfigurationException { + final XPathFactory xPathFactory = XPathFactory.newInstance(); + xPathFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + return xPathFactory; + } } diff --git a/src/module-info/COMPILING b/src/module-info/COMPILING new file mode 100644 index 000000000..8c20e98f9 --- /dev/null +++ b/src/module-info/COMPILING @@ -0,0 +1,4 @@ +# This is compiled separately from ClassGraph, so that ClassGraph can be built on JDK 8 in JDK 7 mode + +# Replace 1.0.4 with the latest cached version of narcissus +javac -p ~/.m2/repository/io/github/toolfactory/narcissus/1.0.4/narcissus-1.0.4.jar --release 9 io.github.classgraph/module-info.java io.github.classgraph/io/github/classgraph/Placeholder.java diff --git a/src/module-info/io.github.classgraph/io/github/classgraph/Placeholder.java b/src/module-info/io.github.classgraph/io/github/classgraph/Placeholder.java new file mode 100644 index 000000000..a9a30d332 --- /dev/null +++ b/src/module-info/io.github.classgraph/io/github/classgraph/Placeholder.java @@ -0,0 +1,3 @@ +// See https://stackoverflow.com/a/50204997/3950982 +package io.github.classgraph; +class Placeholder {} diff --git a/src/module-info/io.github.classgraph/module-info.class b/src/module-info/io.github.classgraph/module-info.class new file mode 100644 index 000000000..9b7f52051 Binary files /dev/null and b/src/module-info/io.github.classgraph/module-info.class differ diff --git a/src/module-info/io.github.classgraph/module-info.java b/src/module-info/io.github.classgraph/module-info.java new file mode 100644 index 000000000..4901edf83 --- /dev/null +++ b/src/module-info/io.github.classgraph/module-info.java @@ -0,0 +1,53 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * ClassGraph: the uber-fast, ultra-lightweight classpath + * and module scanner for JVM languages. + * + * @author Luke Hutchison + */ +// Compile this in JDK 9 compatibility mode +module io.github.classgraph { + exports io.github.classgraph; + + // N.B. make sure the "Import-Package" entries in the manifest (in pom.xml) match these "requires" statements + // VersionFinder requires java.xml + requires java.xml; + // FileUtils requires jdk.unsupported (for usage of Unsafe) + requires jdk.unsupported; + // ModulePathInfo requires java.management + requires java.management; + // LogNode requires java.logging + requires java.logging; + + // ReflectionUtils may use narcissus or jvm-driver, if available + requires static io.github.toolfactory.narcissus; + requires static io.github.toolfactory.jvm; +} diff --git a/src/test/java/ClassGraphGraphVizGenerator.java b/src/test/java/ClassGraphGraphVizGenerator.java index 25668a16f..93640edd7 100644 --- a/src/test/java/ClassGraphGraphVizGenerator.java +++ b/src/test/java/ClassGraphGraphVizGenerator.java @@ -5,10 +5,9 @@ import io.github.classgraph.ScanResult; /** - * The Class ClassGraphGraphVizGenerator. + * ClassGraphGraphVizGenerator. */ public class ClassGraphGraphVizGenerator { - /** * The main method. * @@ -19,7 +18,7 @@ public class ClassGraphGraphVizGenerator { */ public static void main(final String[] args) throws IOException { try (ScanResult scanResult = new ClassGraph() // - .whitelistPackagesNonRecursive("io.github.classgraph") // + .acceptPackagesNonRecursive("io.github.classgraph") // .enableMethodInfo() // .ignoreMethodVisibility() // .enableFieldInfo() // diff --git a/src/test/java/ClassInDefaultPackage.java b/src/test/java/ClassInDefaultPackage.java index 478de30b7..7a67e1f40 100644 --- a/src/test/java/ClassInDefaultPackage.java +++ b/src/test/java/ClassInDefaultPackage.java @@ -1,5 +1,5 @@ /** - * The Class ClassInDefaultPackage. + * ClassInDefaultPackage. */ public class ClassInDefaultPackage { } diff --git a/src/test/java/DefaultPackageTest.java b/src/test/java/DefaultPackageTest.java index acf2e3601..7b40a1ff6 100644 --- a/src/test/java/DefaultPackageTest.java +++ b/src/test/java/DefaultPackageTest.java @@ -32,46 +32,45 @@ import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; -import io.github.classgraph.test.whitelisted.Cls; -import io.github.classgraph.test.whitelisted.blacklistedsub.BlacklistedSub; +import io.github.classgraph.test.accepted.Cls; +import io.github.classgraph.test.accepted.rejectedsub.RejectedSub; /** - * The Class DefaultPackageTest. + * DefaultPackageTest. */ public class DefaultPackageTest { - - /** The Constant WHITELIST_PACKAGE. */ - private static final String WHITELIST_PACKAGE = Cls.class.getPackage().getName(); + /** The Constant ACCEPT_PACKAGE. */ + private static final String ACCEPT_PACKAGE = Cls.class.getPackage().getName(); /** * Scan. */ @Test public void scan() { - try (ScanResult scanResult = new ClassGraph().whitelistPackagesNonRecursive("").scan()) { + try (ScanResult scanResult = new ClassGraph().acceptPackagesNonRecursive("").scan()) { final List allClasses = scanResult.getAllClasses().getNames(); assertThat(allClasses).contains(DefaultPackageTest.class.getName()); assertThat(allClasses).contains(ClassInDefaultPackage.class.getName()); assertThat(allClasses).doesNotContain(Cls.class.getName()); assertThat(allClasses).doesNotContain(ClassGraph.class.getName()); assertThat(allClasses).doesNotContain(String.class.getName()); - assertThat(allClasses).doesNotContain(BlacklistedSub.class.getName()); + assertThat(allClasses).doesNotContain(RejectedSub.class.getName()); } } /** - * Scan with whitelist. + * Scan with accept. */ @Test - public void scanWithWhitelist() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).scan()) { + public void scanWithAccept() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { final List allClasses = scanResult.getAllClasses().getNames(); assertThat(allClasses).doesNotContain(DefaultPackageTest.class.getName()); - assertThat(allClasses).contains(BlacklistedSub.class.getName()); + assertThat(allClasses).contains(RejectedSub.class.getName()); assertThat(allClasses).contains(Cls.class.getName()); assertThat(allClasses).doesNotContain(ClassGraph.class.getName()); assertThat(allClasses).doesNotContain(String.class.getName()); diff --git a/src/test/java/DisableRecursiveScanningTest.java b/src/test/java/DisableRecursiveScanningTest.java index bd5b4da46..8b0506b96 100644 --- a/src/test/java/DisableRecursiveScanningTest.java +++ b/src/test/java/DisableRecursiveScanningTest.java @@ -32,18 +32,17 @@ import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; -import io.github.classgraph.test.whitelisted.Cls; -import io.github.classgraph.test.whitelisted.blacklistedsub.BlacklistedSub; +import io.github.classgraph.test.accepted.Cls; +import io.github.classgraph.test.accepted.rejectedsub.RejectedSub; /** - * The Class DisableRecursiveScanningTest. + * DisableRecursiveScanningTest. */ public class DisableRecursiveScanningTest { - /** The Constant PKG. */ private static final String PKG = Cls.class.getPackage().getName(); @@ -52,10 +51,10 @@ public class DisableRecursiveScanningTest { */ @Test public void nonRootPackage() { - try (ScanResult scanResult = new ClassGraph().whitelistPackagesNonRecursive(PKG).scan()) { + try (ScanResult scanResult = new ClassGraph().acceptPackagesNonRecursive(PKG).scan()) { final List allClasses = scanResult.getAllClasses().getNames(); assertThat(allClasses).contains(Cls.class.getName()); - assertThat(allClasses).doesNotContain(BlacklistedSub.class.getName()); + assertThat(allClasses).doesNotContain(RejectedSub.class.getName()); } } @@ -64,7 +63,7 @@ public void nonRootPackage() { */ @Test public void rootPackage() { - try (ScanResult scanResult = new ClassGraph().whitelistPackagesNonRecursive("").scan()) { + try (ScanResult scanResult = new ClassGraph().acceptPackagesNonRecursive("").scan()) { final List allClasses = scanResult.getAllClasses().getNames(); assertThat(allClasses).contains(ClassInDefaultPackage.class.getName()); assertThat(allClasses).doesNotContain(Cls.class.getName()); diff --git a/src/test/java/com/xyz/GenerateClassGraphFigDotFile.java b/src/test/java/com/xyz/GenerateClassGraphFigDotFile.java index 176212755..fb4ecb89e 100644 --- a/src/test/java/com/xyz/GenerateClassGraphFigDotFile.java +++ b/src/test/java/com/xyz/GenerateClassGraphFigDotFile.java @@ -4,10 +4,9 @@ import io.github.classgraph.ScanResult; /** - * The Class GenerateClassGraphFigDotFile. + * GenerateClassGraphFigDotFile. */ public class GenerateClassGraphFigDotFile { - /** * The main method. * @@ -16,7 +15,7 @@ public class GenerateClassGraphFigDotFile { */ public static void main(final String[] args) { try (ScanResult scanResult = new ClassGraph() // - .whitelistPackages("com.xyz.fig") // + .acceptPackages("com.xyz.fig") // .ignoreFieldVisibility() // .enableFieldInfo() // .ignoreMethodVisibility() // diff --git a/src/test/java/com/xyz/MetaAnnotationTest.java b/src/test/java/com/xyz/MetaAnnotationTest.java index 866eb8915..b980942d6 100644 --- a/src/test/java/com/xyz/MetaAnnotationTest.java +++ b/src/test/java/com/xyz/MetaAnnotationTest.java @@ -30,35 +30,34 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class MetaAnnotationTest. + * MetaAnnotationTest. */ -public class MetaAnnotationTest { - +class MetaAnnotationTest { /** The scan result. */ static ScanResult scanResult; /** * Setup. */ - @BeforeClass - public static void setUp() { - scanResult = new ClassGraph().whitelistPackages("com.xyz.meta").enableClassInfo().enableAnnotationInfo() + @BeforeAll + static void setUp() { + scanResult = new ClassGraph().acceptPackages("com.xyz.meta").enableClassInfo().enableAnnotationInfo() .scan(); } /** * Teardown. */ - @AfterClass - public static void tearDown() { + @AfterAll + static void tearDown() { scanResult.close(); scanResult = null; } @@ -67,101 +66,100 @@ public static void tearDown() { * One level. */ @Test - public void oneLevel() { + void oneLevel() { assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.E").directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.B"); + .containsOnly("com.xyz.meta.B"); assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.F").directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.B", "com.xyz.meta.A"); + .containsOnly("com.xyz.meta.B", "com.xyz.meta.A"); assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.G").directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.C"); + .containsOnly("com.xyz.meta.C"); } /** * Two levels. */ @Test - public void twoLevels() { - assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.J").getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.F", "com.xyz.meta.E", "com.xyz.meta.B", "com.xyz.meta.A"); + void twoLevels() { + assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.J").getNames()).containsOnly("com.xyz.meta.F", + "com.xyz.meta.E", "com.xyz.meta.B", "com.xyz.meta.A"); } /** * Three levels. */ @Test - public void threeLevels() { - assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.L").getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.I", "com.xyz.meta.E", "com.xyz.meta.B", "com.xyz.meta.H"); + void threeLevels() { + assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.L").getNames()).containsOnly("com.xyz.meta.I", + "com.xyz.meta.E", "com.xyz.meta.B", "com.xyz.meta.H"); } /** * Across cycle. */ @Test - public void acrossCycle() { + void acrossCycle() { assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.H").directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.I"); - assertThat(scanResult.getAnnotationsOnClass("com.xyz.meta.H").directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.I", "com.xyz.meta.K", "java.lang.annotation.Retention", - "java.lang.annotation.Target"); + .containsOnly("com.xyz.meta.I"); + assertThat(scanResult.getAnnotationsOnClass("com.xyz.meta.H").directOnly().getNames()).containsOnly( + "com.xyz.meta.I", "com.xyz.meta.K", "java.lang.annotation.Retention", + "java.lang.annotation.Target"); assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.I").directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.E", "com.xyz.meta.H"); - assertThat(scanResult.getAnnotationsOnClass("com.xyz.meta.I").directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.L", "com.xyz.meta.H", "java.lang.annotation.Retention", - "java.lang.annotation.Target"); + .containsOnly("com.xyz.meta.E", "com.xyz.meta.H"); + assertThat(scanResult.getAnnotationsOnClass("com.xyz.meta.I").directOnly().getNames()).containsOnly( + "com.xyz.meta.L", "com.xyz.meta.H", "java.lang.annotation.Retention", + "java.lang.annotation.Target"); assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.K").directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.H"); + .containsOnly("com.xyz.meta.H"); assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.D").directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.K"); + .containsOnly("com.xyz.meta.K"); } /** * Cycle annotates self. */ @Test - public void cycleAnnotatesSelf() { - assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.I").getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.E", "com.xyz.meta.B", "com.xyz.meta.H", "com.xyz.meta.I"); + void cycleAnnotatesSelf() { + assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.I").getNames()).containsOnly("com.xyz.meta.E", + "com.xyz.meta.B", "com.xyz.meta.H", "com.xyz.meta.I"); } /** * Names of meta annotations. */ @Test - public void namesOfMetaAnnotations() { - assertThat(scanResult.getAnnotationsOnClass("com.xyz.meta.A").getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.J", "com.xyz.meta.F"); - assertThat(scanResult.getAnnotationsOnClass("com.xyz.meta.C").getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.G"); + void namesOfMetaAnnotations() { + assertThat(scanResult.getAnnotationsOnClass("com.xyz.meta.A").getNames()).containsOnly("com.xyz.meta.J", + "com.xyz.meta.F"); + assertThat(scanResult.getAnnotationsOnClass("com.xyz.meta.C").getNames()).containsOnly("com.xyz.meta.G"); } /** * Union. */ @Test - public void union() { + void union() { assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.J") .union(scanResult.getClassesWithAnnotation("com.xyz.meta.G")).directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.E", "com.xyz.meta.F", "com.xyz.meta.C"); + .containsOnly("com.xyz.meta.E", "com.xyz.meta.F", "com.xyz.meta.C"); assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.I") - .union(scanResult.getClassesWithAnnotation("com.xyz.meta.J")).getNames()).containsExactlyInAnyOrder( + .union(scanResult.getClassesWithAnnotation("com.xyz.meta.J")).getNames()).containsOnly( "com.xyz.meta.A", "com.xyz.meta.B", "com.xyz.meta.F", "com.xyz.meta.E", "com.xyz.meta.H", "com.xyz.meta.I"); assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.I") .union(scanResult.getClassesWithAnnotation("com.xyz.meta.J")).directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.F", "com.xyz.meta.E", "com.xyz.meta.H"); + .containsOnly("com.xyz.meta.F", "com.xyz.meta.E", "com.xyz.meta.H"); } /** * Intersect. */ @Test - public void intersect() { + void intersect() { assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.I") .intersect(scanResult.getClassesWithAnnotation("com.xyz.meta.J")).getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.E", "com.xyz.meta.B"); + .containsOnly("com.xyz.meta.E", "com.xyz.meta.B"); assertThat(scanResult.getClassesWithAnnotation("com.xyz.meta.I") .intersect(scanResult.getClassesWithAnnotation("com.xyz.meta.J")).directOnly().getNames()) - .containsExactlyInAnyOrder("com.xyz.meta.E"); + .containsOnly("com.xyz.meta.E"); } } diff --git a/src/test/java/com/xyz/ScanEverything.java b/src/test/java/com/xyz/ScanEverything.java index e34eb86fc..327ba7405 100644 --- a/src/test/java/com/xyz/ScanEverything.java +++ b/src/test/java/com/xyz/ScanEverything.java @@ -1,16 +1,15 @@ package com.xyz; /** - * The Class ScanEverything. + * ScanEverything. */ public class ScanEverything { - // @Test // public void scanEverything() throws IOException { // final long t0 = System.nanoTime(); // try (ScanResult scanResult = new ClassGraph() // // // .verbose() // - // // .whitelistPackages("io.github") // + // // .acceptPackages("io.github") // // // .enableAllInfo() // // .enableSystemPackages() // // .disableJarScanning() // diff --git a/src/test/java/com/xyz/fig/Figure.java b/src/test/java/com/xyz/fig/Figure.java index 196e7cdff..5edfc623d 100644 --- a/src/test/java/com/xyz/fig/Figure.java +++ b/src/test/java/com/xyz/fig/Figure.java @@ -5,11 +5,10 @@ import com.xyz.fig.shape.Shape; /** - * The Class Figure. + * Figure. */ @UIWidget public class Figure implements Drawable { - /** The scene graph. */ SceneGraph sceneGraph = new SceneGraph(); diff --git a/src/test/java/com/xyz/fig/SceneGraph.java b/src/test/java/com/xyz/fig/SceneGraph.java index 6ea89d10a..0159885be 100644 --- a/src/test/java/com/xyz/fig/SceneGraph.java +++ b/src/test/java/com/xyz/fig/SceneGraph.java @@ -6,10 +6,9 @@ import com.xyz.fig.shape.Shape; /** - * The Class SceneGraph. + * SceneGraph. */ public class SceneGraph implements Drawable { - /** The shapes. */ ArrayList shapes = new ArrayList<>(); @@ -23,6 +22,12 @@ public void addShape(final Shape shape) { shapes.add(shape); } + /** + * Draw. + * + * @param g + * the g + */ /* (non-Javadoc) * @see com.xyz.fig.Drawable#draw(java.awt.Graphics2D) */ diff --git a/src/test/java/com/xyz/fig/shape/Circle.java b/src/test/java/com/xyz/fig/shape/Circle.java index 31c3f4035..9169d57b8 100644 --- a/src/test/java/com/xyz/fig/shape/Circle.java +++ b/src/test/java/com/xyz/fig/shape/Circle.java @@ -3,10 +3,9 @@ import java.awt.Graphics2D; /** - * The Class Circle. + * Circle. */ public class Circle extends ShapeImpl { - /** The r. */ private final float r; diff --git a/src/test/java/com/xyz/fig/shape/Diamond.java b/src/test/java/com/xyz/fig/shape/Diamond.java index f47faa178..26cb90422 100644 --- a/src/test/java/com/xyz/fig/shape/Diamond.java +++ b/src/test/java/com/xyz/fig/shape/Diamond.java @@ -3,10 +3,9 @@ import java.awt.Graphics2D; /** - * The Class Diamond. + * Diamond. */ public class Diamond extends ShapeImpl { - /** The w. */ private final float w; diff --git a/src/test/java/com/xyz/fig/shape/ShapeImpl.java b/src/test/java/com/xyz/fig/shape/ShapeImpl.java index 7cef26fbf..0f3730653 100644 --- a/src/test/java/com/xyz/fig/shape/ShapeImpl.java +++ b/src/test/java/com/xyz/fig/shape/ShapeImpl.java @@ -3,7 +3,7 @@ import java.awt.Graphics2D; /** - * The Class ShapeImpl. + * ShapeImpl. */ public abstract class ShapeImpl implements Shape { diff --git a/src/test/java/com/xyz/fig/shape/Square.java b/src/test/java/com/xyz/fig/shape/Square.java index 54c48be5b..42d0543ba 100644 --- a/src/test/java/com/xyz/fig/shape/Square.java +++ b/src/test/java/com/xyz/fig/shape/Square.java @@ -3,10 +3,9 @@ import java.awt.Graphics2D; /** - * The Class Square. + * Square. */ public class Square extends ShapeImpl { - /** The size. */ private final float size; diff --git a/src/test/java/com/xyz/fig/shape/Triangle.java b/src/test/java/com/xyz/fig/shape/Triangle.java index 5b5a0b83f..7f473c2a3 100644 --- a/src/test/java/com/xyz/fig/shape/Triangle.java +++ b/src/test/java/com/xyz/fig/shape/Triangle.java @@ -3,10 +3,9 @@ import java.awt.Graphics2D; /** - * The Class Triangle. + * Triangle. */ public class Triangle extends ShapeImpl { - /** The edge len. */ private final float edgeLen; diff --git a/src/test/java/com/xyz/meta/A.java b/src/test/java/com/xyz/meta/A.java index 031470537..a1f529a3d 100644 --- a/src/test/java/com/xyz/meta/A.java +++ b/src/test/java/com/xyz/meta/A.java @@ -1,7 +1,7 @@ package com.xyz.meta; /** - * The Class A. + * A. */ @F public class A { diff --git a/src/test/java/com/xyz/meta/B.java b/src/test/java/com/xyz/meta/B.java index a71d91a6a..e47650a14 100644 --- a/src/test/java/com/xyz/meta/B.java +++ b/src/test/java/com/xyz/meta/B.java @@ -1,7 +1,7 @@ package com.xyz.meta; /** - * The Class B. + * B. */ @E @F diff --git a/src/test/java/com/xyz/meta/C.java b/src/test/java/com/xyz/meta/C.java index 78cf92ca0..3a2dcceca 100644 --- a/src/test/java/com/xyz/meta/C.java +++ b/src/test/java/com/xyz/meta/C.java @@ -1,7 +1,7 @@ package com.xyz.meta; /** - * The Class C. + * C. */ @G public class C { diff --git a/src/test/java/io/github/classgraph/features/AnnotationEquality.java b/src/test/java/io/github/classgraph/features/AnnotationEqualityTest.java similarity index 70% rename from src/test/java/io/github/classgraph/features/AnnotationEquality.java rename to src/test/java/io/github/classgraph/features/AnnotationEqualityTest.java index 9b45c6380..b65d005f7 100644 --- a/src/test/java/io/github/classgraph/features/AnnotationEquality.java +++ b/src/test/java/io/github/classgraph/features/AnnotationEqualityTest.java @@ -6,7 +6,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.AnnotationInfo; import io.github.classgraph.ClassGraph; @@ -14,9 +14,9 @@ import io.github.classgraph.ScanResult; /** - * The Class AnnotationEquality. + * AnnotationEqualityTest. */ -public class AnnotationEquality { +class AnnotationEqualityTest { /** * The Interface W. */ @@ -72,7 +72,7 @@ int a() /** * The Class Y. */ - @X(b = 5, c = { Long.class, Integer.class, AnnotationEquality.class, W.class, X.class } + @X(b = 5, c = { Long.class, Integer.class, AnnotationEqualityTest.class, W.class, X.class } // , d = "xyz", e = 'w' ) private static class Y { @@ -82,21 +82,25 @@ private static class Y { * Test equality of JRE-instantiated Annotation with proxy instance instantiated by ClassGraph. */ @Test - public void annotationEquality() { + void annotationEquality() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(AnnotationEquality.class.getPackage().getName()).enableAllInfo().scan()) { + .acceptPackages(AnnotationEqualityTest.class.getPackage().getName()).enableAllInfo().scan()) { final ClassInfo classInfo = scanResult.getClassInfo(Y.class.getName()); assertThat(classInfo).isNotNull(); final Class cls = classInfo.loadClass(); - final Annotation annotation = cls.getAnnotations()[0]; + final X annotation = (X) cls.getAnnotations()[0]; assertThat(X.class.isInstance(annotation)); final AnnotationInfo annotationInfo = classInfo.getAnnotationInfo().get(0); - final Annotation proxyAnnotation = annotationInfo.loadClassAndInstantiate(); - assertThat(X.class.isInstance(proxyAnnotation)); + final Annotation proxyAnnotationGeneric = annotationInfo.loadClassAndInstantiate(); + assertThat(X.class.isInstance(proxyAnnotationGeneric)); + final X proxyAnnotation = (X) proxyAnnotationGeneric; + assertThat(proxyAnnotation.b()).isEqualTo(annotation.b()); + assertThat(proxyAnnotation.c()).isEqualTo(annotation.c()); assertThat(annotation.hashCode()).isEqualTo(proxyAnnotation.hashCode()); assertThat(annotation).isEqualTo(proxyAnnotation); - assertThat(annotation.toString()).isEqualTo(annotationInfo.toString()); - assertThat(annotation.toString()).isEqualTo(proxyAnnotation.toString()); + // Annotation::toString is implementation-dependent (#361) + // assertThat(annotation.toString()).isEqualTo(annotationInfo.toString()); + // assertThat(annotation.toString()).isEqualTo(proxyAnnotation.toString()); } } } diff --git a/src/test/java/io/github/classgraph/features/AnnotationParamWithPrimitiveTypedArray.java b/src/test/java/io/github/classgraph/features/AnnotationParamWithPrimitiveTypedArrayTest.java similarity index 93% rename from src/test/java/io/github/classgraph/features/AnnotationParamWithPrimitiveTypedArray.java rename to src/test/java/io/github/classgraph/features/AnnotationParamWithPrimitiveTypedArrayTest.java index 1ad8dfbf5..195cbf394 100644 --- a/src/test/java/io/github/classgraph/features/AnnotationParamWithPrimitiveTypedArray.java +++ b/src/test/java/io/github/classgraph/features/AnnotationParamWithPrimitiveTypedArrayTest.java @@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.Arrays; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.AnnotationInfo; import io.github.classgraph.AnnotationParameterValueList; @@ -14,10 +14,9 @@ import io.github.classgraph.ScanResult; /** - * The Class AnnotationParamWithPrimitiveTypedArray. + * AnnotationParamWithPrimitiveTypedArray. */ -public class AnnotationParamWithPrimitiveTypedArray { - +public class AnnotationParamWithPrimitiveTypedArrayTest { /** * The Interface NestedAnnotation. */ @@ -95,7 +94,7 @@ public abstract static class AnnotatedClass { @Test public void primitiveArrayParams() { try (ScanResult scanResult = new ClassGraph().enableAllInfo() - .whitelistPackages(AnnotationParamWithPrimitiveTypedArray.class.getPackage().getName()).scan()) { + .acceptPackages(AnnotationParamWithPrimitiveTypedArrayTest.class.getPackage().getName()).scan()) { final AnnotationInfo annotationInfo = scanResult.getClassInfo(AnnotatedClass.class.getName()) .getAnnotationInfo().get(0); final AnnotationParameterValueList annotationParams = annotationInfo.getParameterValues(); @@ -117,7 +116,7 @@ public void primitiveArrayParams() { assertThat(v2).isEqualTo(new String[] { "x" }); assertThat(v3).isEqualTo(new int[] {}); assertThat(Arrays.toString((Object[]) v4)) - .isEqualTo("[@" + NestedAnnotation.class.getName() + "(str=\"Test\", intArray=[9])]"); + .isEqualTo("[@" + NestedAnnotation.class.getName() + "(str=\"Test\", intArray={9})]"); final AnnotationWithPrimitiveArrayParams annotation = (AnnotationWithPrimitiveArrayParams) annotationInfo .loadClassAndInstantiate(); diff --git a/src/test/java/io/github/classgraph/features/ClassTypeAnnotation.java b/src/test/java/io/github/classgraph/features/ClassTypeAnnotation.java new file mode 100644 index 000000000..8e358e834 --- /dev/null +++ b/src/test/java/io/github/classgraph/features/ClassTypeAnnotation.java @@ -0,0 +1,116 @@ +package io.github.classgraph.features; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Test + */ +class ClassTypeAnnotation { + /***/ + @Retention(RetentionPolicy.RUNTIME) + @Target(value = { ElementType.TYPE_USE, ElementType.TYPE }) + private static @interface P { + } + + /***/ + @Retention(RetentionPolicy.RUNTIME) + @Target(value = { ElementType.TYPE_USE, ElementType.TYPE }) + private static @interface Q { + } + + /***/ + @Retention(RetentionPolicy.RUNTIME) + @Target(value = { ElementType.TYPE_USE, ElementType.TYPE }) + private static @interface R { + } + + /***/ + private static class Z { + } + + /***/ + private static interface A { + } + + /***/ + private static interface B { + } + + /***/ + private static class E extends @P Z implements @Q A, @R B { + } + + /***/ + private static class F extends @P Z implements @Q A, @R B { + } + + /***/ + private static class G extends @P Z implements @Q B, @R A { + } + + /***/ + private static class H extends @P Z { + } + + /***/ + private static class I implements @Q B, @R A { + } + + @Test + void classTypeAnnotation() { + try (ScanResult scanResult = new ClassGraph() + .acceptPackages(ClassTypeAnnotation.class.getPackage().getName()).enableAllInfo().scan()) { + + // Type with annotations should be rendered by toString() as + // Y extends ClassTypeAnnotation$@X Z + // and not + // Y extends @X ClassTypeAnnotation$Z + // because the annotation is on Z, not ClassTypeAnnotation + + assertThat(scanResult.getClassInfo(E.class.getName()).getTypeSignature().toString()) + .isEqualTo("private static class " + E.class.getName() + " extends " + + ClassTypeAnnotation.class.getName() + "$@" + P.class.getName() + " " + + Z.class.getSimpleName() + " implements " + ClassTypeAnnotation.class.getName() + "$@" + + Q.class.getName() + " " + A.class.getSimpleName() + ", " + + ClassTypeAnnotation.class.getName() + "$@" + R.class.getName() + " " + + B.class.getSimpleName()); + + assertThat(scanResult.getClassInfo(F.class.getName()).getTypeSignatureOrTypeDescriptor().toString()) + .isEqualTo("private static class " + F.class.getName() + " extends " + + ClassTypeAnnotation.class.getName() + "$@" + P.class.getName() + " " + + Z.class.getSimpleName() + " implements " + ClassTypeAnnotation.class.getName() + "$@" + + Q.class.getName() + " " + A.class.getSimpleName() + ", " + + ClassTypeAnnotation.class.getName() + "$@" + R.class.getName() + " " + + B.class.getSimpleName()); + + assertThat(scanResult.getClassInfo(G.class.getName()).getTypeSignatureOrTypeDescriptor().toString()) + .isEqualTo("private static class " + G.class.getName() + " extends " + + ClassTypeAnnotation.class.getName() + "$@" + P.class.getName() + " " + + Z.class.getSimpleName() + " implements " + ClassTypeAnnotation.class.getName() + "$@" + + Q.class.getName() + " " + B.class.getSimpleName() + ", " + + ClassTypeAnnotation.class.getName() + "$@" + R.class.getName() + " " + + A.class.getSimpleName()); + + assertThat(scanResult.getClassInfo(H.class.getName()).getTypeSignatureOrTypeDescriptor().toString()) + .isEqualTo("private static class " + H.class.getName() + " extends " + + ClassTypeAnnotation.class.getName() + "$@" + P.class.getName() + " " + + Z.class.getSimpleName()); + + assertThat(scanResult.getClassInfo(I.class.getName()).getTypeSignatureOrTypeDescriptor().toString()) + .isEqualTo("private static class " + I.class.getName() + " implements " + + ClassTypeAnnotation.class.getName() + "$@" + Q.class.getName() + " " + + B.class.getSimpleName() + ", " + ClassTypeAnnotation.class.getName() + "$@" + + R.class.getName() + " " + A.class.getSimpleName()); + } + } +} diff --git a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/RecyclableInflater.java b/src/test/java/io/github/classgraph/features/CustomURLScheme.java similarity index 53% rename from src/main/java/nonapi/io/github/classgraph/fastzipfilereader/RecyclableInflater.java rename to src/test/java/io/github/classgraph/features/CustomURLScheme.java index 4635f4bf4..4bb200b7f 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/RecyclableInflater.java +++ b/src/test/java/io/github/classgraph/features/CustomURLScheme.java @@ -26,39 +26,35 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE * OR OTHER DEALINGS IN THE SOFTWARE. */ -package nonapi.io.github.classgraph.fastzipfilereader; +package io.github.classgraph.features; -import java.util.zip.Inflater; - -import nonapi.io.github.classgraph.recycler.Recycler; -import nonapi.io.github.classgraph.recycler.Resettable; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.HashMap; +import java.util.Map; /** - * Wrapper class that allows an {@link Inflater} instance to be reset for reuse and then recycled by a - * {@link Recycler}. + * CustomURLScheme. */ -class RecyclableInflater implements Resettable, AutoCloseable { - /** Create a new {@link Inflater} instance with the "nowrap" option (which is needed for zipfile entries). */ - private final Inflater inflater = new Inflater(/* nowrap = */ true); - - /** - * Get the {@link Inflater} instance. - * - * @return the {@link Inflater} instance. - */ - public Inflater getInflater() { - return inflater; - } +public class CustomURLScheme { + /** URL scheme. */ + public static final String SCHEME = "customscheme"; - /** Called when an {@link Inflater} instance is recycled, to reset the inflater so it can accept new input. */ - @Override - public void reset() { - inflater.reset(); - } + /** Any URLs that were remapped. */ + public static Map remappedURLs = new HashMap<>(); - /** Called when the {@link Recycler} instance is closed, to destroy the {@link Inflater} instance. */ - @Override - public void close() { - inflater.end(); + static { + URL.setURLStreamHandlerFactory(protocol -> SCHEME.equals(protocol) ? new URLStreamHandler() { + @Override + protected URLConnection openConnection(final URL url) throws IOException { + // Record that the URL was remapped, so we know this custom URLStreamHandler was called + final String newURL = "file:" + url.getPath(); + remappedURLs.put(url.toString(), newURL); + // Replace scheme with "file://" + return new URL(newURL).openConnection(); + } + } : null); } } \ No newline at end of file diff --git a/src/test/java/io/github/classgraph/features/DeclaredVsNonDeclared.java b/src/test/java/io/github/classgraph/features/DeclaredVsNonDeclaredTest.java similarity index 73% rename from src/test/java/io/github/classgraph/features/DeclaredVsNonDeclared.java rename to src/test/java/io/github/classgraph/features/DeclaredVsNonDeclaredTest.java index cdf8630b7..4e81fbc35 100644 --- a/src/test/java/io/github/classgraph/features/DeclaredVsNonDeclared.java +++ b/src/test/java/io/github/classgraph/features/DeclaredVsNonDeclaredTest.java @@ -6,8 +6,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import org.assertj.core.api.iterable.Extractor; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.AnnotationInfo; import io.github.classgraph.AnnotationInfoList; @@ -37,10 +36,9 @@ } /** - * The Class DeclaredVsNonDeclared. + * DeclaredVsNonDeclared. */ -public class DeclaredVsNonDeclared { - +public class DeclaredVsNonDeclaredTest { /** * The Class A. */ @@ -123,16 +121,18 @@ public abstract static class C extends A { @Test public void declaredVsNonDeclaredMethods() { try (ScanResult scanResult = new ClassGraph().enableAllInfo() - .whitelistPackages(DeclaredVsNonDeclared.class.getPackage().getName()).scan()) { + .acceptPackages(DeclaredVsNonDeclaredTest.class.getPackage().getName()).scan()) { final ClassInfo A = scanResult.getClassInfo(A.class.getName()); final ClassInfo B = scanResult.getClassInfo(B.class.getName()); assertThat(B.getFieldInfo("x").getClassInfo().getName()).isEqualTo(B.class.getName()); assertThat(B.getFieldInfo("z").getClassInfo().getName()).isEqualTo(A.class.getName()); assertThat(A.getFieldInfo().get(0).getTypeDescriptor().toString()).isEqualTo("float"); assertThat(B.getFieldInfo().get(0).getTypeDescriptor().toString()).isEqualTo("int"); - assertThat(B.getMethodInfo().toString()).isEqualTo( - "[void y(int, int), void w(), abstract void y(java.lang.String), abstract void y(java.lang.Integer)]"); - assertThat(B.getDeclaredMethodInfo().toString()).isEqualTo("[void y(int, int), void w()]"); + assertThat(B.getMethodInfo().toString()) + .isEqualTo("[void y(final int x, final int y), void w(), abstract void y(java.lang.String x), " + + "abstract void y(java.lang.Integer x)]"); + assertThat(B.getDeclaredMethodInfo().toString()) + .isEqualTo("[void y(final int x, final int y), void w()]"); } } @@ -141,15 +141,8 @@ public void declaredVsNonDeclaredMethods() { */ @Test public void annotationInfosShouldBeAbleToDifferentiateBetweenDirectAndReachable() { - final Extractor annotationNameExtractor = new Extractor() { - @Override - public Object extract(final AnnotationInfo input) { - return input.getName(); - } - }; - try (ScanResult scanResult = new ClassGraph().enableAllInfo() - .whitelistPackages(DeclaredVsNonDeclared.class.getPackage().getName()).scan()) { + .acceptPackages(DeclaredVsNonDeclaredTest.class.getPackage().getName()).scan()) { final ClassInfo A = scanResult.getClassInfo(A.class.getName()); final ClassInfo B = scanResult.getClassInfo(B.class.getName()); final ClassInfo C = scanResult.getClassInfo(C.class.getName()); @@ -157,31 +150,30 @@ public Object extract(final AnnotationInfo input) { final AnnotationInfoList annotationInfossOnA = A.getAnnotationInfo(); final AnnotationInfoList annotationsInfosOnB = B.getAnnotationInfo(); - assertThat(annotationInfossOnA).extracting(annotationNameExtractor).containsExactlyInAnyOrder( + assertThat(annotationInfossOnA).extracting(AnnotationInfo::getName).containsOnly( InheritedAnnotation.class.getName(), NormalAnnotation.class.getName(), InheritedMetaAnnotation.class.getName(), NonInheritedMetaAnnotation.class.getName()); - assertThat(annotationsInfosOnB).extracting(annotationNameExtractor).containsExactlyInAnyOrder( - InheritedAnnotation.class.getName(), InheritedMetaAnnotation.class.getName()); - assertThat(annotationInfossOnA.directOnly()).extracting(annotationNameExtractor) - .containsExactlyInAnyOrder(NormalAnnotation.class.getName(), - InheritedAnnotation.class.getName()); + assertThat(annotationsInfosOnB).extracting(AnnotationInfo::getName) + .containsOnly(InheritedAnnotation.class.getName(), InheritedMetaAnnotation.class.getName()); + assertThat(annotationInfossOnA.directOnly()).extracting(AnnotationInfo::getName) + .containsOnly(NormalAnnotation.class.getName(), InheritedAnnotation.class.getName()); assertThat(annotationsInfosOnB.directOnly()).isEmpty(); - assertThat(C.getAnnotationInfo().directOnly()).extracting(annotationNameExtractor) - .containsExactlyInAnyOrder(NormalAnnotation.class.getName()); + assertThat(C.getAnnotationInfo().directOnly()).extracting(AnnotationInfo::getName) + .containsOnly(NormalAnnotation.class.getName()); final AnnotationInfoList annotationsOnAw = A.getMethodInfo().getSingleMethod("w").getAnnotationInfo(); - assertThat(annotationsOnAw).extracting(annotationNameExtractor).containsExactlyInAnyOrder( + assertThat(annotationsOnAw).extracting(AnnotationInfo::getName).containsOnly( InheritedAnnotation.class.getName(), InheritedMetaAnnotation.class.getName(), NonInheritedMetaAnnotation.class.getName()); final AnnotationInfoList annotationsOnBw = B.getMethodInfo().getSingleMethod("w").getAnnotationInfo(); - assertThat(annotationsOnBw).extracting(annotationNameExtractor).isEmpty(); + assertThat(annotationsOnBw).extracting(AnnotationInfo::getName).isEmpty(); // See note on inherited annotations on methods // https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Inherited.html // "Note that this (@Inherited) meta-annotation type has no effect if the annotated type is used to // annotate anything other than a class. Note also that this meta-annotation only causes annotations // to be inherited from superclasses; annotations on implemented interfaces have no effect." - assertThat(annotationsOnBw.directOnly()).extracting(annotationNameExtractor).isEmpty(); + assertThat(annotationsOnBw.directOnly()).extracting(AnnotationInfo::getName).isEmpty(); } } @@ -191,18 +183,18 @@ public Object extract(final AnnotationInfo input) { @Test public void annotationsShouldBeAbleToDifferentiateBetweenDirectAndReachable() { try (ScanResult scanResult = new ClassGraph().enableAllInfo() - .whitelistPackages(DeclaredVsNonDeclared.class.getPackage().getName()).scan()) { + .acceptPackages(DeclaredVsNonDeclaredTest.class.getPackage().getName()).scan()) { final ClassInfo A = scanResult.getClassInfo(A.class.getName()); final ClassInfo B = scanResult.getClassInfo(B.class.getName()); final ClassInfoList annotationsOnA = A.getAnnotations(); final ClassInfoList annotationsOnB = B.getAnnotations(); - assertThat(annotationsOnA.loadClasses()).containsExactlyInAnyOrder(NormalAnnotation.class, - InheritedAnnotation.class, InheritedMetaAnnotation.class, NonInheritedMetaAnnotation.class); - assertThat(annotationsOnB.loadClasses()).containsExactlyInAnyOrder(InheritedAnnotation.class, + assertThat(annotationsOnA.loadClasses()).containsOnly(NormalAnnotation.class, InheritedAnnotation.class, + InheritedMetaAnnotation.class, NonInheritedMetaAnnotation.class); + assertThat(annotationsOnB.loadClasses()).containsOnly(InheritedAnnotation.class, InheritedMetaAnnotation.class); - assertThat(annotationsOnA.directOnly().loadClasses()).containsExactlyInAnyOrder(NormalAnnotation.class, + assertThat(annotationsOnA.directOnly().loadClasses()).containsOnly(NormalAnnotation.class, InheritedAnnotation.class); assertThat(annotationsOnB.directOnly()).isEmpty(); } @@ -214,7 +206,7 @@ public void annotationsShouldBeAbleToDifferentiateBetweenDirectAndReachable() { @Test public void loadFieldAndMethod() { try (ScanResult scanResult = new ClassGraph().enableAllInfo() - .whitelistPackages(DeclaredVsNonDeclared.class.getPackage().getName()).scan()) { + .acceptPackages(DeclaredVsNonDeclaredTest.class.getPackage().getName()).scan()) { final ClassInfo B = scanResult.getClassInfo(B.class.getName()); assertThat(B.getFieldInfo("x").loadClassAndGetField().getName()).isEqualTo("x"); assertThat(B.getMethodInfo("y").get(0).loadClassAndGetMethod().getName()).isEqualTo("y"); diff --git a/src/test/java/io/github/classgraph/features/EncapsulationCircumventionTest.java b/src/test/java/io/github/classgraph/features/EncapsulationCircumventionTest.java new file mode 100644 index 000000000..2acd456a7 --- /dev/null +++ b/src/test/java/io/github/classgraph/features/EncapsulationCircumventionTest.java @@ -0,0 +1,37 @@ +package io.github.classgraph.features; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassGraph.CircumventEncapsulationMethod; +import io.github.classgraph.ScanResult; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; + +/** + * Encapsulation circumvention test. + */ +class EncapsulationCircumventionTest { + /** Reset encapsulation circumvention method after each test. */ + @AfterEach + void resetAfterEachTest() { + ClassGraph.CIRCUMVENT_ENCAPSULATION = CircumventEncapsulationMethod.NONE; + } + + /** Test Narcissus. */ + @Test + void testNarcissus() { + ClassGraph.CIRCUMVENT_ENCAPSULATION = CircumventEncapsulationMethod.NARCISSUS; + final ReflectionUtils reflectionUtils = new ReflectionUtils(); + assertThat( + reflectionUtils.getFieldVal(true, reflectionUtils, "reflectionDriver").getClass().getSimpleName()) + .isEqualTo("NarcissusReflectionDriver"); + try (ScanResult scanResult = new ClassGraph() + .acceptPackages(EncapsulationCircumventionTest.class.getPackage().getName()).enableAllInfo() + .scan()) { + assertThat(scanResult.getAllClasses().getNames()).isNotEmpty(); + } + } +} diff --git a/src/test/java/io/github/classgraph/features/EnumTest.java b/src/test/java/io/github/classgraph/features/EnumTest.java new file mode 100644 index 000000000..edab8718a --- /dev/null +++ b/src/test/java/io/github/classgraph/features/EnumTest.java @@ -0,0 +1,59 @@ +package io.github.classgraph.features; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; + +/** + * Test. + */ +public class EnumTest { + /** Regular enum */ + private static enum MyEnumWithoutMethod { + A, B, C; + } + + private static enum EnumWithMethod { + P(1), Q(2); + + int val; + + EnumWithMethod(final int val) { + this.val = val; + } + + int getVal() { + return val; + } + }; + + /** Test regular enum */ + @Test + public void enumWithoutMethod() throws Exception { + try (ScanResult scanResult = new ClassGraph().acceptClasses(MyEnumWithoutMethod.class.getName()) + .enableAllInfo().scan()) { + assertThat(scanResult.getAllEnums().size() == 1); + final ClassInfo myEnum = scanResult.getAllEnums().get(0); + assertThat(myEnum.getName().equals(MyEnumWithoutMethod.class.getName())); + assertThat(myEnum.getEnumConstants().getNames()).containsExactly("A", "B", "C"); + } + } + + /** Test enum with method */ + @Test + public void enumWithMethod() throws Exception { + try (ScanResult scanResult = new ClassGraph().acceptClasses(EnumWithMethod.class.getName()).enableAllInfo() + .scan()) { + assertThat(scanResult.getAllEnums().size() == 1); + final ClassInfo myEnum = scanResult.getAllEnums().get(0); + assertThat(myEnum.getName().equals(EnumWithMethod.class.getName())); + assertThat(myEnum.getEnumConstants().getNames()).containsExactly("P", "Q"); + assertThat(((EnumWithMethod) myEnum.getEnumConstantObjects().get(0)).getVal()).isEqualTo(1); + assertThat(((EnumWithMethod) myEnum.getEnumConstantObjects().get(1)).getVal()).isEqualTo(2); + } + } +} diff --git a/src/test/java/io/github/classgraph/features/MethodParameterAnnotations.java b/src/test/java/io/github/classgraph/features/MethodParameterAnnotationsTest.java similarity index 77% rename from src/test/java/io/github/classgraph/features/MethodParameterAnnotations.java rename to src/test/java/io/github/classgraph/features/MethodParameterAnnotationsTest.java index 2c528eb97..9373a1d7c 100644 --- a/src/test/java/io/github/classgraph/features/MethodParameterAnnotations.java +++ b/src/test/java/io/github/classgraph/features/MethodParameterAnnotationsTest.java @@ -5,15 +5,15 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class AnnotationEquality. + * AnnotationEquality. */ -public class MethodParameterAnnotations { +public class MethodParameterAnnotationsTest { /** * The Annotation W. */ @@ -32,6 +32,13 @@ public class MethodParameterAnnotations { * The Class Y. */ private abstract static class Y { + + /** + * W. + * + * @param w + * the w + */ abstract void w(@W int w); } @@ -39,6 +46,13 @@ private abstract static class Y { * The Class Z. */ private abstract static class Z { + + /** + * X. + * + * @param x + * the x + */ abstract void x(@X int x); } @@ -48,15 +62,15 @@ private abstract static class Z { @Test public void annotationEquality() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(MethodParameterAnnotations.class.getPackage().getName()).enableAllInfo() + .acceptPackages(MethodParameterAnnotationsTest.class.getPackage().getName()).enableAllInfo() .scan()) { assertThat(scanResult.getClassInfo(Y.class.getName()).getMethodParameterAnnotations().getNames()) .containsOnly(W.class.getName()); assertThat(scanResult.getClassInfo(Z.class.getName()).getMethodParameterAnnotations().getNames()) .containsOnly(X.class.getName()); - assertThat(scanResult.getClassesWithMethodParameterAnnotation(W.class.getName()).getNames()) + assertThat(scanResult.getClassesWithMethodParameterAnnotation(W.class).getNames()) .containsOnly(Y.class.getName()); - assertThat(scanResult.getClassesWithMethodParameterAnnotation(X.class.getName()).getNames()) + assertThat(scanResult.getClassesWithMethodParameterAnnotation(X.class).getNames()) .containsOnly(Z.class.getName()); } } diff --git a/src/test/java/io/github/classgraph/features/MultiReleaseJar.java b/src/test/java/io/github/classgraph/features/MultiReleaseJar.java deleted file mode 100644 index 0f43f87c5..000000000 --- a/src/test/java/io/github/classgraph/features/MultiReleaseJar.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.github.classgraph.features; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.net.URL; -import java.net.URLClassLoader; - -import org.junit.Test; - -import io.github.classgraph.ClassGraph; -import io.github.classgraph.ClassInfo; -import io.github.classgraph.Resource; -import io.github.classgraph.ResourceList; -import io.github.classgraph.ResourceList.ByteArrayConsumer; -import io.github.classgraph.ScanResult; -import nonapi.io.github.classgraph.utils.VersionFinder; - -/** - * The Class MultiReleaseJar. - */ -public class MultiReleaseJar { - - /** The Constant jarURL. */ - private static final URL jarURL = MultiReleaseJar.class.getClassLoader().getResource("multi-release-jar.jar"); - - /** - * Multi release jar. - */ - @Test - public void multiReleaseJar() throws Exception { - if (VersionFinder.JAVA_MAJOR_VERSION < 9) { - // Multi-release jar sections are ignored by ClassGraph if JDK<9 - System.out.println("Skipping test, as JDK version is less than 9"); - } else { - try (ScanResult scanResult = new ClassGraph() - .overrideClassLoaders(new URLClassLoader(new URL[] { jarURL })).enableAllInfo().scan()) { - final ClassInfo classInfo = scanResult.getClassInfo("mrj.Cls"); - assertThat(classInfo).isNotNull(); - final Class cls = classInfo.loadClass(); - final Method getVersionStatic = cls.getMethod("getVersionStatic"); - getVersionStatic.setAccessible(true); - assertThat(getVersionStatic.invoke(null)).isEqualTo(9); - final Constructor constructor = cls.getConstructor(); - constructor.setAccessible(true); - assertThat(constructor).isNotNull(); - final Object clsInstance = constructor.newInstance(); - assertThat(clsInstance).isNotNull(); - final Method getVersion = cls.getMethod("getVersion"); - getVersion.setAccessible(true); - assertThat(getVersion.invoke(clsInstance)).isEqualTo(9); - - final ResourceList resources = scanResult.getResourcesWithPath("resource.txt"); - assertThat(resources.size()).isEqualTo(1); - resources.forEachByteArray(new ByteArrayConsumer() { - @Override - public void accept(final Resource resource, final byte[] byteArray) { - assertThat(new String(byteArray).trim()).isEqualTo("9"); - } - }); - } - } - } - - /** - * Multi release versioning of resources. - */ - @Test - public void multiReleaseVersioningOfResources() throws Exception { - if (VersionFinder.JAVA_MAJOR_VERSION < 9) { - // Multi-release jar sections are ignored by ClassGraph if JDK<9 - System.out.println("Skipping test, as JDK version is less than 9"); - } else { - try (ScanResult scanResult = new ClassGraph() - .overrideClassLoaders(new URLClassLoader(new URL[] { jarURL })) - .whitelistPaths("nonexistent_path").scan()) { - assertThat(scanResult.getResourcesWithPath("mrj/Cls.class")).isEmpty(); - assertThat(scanResult.getResourcesWithPathIgnoringWhitelist("mrj/Cls.class")).isNotEmpty(); - } - } - } -} diff --git a/src/test/java/io/github/classgraph/features/MultiReleaseJarTest.java b/src/test/java/io/github/classgraph/features/MultiReleaseJarTest.java new file mode 100644 index 000000000..7ca13b4ab --- /dev/null +++ b/src/test/java/io/github/classgraph/features/MultiReleaseJarTest.java @@ -0,0 +1,137 @@ +package io.github.classgraph.features; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ResourceList; +import io.github.classgraph.ScanResult; +import nonapi.io.github.classgraph.utils.VersionFinder; + +/** + * MultiReleaseJar. + */ +public class MultiReleaseJarTest { + /** The jar URL. */ + private static final URL jarURL = MultiReleaseJarTest.class.getClassLoader() + .getResource("multi-release-jar.jar"); + + /** + * Multi release jar. + * + * @throws Exception + * the exception + */ + @Test + public void multiReleaseJar() throws Exception { + if (VersionFinder.JAVA_MAJOR_VERSION < 9) { + // Multi-release jar sections are ignored by ClassGraph if JDK<9 + System.out.println("Skipping multi-release jar test, as JDK version " + VersionFinder.JAVA_VERSION + + " is less than 9"); + } else { + try (ScanResult scanResult = new ClassGraph() + .overrideClassLoaders(new URLClassLoader(new URL[] { jarURL })).enableAllInfo().scan()) { + final ClassInfo classInfo = scanResult.getClassInfo("mrj.Cls"); + assertThat(classInfo).isNotNull(); + final Class cls = classInfo.loadClass(); + final Method getVersionStatic = cls.getMethod("getVersionStatic"); + getVersionStatic.setAccessible(true); + assertThat(getVersionStatic.invoke(null)).isEqualTo(9); + final Constructor constructor = cls.getConstructor(); + constructor.setAccessible(true); + assertThat(constructor).isNotNull(); + final Object clsInstance = constructor.newInstance(); + assertThat(clsInstance).isNotNull(); + final Method getVersion = cls.getMethod("getVersion"); + getVersion.setAccessible(true); + assertThat(getVersion.invoke(clsInstance)).isEqualTo(9); + + final ResourceList resources = scanResult.getResourcesWithPath("resource.txt"); + assertThat(resources.size()).isEqualTo(1); + resources.forEachByteArrayThrowingIOException( + (resource, byteArray) -> assertThat(new String(byteArray).trim()).isEqualTo("9")); + } + } + } + + /** + * Multi release versioning of resources. + * + * @throws Exception + * the exception + */ + @Test + public void multiReleaseVersioningOfResources() throws Exception { + // Multi-release jar sections are ignored by ClassGraph if JDK<9 + if (VersionFinder.JAVA_MAJOR_VERSION >= 9) { + try (ScanResult scanResult = new ClassGraph() + .overrideClassLoaders(new URLClassLoader(new URL[] { jarURL })).acceptPaths("nonexistent_path") + .scan()) { + assertThat(scanResult.getResourcesWithPath("mrj/Cls.class")).isEmpty(); + assertThat(scanResult.getResourcesWithPathIgnoringAccept("mrj/Cls.class")).isNotEmpty(); + } + } + } + + /** + * Loading all versions of multi release class and text resources with `enableMultiReleaseVersions`. + * + * @throws Exception + * the exception + */ + @Test + public void enableMultiReleaseVersions() throws Exception { + // Multi-release jar sections are ignored by ClassGraph if JDK<9 + if (VersionFinder.JAVA_MAJOR_VERSION >= 9) { + try (ScanResult scanResult = new ClassGraph() + .overrideClassLoaders(new URLClassLoader(new URL[] { jarURL })).enableMultiReleaseVersions() + .scan()) { + final ResourceList java8ClassResource = scanResult.getResourcesWithPath("mrj/Cls.class"); + assertThat(java8ClassResource).hasSize(1); + final ResourceList java9ClassResource = scanResult + .getResourcesWithPath("META-INF/versions/9/mrj/Cls.class"); + assertThat(java9ClassResource).hasSize(1); + assertThat(java8ClassResource.get(0).load()).isNotEqualTo(java9ClassResource.get(0).load()); + + final ResourceList java8Resource = scanResult.getResourcesWithPath("resource.txt"); + assertThat(java8Resource.size()).isEqualTo(1); + java8Resource.forEachByteArrayThrowingIOException( + (resource, byteArray) -> assertThat(new String(byteArray).trim()).isEqualTo("8")); + final ResourceList java9Resource = scanResult + .getResourcesWithPath("META-INF/versions/9/resource.txt"); + assertThat(java9Resource.size()).isEqualTo(1); + java9Resource.forEachByteArrayThrowingIOException( + (resource, byteArray) -> assertThat(new String(byteArray).trim()).isEqualTo("9")); + } + } + } + + /** + * `enableMultiReleaseVersions` does not make sense with class info and should disable it. + * + * @throws Exception + * the exception + */ + @Test + public void enableMultiReleaseVersionsWithClassInfo() throws Exception { + // Multi-release jar sections are ignored by ClassGraph if JDK<9 + if (VersionFinder.JAVA_MAJOR_VERSION >= 9) { + try (ScanResult scanResult = new ClassGraph() + .overrideClassLoaders(new URLClassLoader(new URL[] { jarURL })).enableAllInfo() + .enableMultiReleaseVersions().scan()) { + final ResourceList java8ClassResource = scanResult.getResourcesWithPath("mrj/Cls.class"); + assertThat(java8ClassResource).hasSize(1); + assertThatThrownBy(() -> scanResult.getClassInfo("mrj.Cls")) + .isInstanceOfAny(IllegalArgumentException.class); + } + } + } +} diff --git a/src/test/java/io/github/classgraph/features/RecordTest.java b/src/test/java/io/github/classgraph/features/RecordTest.java new file mode 100644 index 000000000..f066b70bc --- /dev/null +++ b/src/test/java/io/github/classgraph/features/RecordTest.java @@ -0,0 +1,40 @@ +package io.github.classgraph.features; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.FieldInfo; +import io.github.classgraph.ScanResult; + +/** + * MultiReleaseJar. + */ +public class RecordTest { + /** The jar URL. */ + private static final URL jarURL = RecordTest.class.getClassLoader().getResource("record.jar"); + + /** + * Test records (JDK 14+). + * + * @throws Exception + * the exception + */ + @Test + public void recordJar() throws Exception { + try (ScanResult scanResult = new ClassGraph().overrideClassLoaders(new URLClassLoader(new URL[] { jarURL })) + .enableAllInfo().scan()) { + final ClassInfoList classInfoList = scanResult.getAllRecords(); + assertThat(classInfoList).isNotEmpty(); + final ClassInfo classInfo = classInfoList.get(0); + final FieldInfo fieldInfo = classInfo.getFieldInfo("x"); + assertThat(fieldInfo).isNotNull(); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/DeclaredVsNonDeclaredTest.java b/src/test/java/io/github/classgraph/issues/DeclaredVsNonDeclaredTest.java new file mode 100644 index 000000000..e7007664d --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/DeclaredVsNonDeclaredTest.java @@ -0,0 +1,219 @@ +package io.github.classgraph.issues; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.FieldInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.ScanResult; + +/** + * Test. + */ +public class DeclaredVsNonDeclaredTest { + /** + * SuperClass. + */ + public static class SuperClass { + /** Public superclass field. */ + public int publicSuperClassField; + + /** Private superclass field. */ + @SuppressWarnings("unused") + private int privateSuperClassField; + + /** + * Public superclass method. + */ + public void publicSuperClassMethod() { + } + + /** + * Private superclass method. + */ + @SuppressWarnings("unused") + private void privateSuperClassMethod() { + } + } + + /** + * SubClass. + */ + public static class SubClass extends SuperClass { + /** Public subclass field. */ + public int publicSubClassField; + + /** Private subclass field. */ + @SuppressWarnings("unused") + private int privateSubClassField; + + /** + * Public subclass method. + */ + public void publicSubClassMethod() { + } + + /** + * Private subclass method. + */ + @SuppressWarnings("unused") + private void privateSubClassMethod() { + } + } + + /** + * Compare results. + * + * @param superClassInfo + * the superclass info + * @param subClassInfo + * the subclass info + * @param ignoreVisibility + * whether or not to ignore method and field visibility + */ + private void compareResults(final ClassInfo superClassInfo, final ClassInfo subClassInfo, + final boolean ignoreVisibility) { + final Predicate filterOutClassMethods = name -> !name.equals("wait") && !name.equals("equals") + && !name.equals("toString") && !name.equals("hashCode") && !name.equals("getClass") + && !name.equals("notify") && !name.equals("notifyAll"); + + // METHODS + + final Function> getClassGraphMethodNames = classInfo -> classInfo.getMethodInfo() + .stream().map(MethodInfo::getName).collect(Collectors.toList()); + + final Function> getClassGraphDeclaredMethodNames = classInfo -> classInfo + .getDeclaredMethodInfo().stream().map(MethodInfo::getName).collect(Collectors.toList()); + + final Function, String[]> getClassMethodNames = clazz -> Arrays.stream(clazz.getMethods()) + .map(Method::getName).filter(filterOutClassMethods).collect(Collectors.toList()) + .toArray(new String[0]); + + final Function, String[]> getClassDeclaredMethodNames = clazz -> Arrays + .stream(clazz.getDeclaredMethods()).map(Method::getName).filter(filterOutClassMethods) + .collect(Collectors.toList()).toArray(new String[0]); + + // Non-"declared" methods, superclass + + assertThat(getClassGraphMethodNames.apply(superClassInfo)).containsExactlyInAnyOrder( + ignoreVisibility ? new String[] { "publicSuperClassMethod", "privateSuperClassMethod" } + : new String[] { "publicSuperClassMethod" }); + assertThat(getClassMethodNames.apply(SuperClass.class)).containsExactlyInAnyOrder("publicSuperClassMethod"); + + // Non-"declared" methods, subclass + + assertThat(getClassGraphMethodNames.apply(subClassInfo)).containsExactlyInAnyOrder(ignoreVisibility + ? new String[] { "publicSuperClassMethod", "publicSubClassMethod", "privateSuperClassMethod", + "privateSubClassMethod" } + : new String[] { "publicSuperClassMethod", "publicSubClassMethod" }); + assertThat(getClassMethodNames.apply(SubClass.class)).containsExactlyInAnyOrder("publicSuperClassMethod", + "publicSubClassMethod"); + + // "Declared" methods, superclass + + assertThat(getClassGraphDeclaredMethodNames.apply(superClassInfo)).containsExactlyInAnyOrder( + ignoreVisibility ? new String[] { "publicSuperClassMethod", "privateSuperClassMethod" } + : new String[] { "publicSuperClassMethod" }); + assertThat(getClassDeclaredMethodNames.apply(SuperClass.class)) + .containsExactlyInAnyOrder("publicSuperClassMethod", "privateSuperClassMethod"); + + // "Declared" methods, subclass + + assertThat(getClassGraphDeclaredMethodNames.apply(subClassInfo)).containsExactlyInAnyOrder( + ignoreVisibility ? new String[] { "publicSubClassMethod", "privateSubClassMethod" } + : new String[] { "publicSubClassMethod" }); + assertThat(getClassDeclaredMethodNames.apply(SubClass.class)) + .containsExactlyInAnyOrder("publicSubClassMethod", "privateSubClassMethod"); + + // FIELDS + + final Function> getClassGraphFieldNames = classInfo -> classInfo.getFieldInfo() + .stream().map(FieldInfo::getName).collect(Collectors.toList()); + + final Function> getClassGraphDeclaredFieldNames = classInfo -> classInfo + .getDeclaredFieldInfo().stream().map(FieldInfo::getName).collect(Collectors.toList()); + + final Function, List> getClassFieldNames = clazz -> Arrays.stream(clazz.getFields()) + .map(Field::getName).filter(filterOutClassMethods).collect(Collectors.toList()); + + final Function, List> getClassDeclaredFieldNames = clazz -> Arrays + .stream(clazz.getDeclaredFields()).map(Field::getName).filter(filterOutClassMethods) + .collect(Collectors.toList()); + + // Non-"declared" fields, superclass + + assertThat(getClassGraphFieldNames.apply(superClassInfo)).containsExactlyInAnyOrder( + ignoreVisibility ? new String[] { "publicSuperClassField", "privateSuperClassField" } + : new String[] { "publicSuperClassField" }); + assertThat(getClassFieldNames.apply(SuperClass.class)).containsExactlyInAnyOrder("publicSuperClassField"); + + // Non-"declared" fields, subclass + + assertThat(getClassGraphFieldNames.apply(subClassInfo)) + .containsExactlyInAnyOrder(ignoreVisibility + ? new String[] { "publicSuperClassField", "publicSubClassField", "privateSuperClassField", + "privateSubClassField" } + : new String[] { "publicSuperClassField", "publicSubClassField" }); + assertThat(getClassFieldNames.apply(SubClass.class)).containsExactlyInAnyOrder("publicSuperClassField", + "publicSubClassField"); + + // "Declared" fields, superclass + + assertThat(getClassGraphDeclaredFieldNames.apply(superClassInfo)).containsExactlyInAnyOrder( + ignoreVisibility ? new String[] { "publicSuperClassField", "privateSuperClassField" } + : new String[] { "publicSuperClassField" }); + assertThat(getClassDeclaredFieldNames.apply(SuperClass.class)) + .containsExactlyInAnyOrder("publicSuperClassField", "privateSuperClassField"); + + // "Declared" fields, subclass + + assertThat(getClassGraphDeclaredFieldNames.apply(subClassInfo)).containsExactlyInAnyOrder( + ignoreVisibility ? new String[] { "publicSubClassField", "privateSubClassField" } + : new String[] { "publicSubClassField" }); + assertThat(getClassDeclaredFieldNames.apply(SubClass.class)) + .containsExactlyInAnyOrder("publicSubClassField", "privateSubClassField"); + } + + /** + * Test ClassGraph's "declared" vs. non-"declared" method/field retrieval against the Java reflection API, + * without calling {@link ClassGraph#ignoreMethodVisibility()} or {@link ClassGraph#ignoreFieldVisibility()}. + */ + @Test + public void publicDeclaredVsNonDeclared() { + try (ScanResult scanResult = new ClassGraph().enableClassInfo() // + .enableMethodInfo() // + .enableFieldInfo() // + .acceptPackages(DeclaredVsNonDeclaredTest.class.getPackage().getName()).scan()) { + final ClassInfo superClassInfo = scanResult.getClassInfo(SuperClass.class.getName()); + final ClassInfo subClassInfo = scanResult.getClassInfo(SubClass.class.getName()); + compareResults(superClassInfo, subClassInfo, /* ignoreVisibility = */ false); + } + } + + /** + * Test ClassGraph's "declared" vs. non-"declared" method/field retrieval against the Java reflection API, + * without calling {@link ClassGraph#ignoreMethodVisibility()} or {@link ClassGraph#ignoreFieldVisibility()}. + */ + @Test + public void publicAndPrivateDeclaredVsNonDeclared() { + try (ScanResult scanResult = new ClassGraph().enableClassInfo() // + .enableMethodInfo().ignoreMethodVisibility() // + .enableFieldInfo().ignoreFieldVisibility() // + .acceptPackages(DeclaredVsNonDeclaredTest.class.getPackage().getName()).scan()) { + final ClassInfo superClassInfo = scanResult.getClassInfo(SuperClass.class.getName()); + final ClassInfo subClassInfo = scanResult.getClassInfo(SubClass.class.getName()); + compareResults(superClassInfo, subClassInfo, /* ignoreVisibility = */ true); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/GenericInnerClassTypedField.java b/src/test/java/io/github/classgraph/issues/GenericInnerClassTypedField.java index 0feecfe70..ad08d5c66 100644 --- a/src/test/java/io/github/classgraph/issues/GenericInnerClassTypedField.java +++ b/src/test/java/io/github/classgraph/issues/GenericInnerClassTypedField.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassRefTypeSignature; @@ -10,10 +10,9 @@ import io.github.classgraph.ScanResult; /** - * The Class GenericInnerClassTypedField. + * GenericInnerClassTypedField. */ public class GenericInnerClassTypedField { - /** * The Class A. * @@ -40,14 +39,13 @@ private class B { @Test public void testGenericInnerClassTypedField() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(GenericInnerClassTypedField.class.getPackage().getName()).enableAllInfo() - .scan()) { + .acceptPackages(GenericInnerClassTypedField.class.getPackage().getName()).enableAllInfo().scan()) { final FieldInfoList fields = scanResult.getClassInfo(GenericInnerClassTypedField.class.getName()) .getFieldInfo(); final ClassRefTypeSignature classRefTypeSignature = (ClassRefTypeSignature) fields.get(0) .getTypeSignature(); assertThat(classRefTypeSignature.toString()).isEqualTo( - A.class.getName() + "<" + Integer.class.getName() + ", " + String.class.getName() + ">.B"); + A.class.getName() + "<" + Integer.class.getName() + ", " + String.class.getName() + ">$B"); assertThat(classRefTypeSignature.getFullyQualifiedClassName()).isEqualTo(A.class.getName() + "$B"); } } diff --git a/src/test/java/io/github/classgraph/issues/IssuesTest.java b/src/test/java/io/github/classgraph/issues/IssuesTest.java index 771d1ffc7..6a8291f5a 100644 --- a/src/test/java/io/github/classgraph/issues/IssuesTest.java +++ b/src/test/java/io/github/classgraph/issues/IssuesTest.java @@ -2,28 +2,26 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; +import io.github.classgraph.test.accepted.Impl1; +import io.github.classgraph.test.accepted.Impl1Sub; import io.github.classgraph.test.external.ExternalSuperclass; import io.github.classgraph.test.internal.InternalExtendsExternal; -import io.github.classgraph.test.whitelisted.Impl1; -import io.github.classgraph.test.whitelisted.Impl1Sub; /** - * The Class IssuesTest. + * IssuesTest. */ public class IssuesTest { - /** * Issue 70. */ @Test public void issue70() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Impl1.class.getPackage().getName()) - .scan()) { - assertThat(scanResult.getSubclasses(Object.class.getName()).getNames()).contains(Impl1.class.getName()); + try (ScanResult scanResult = new ClassGraph().acceptPackages(Impl1.class.getPackage().getName()).scan()) { + assertThat(scanResult.getSubclasses(Object.class).getNames()).contains(Impl1.class.getName()); } } @@ -32,11 +30,11 @@ public void issue70() { */ @Test public void issue70EnableExternalClasses() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Impl1.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Impl1.class.getPackage().getName()) .enableExternalClasses().scan()) { - assertThat(scanResult.getSubclasses(Object.class.getName()).getNames()).contains(Impl1.class.getName()); + assertThat(scanResult.getSubclasses(Object.class).getNames()).contains(Impl1.class.getName()); assertThat(scanResult.getSuperclasses(Impl1Sub.class.getName()).getNames()) - .containsExactlyInAnyOrder(Impl1.class.getName()); + .containsOnly(Impl1.class.getName()); } } @@ -46,9 +44,9 @@ public void issue70EnableExternalClasses() { @Test public void extendsExternal() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(InternalExtendsExternal.class.getPackage().getName()).scan()) { + .acceptPackages(InternalExtendsExternal.class.getPackage().getName()).scan()) { assertThat(scanResult.getSuperclasses(InternalExtendsExternal.class.getName()).getNames()) - .containsExactlyInAnyOrder(ExternalSuperclass.class.getName()); + .containsOnly(ExternalSuperclass.class.getName()); } } @@ -58,10 +56,10 @@ public void extendsExternal() { @Test public void extendsExternalWithEnableExternal() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(InternalExtendsExternal.class.getPackage().getName()).enableExternalClasses() + .acceptPackages(InternalExtendsExternal.class.getPackage().getName()).enableExternalClasses() .scan()) { assertThat(scanResult.getSuperclasses(InternalExtendsExternal.class.getName()).getNames()) - .containsExactlyInAnyOrder(ExternalSuperclass.class.getName()); + .containsOnly(ExternalSuperclass.class.getName()); } } @@ -71,9 +69,9 @@ public void extendsExternalWithEnableExternal() { @Test public void extendsExternalSubclass() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(InternalExtendsExternal.class.getPackage().getName()).scan()) { - assertThat(scanResult.getSubclasses(ExternalSuperclass.class.getName()).getNames()) - .containsExactlyInAnyOrder(InternalExtendsExternal.class.getName()); + .acceptPackages(InternalExtendsExternal.class.getPackage().getName()).scan()) { + assertThat(scanResult.getSubclasses(ExternalSuperclass.class).getNames()) + .containsOnly(InternalExtendsExternal.class.getName()); } } @@ -83,10 +81,10 @@ public void extendsExternalSubclass() { @Test public void nonStrictExtendsExternalSubclass() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(InternalExtendsExternal.class.getPackage().getName()).enableExternalClasses() + .acceptPackages(InternalExtendsExternal.class.getPackage().getName()).enableExternalClasses() .scan()) { - assertThat(scanResult.getSubclasses(ExternalSuperclass.class.getName()).getNames()) - .containsExactlyInAnyOrder(InternalExtendsExternal.class.getName()); + assertThat(scanResult.getSubclasses(ExternalSuperclass.class).getNames()) + .containsOnly(InternalExtendsExternal.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/ResolveTypeVariable.java b/src/test/java/io/github/classgraph/issues/ResolveTypeVariableTest.java similarity index 72% rename from src/test/java/io/github/classgraph/issues/ResolveTypeVariable.java rename to src/test/java/io/github/classgraph/issues/ResolveTypeVariableTest.java index b4718dc6a..6017f477f 100644 --- a/src/test/java/io/github/classgraph/issues/ResolveTypeVariable.java +++ b/src/test/java/io/github/classgraph/issues/ResolveTypeVariableTest.java @@ -4,7 +4,7 @@ import java.util.ArrayList; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.FieldInfoList; @@ -12,14 +12,14 @@ import io.github.classgraph.TypeVariableSignature; /** - * The Class ResolveTypeVariable. + * ResolveTypeVariable. * * @param * the generic type */ -public class ResolveTypeVariable> { - +public class ResolveTypeVariableTest> { /** The list. */ + @SuppressWarnings("null") T list; /** @@ -28,8 +28,8 @@ public class ResolveTypeVariable> { @Test public void test() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(ResolveTypeVariable.class.getPackage().getName()).enableAllInfo().scan()) { - final FieldInfoList fields = scanResult.getClassInfo(ResolveTypeVariable.class.getName()) + .acceptPackages(ResolveTypeVariableTest.class.getPackage().getName()).enableAllInfo().scan()) { + final FieldInfoList fields = scanResult.getClassInfo(ResolveTypeVariableTest.class.getName()) .getFieldInfo(); assertThat(((TypeVariableSignature) fields.get(0).getTypeSignature()).resolve().toString()) .isEqualTo("T extends java.util.ArrayList"); diff --git a/src/test/java/io/github/classgraph/issues/TestGetUniqueClasspathElements.java b/src/test/java/io/github/classgraph/issues/TestGetUniqueClasspathElements.java index a53c7e5de..037b45a0d 100644 --- a/src/test/java/io/github/classgraph/issues/TestGetUniqueClasspathElements.java +++ b/src/test/java/io/github/classgraph/issues/TestGetUniqueClasspathElements.java @@ -5,21 +5,20 @@ import java.io.File; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; /** - * The Class TestGetUniqueClasspathElements. + * TestGetUniqueClasspathElements. */ -public class TestGetUniqueClasspathElements { - +class TestGetUniqueClasspathElements { /** * Test get unique classpath elements. */ @Test - public void testGetUniqueClasspathElements() { - final List classpathElements = new ClassGraph().whitelistPackages("com.xyz").getClasspathFiles(); + void testGetUniqueClasspathElements() { + final List classpathElements = new ClassGraph().acceptPackages("com.xyz").getClasspathFiles(); assertThat(classpathElements).isNotEmpty(); } } diff --git a/src/test/java/io/github/classgraph/issues/issue100/Issue100Test.java b/src/test/java/io/github/classgraph/issues/issue100/Issue100Test.java index 481e4aaa6..1cc96d4a5 100644 --- a/src/test/java/io/github/classgraph/issues/issue100/Issue100Test.java +++ b/src/test/java/io/github/classgraph/issues/issue100/Issue100Test.java @@ -34,7 +34,7 @@ import java.net.URLClassLoader; import java.util.ArrayList; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; @@ -42,10 +42,9 @@ import io.github.classgraph.ScanResult; /** - * The Class Issue100Test. + * Issue100Test. */ public class Issue100Test { - /** * Issue 100 test. */ @@ -62,29 +61,29 @@ public void issue100Test() { // earlier in classpath than "...b.jar" final ArrayList fieldNames1 = new ArrayList<>(); try (ScanResult scanResult = new ClassGraph().overrideClassLoaders(overrideClassLoader) - .whitelistPackages("issue100").blacklistJars(bJarName).enableFieldInfo().scan()) { + .acceptPackages("issue100").rejectJars(bJarName).enableFieldInfo().scan()) { for (final ClassInfo ci : scanResult.getAllClasses()) { for (final FieldInfo f : ci.getFieldInfo()) { fieldNames1.add(f.getName()); } } } - assertThat(fieldNames1).containsExactlyInAnyOrder("a"); + assertThat(fieldNames1).containsOnly("a"); - // However, if "...b.jar" is specifically whitelisted, "...a.jar" should not be visible. Originally, the + // However, if "...b.jar" is specifically accepted, "...a.jar" should not be visible. Originally, the // version of the class in "...a.jar" was supposed to mask the same class in "...b.jar" (#100). However, // this resulted in a slowdown in scan time (#117). Since classloading behavior is undefined if you override // the classpath (or in this case, the classloaders), we should only see field "b" in "...b.jar" (which is - // what we actually see through scanning the whitelisted jar, "bJarName"). + // what we actually see through scanning the accepted jar, "bJarName"). final ArrayList fieldNames2 = new ArrayList<>(); try (ScanResult scanResult = new ClassGraph().overrideClassLoaders(overrideClassLoader) - .whitelistPackages("issue100").whitelistJars(bJarName).enableFieldInfo().scan()) { + .acceptPackages("issue100").acceptJars(bJarName).enableFieldInfo().scan()) { for (final ClassInfo ci : scanResult.getAllClasses()) { for (final FieldInfo f : ci.getFieldInfo()) { fieldNames2.add(f.getName()); } } } - assertThat(fieldNames2).containsExactlyInAnyOrder("b"); + assertThat(fieldNames2).containsOnly("b"); } } diff --git a/src/test/java/io/github/classgraph/issues/issue101/AnnotatedClass.java b/src/test/java/io/github/classgraph/issues/issue101/AnnotatedClass.java index 9cd8a4273..3d240b43e 100644 --- a/src/test/java/io/github/classgraph/issues/issue101/AnnotatedClass.java +++ b/src/test/java/io/github/classgraph/issues/issue101/AnnotatedClass.java @@ -1,7 +1,7 @@ package io.github.classgraph.issues.issue101; /** - * The Class AnnotatedClass. + * AnnotatedClass. */ @NonInheritedAnnotation @InheritedAnnotation diff --git a/src/test/java/io/github/classgraph/issues/issue101/ImplementsAnnotatedInterface.java b/src/test/java/io/github/classgraph/issues/issue101/ImplementsAnnotatedInterface.java index e8f83f628..b1300408d 100644 --- a/src/test/java/io/github/classgraph/issues/issue101/ImplementsAnnotatedInterface.java +++ b/src/test/java/io/github/classgraph/issues/issue101/ImplementsAnnotatedInterface.java @@ -1,7 +1,7 @@ package io.github.classgraph.issues.issue101; /** - * The Class ImplementsAnnotatedInterface. + * ImplementsAnnotatedInterface. */ public class ImplementsAnnotatedInterface implements AnnotatedInterface { } diff --git a/src/test/java/io/github/classgraph/issues/issue101/ImplementsNonAnnotatedSubinterface.java b/src/test/java/io/github/classgraph/issues/issue101/ImplementsNonAnnotatedSubinterface.java index b39dadbc4..4467eb93f 100644 --- a/src/test/java/io/github/classgraph/issues/issue101/ImplementsNonAnnotatedSubinterface.java +++ b/src/test/java/io/github/classgraph/issues/issue101/ImplementsNonAnnotatedSubinterface.java @@ -1,7 +1,7 @@ package io.github.classgraph.issues.issue101; /** - * The Class ImplementsNonAnnotatedSubinterface. + * ImplementsNonAnnotatedSubinterface. */ public class ImplementsNonAnnotatedSubinterface implements NonAnnotatedSubinterface { } diff --git a/src/test/java/io/github/classgraph/issues/issue101/Issue101Test.java b/src/test/java/io/github/classgraph/issues/issue101/Issue101Test.java index 8ac846717..1d60e27c1 100644 --- a/src/test/java/io/github/classgraph/issues/issue101/Issue101Test.java +++ b/src/test/java/io/github/classgraph/issues/issue101/Issue101Test.java @@ -30,25 +30,24 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue101Test. + * Issue101Test. */ public class Issue101Test { - /** * Non inherited annotation. */ @Test public void nonInheritedAnnotation() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Issue101Test.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue101Test.class.getPackage().getName()) .enableAllInfo().scan()) { - assertThat(scanResult.getClassesWithAnnotation(NonInheritedAnnotation.class.getName()).getNames()) - .containsExactlyInAnyOrder(AnnotatedClass.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(NonInheritedAnnotation.class).getNames()) + .containsOnly(AnnotatedClass.class.getName()); } } @@ -57,11 +56,10 @@ public void nonInheritedAnnotation() { */ @Test public void inheritedMetaAnnotation() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Issue101Test.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue101Test.class.getPackage().getName()) .enableAllInfo().scan()) { - assertThat(scanResult.getClassesWithAnnotation(InheritedMetaAnnotation.class.getName()) - .getStandardClasses().getNames()).containsExactlyInAnyOrder(AnnotatedClass.class.getName(), - NonAnnotatedSubclass.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(InheritedMetaAnnotation.class).getStandardClasses() + .getNames()).containsOnly(AnnotatedClass.class.getName(), NonAnnotatedSubclass.class.getName()); } } @@ -70,11 +68,11 @@ public void inheritedMetaAnnotation() { */ @Test public void inheritedAnnotation() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Issue101Test.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue101Test.class.getPackage().getName()) .enableAllInfo().scan()) { - assertThat(scanResult.getClassesWithAnnotation(InheritedAnnotation.class.getName()).getNames()) - .containsExactlyInAnyOrder(AnnotatedClass.class.getName(), NonAnnotatedSubclass.class.getName(), - AnnotatedInterface.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(InheritedAnnotation.class).getNames()).containsOnly( + AnnotatedClass.class.getName(), NonAnnotatedSubclass.class.getName(), + AnnotatedInterface.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue101/NonAnnotatedSubclass.java b/src/test/java/io/github/classgraph/issues/issue101/NonAnnotatedSubclass.java index d6bb77855..72177f5b3 100644 --- a/src/test/java/io/github/classgraph/issues/issue101/NonAnnotatedSubclass.java +++ b/src/test/java/io/github/classgraph/issues/issue101/NonAnnotatedSubclass.java @@ -1,7 +1,7 @@ package io.github.classgraph.issues.issue101; /** - * The Class NonAnnotatedSubclass. + * NonAnnotatedSubclass. */ public class NonAnnotatedSubclass extends AnnotatedClass { } diff --git a/src/test/java/io/github/classgraph/issues/issue107/Issue107Test.java b/src/test/java/io/github/classgraph/issues/issue107/Issue107Test.java index d80162ef4..729c8d85f 100644 --- a/src/test/java/io/github/classgraph/issues/issue107/Issue107Test.java +++ b/src/test/java/io/github/classgraph/issues/issue107/Issue107Test.java @@ -32,32 +32,30 @@ import java.util.Arrays; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue107Test. + * Issue107Test. */ public class Issue107Test { - /** * Issue 107 test. */ @Test public void issue107Test() { // Package annotations should have "package-info" as their class name - final String pkg = Issue107Test.class.getPackage().getName(); - try (ScanResult scanResult = new ClassGraph().whitelistPackages(pkg).enableAnnotationInfo() + try (ScanResult scanResult = new ClassGraph().acceptPackages("io.github.classgraph").enableAnnotationInfo() // package-info is a non-public class .ignoreClassVisibility() // .scan()) { - assertThat(scanResult.getClassesWithAnnotation(PackageAnnotation.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getPackageInfo().getNames()).containsAll(Arrays.asList("", "io", "io.github", - "io.github.classgraph", Issue107Test.class.getPackage().getName())); + assertThat(scanResult.getClassesWithAnnotation(PackageAnnotation.class).getNames()).isEmpty(); + assertThat(scanResult.getPackageInfo().getNames()) + .containsAll(Arrays.asList("io.github.classgraph", Issue107Test.class.getPackage().getName())); assertThat(scanResult.getPackageInfo(Issue107Test.class.getPackage().getName()).getAnnotationInfo() - .getNames()).containsExactlyInAnyOrder(PackageAnnotation.class.getName()); + .getNames()).containsOnly(PackageAnnotation.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue128/Issue128Test.java b/src/test/java/io/github/classgraph/issues/issue128/Issue128Test.java index 41aac37fd..afd4fa4a7 100644 --- a/src/test/java/io/github/classgraph/issues/issue128/Issue128Test.java +++ b/src/test/java/io/github/classgraph/issues/issue128/Issue128Test.java @@ -31,37 +31,36 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; -import java.io.InputStream; +import java.net.HttpURLConnection; import java.net.URL; import java.net.URLClassLoader; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue128Test. + * Issue128Test. */ public class Issue128Test { + /** The site. */ + private static final String SITE = "https://raw.githubusercontent.com/classgraph"; - /** The Constant SITE. */ - private static final String SITE = "https://github.com/classgraph"; - - /** The Constant JAR_URL. */ + /** The jar URL. */ private static final String JAR_URL = SITE + // - "/classgraph/blob/master/src/test/resources/nested-jars-level1.zip?raw=true"; + "/classgraph/latest/src/test/resources/nested-jars-level1.zip"; - /** The Constant NESTED_JAR_URL. */ + /** The nested jar URL. */ private static final String NESTED_JAR_URL = // JAR_URL + "!level2.jar!level3.jar!classpath1/classpath2"; /** * Issue 128 test. * - * @throws IOException - * Signals that an I/O exception has occurred. + * @throws Exception + * the exception */ @Test public void issue128Test() throws Exception { @@ -73,15 +72,27 @@ public void issue128Test() throws Exception { final List filesInsideLevel3 = scanResult.getAllResources().getPaths(); if (filesInsideLevel3.isEmpty()) { // If there were no files inside jar, it is possible that remote jar could not be downloaded - try (InputStream is = jarURL.openStream()) { - throw new Exception("Able to download remote jar, but could not find files within jar"); - } catch (final IOException e) { + try { + final HttpURLConnection connection = (HttpURLConnection) jarURL.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(2000); + connection.connect(); + final int code = connection.getResponseCode(); + if (code != 200) { + throw new Exception( + "Got bad response code " + code + " when trying to fetch URL " + jarURL); + } else { + throw new Exception("Able to download remote jar, but could not find files within jar"); + } + } catch (final java.net.SocketTimeoutException e) { + System.err.println("Timeout while trying to download remote jar, skipping test " + + Issue128Test.class.getName() + ": " + e); + } catch (final IOException | SecurityException e) { System.err.println("Could not download remote jar, skipping test " + Issue128Test.class.getName() + ": " + e); } } else { - assertThat(filesInsideLevel3).containsExactlyInAnyOrder("com/test/Test.java", - "com/test/Test.class"); + assertThat(filesInsideLevel3).containsOnly("com/test/Test.java", "com/test/Test.class"); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue140/Issue140Test.java b/src/test/java/io/github/classgraph/issues/issue140/Issue140Test.java index 50437963f..0ea444094 100644 --- a/src/test/java/io/github/classgraph/issues/issue140/Issue140Test.java +++ b/src/test/java/io/github/classgraph/issues/issue140/Issue140Test.java @@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ArrayTypeSignature; import io.github.classgraph.BaseTypeSignature; @@ -42,10 +42,9 @@ import io.github.classgraph.TypeSignature; /** - * The Class Issue140Test. + * Issue140Test. */ public class Issue140Test { - /** The int field. */ // Order of fields is significant public int intField; @@ -58,7 +57,7 @@ public class Issue140Test { */ @Test public void issue140Test() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Issue140Test.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue140Test.class.getPackage().getName()) .enableFieldInfo().scan()) { final ClassInfo ci = scanResult.getClassInfo(Issue140Test.class.getName()); assertThat(ci).isNotNull(); diff --git a/src/test/java/io/github/classgraph/issues/issue141/Issue141Test.java b/src/test/java/io/github/classgraph/issues/issue141/Issue141Test.java index 10cfe4e07..72fe1192d 100644 --- a/src/test/java/io/github/classgraph/issues/issue141/Issue141Test.java +++ b/src/test/java/io/github/classgraph/issues/issue141/Issue141Test.java @@ -29,7 +29,7 @@ package io.github.classgraph.issues.issue141; /** - * The Class Issue141Test. + * Issue141Test. * * @author wuetherich */ diff --git a/src/test/java/io/github/classgraph/issues/issue146/Issue146Test.java b/src/test/java/io/github/classgraph/issues/issue146/Issue146Test.java index 012479963..a3a0687b7 100644 --- a/src/test/java/io/github/classgraph/issues/issue146/Issue146Test.java +++ b/src/test/java/io/github/classgraph/issues/issue146/Issue146Test.java @@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; @@ -38,10 +38,9 @@ import io.github.classgraph.ScanResult; /** - * The Class Issue146Test. + * Issue146Test. */ public class Issue146Test { - /** * Issue 146 test. */ @@ -50,7 +49,7 @@ public void issue146Test() { // Scans io.github.classgraph.issues.issue146.CompiledWithJDK8, which is in // src/test/resources final String pkg = Issue146Test.class.getPackage().getName(); - try (ScanResult scanResult = new ClassGraph().whitelistPackages(pkg) // + try (ScanResult scanResult = new ClassGraph().acceptPackages(pkg) // .enableMethodInfo() // .scan()) { final ClassInfo classInfo = scanResult.getClassInfo(pkg + "." + "CompiledWithJDK8"); diff --git a/src/test/java/io/github/classgraph/issues/issue148/Issue148Test.java b/src/test/java/io/github/classgraph/issues/issue148/Issue148Test.java index 6d64451e7..a6198a9e8 100644 --- a/src/test/java/io/github/classgraph/issues/issue148/Issue148Test.java +++ b/src/test/java/io/github/classgraph/issues/issue148/Issue148Test.java @@ -30,17 +30,16 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; import io.github.classgraph.ScanResult; /** - * The Class Issue148Test. + * Issue148Test. */ public class Issue148Test { - /** The anonymous inner class 1. */ final Runnable anonymousInnerClass1 = new Runnable() { @Override @@ -64,7 +63,7 @@ public void run() { final String pkg = Issue148Test.class.getPackage().getName(); final StringBuilder buf = new StringBuilder(); - try (ScanResult scanResult = new ClassGraph().whitelistPackages(pkg).enableAllInfo().scan()) { + try (ScanResult scanResult = new ClassGraph().acceptPackages(pkg).enableAllInfo().scan()) { for (final ClassInfo ci : scanResult.getAllClasses()) { buf.append(ci.getName()).append("|"); buf.append(ci.isInnerClass()).append(" ").append(ci.isAnonymousInnerClass()).append(" ") diff --git a/src/test/java/io/github/classgraph/issues/issue148/O1.java b/src/test/java/io/github/classgraph/issues/issue148/O1.java index b16d4b406..0f1ffa7f6 100644 --- a/src/test/java/io/github/classgraph/issues/issue148/O1.java +++ b/src/test/java/io/github/classgraph/issues/issue148/O1.java @@ -1,10 +1,9 @@ package io.github.classgraph.issues.issue148; /** - * The Class O1. + * O1. */ public class O1 { - /** * The Class SI. */ @@ -15,12 +14,10 @@ static class SI { * The Class I. */ public class I { - /** * The Class II. */ public class II { - /** * Constructor. */ diff --git a/src/test/java/io/github/classgraph/issues/issue148/O2.java b/src/test/java/io/github/classgraph/issues/issue148/O2.java index 789c2264a..a514e2791 100644 --- a/src/test/java/io/github/classgraph/issues/issue148/O2.java +++ b/src/test/java/io/github/classgraph/issues/issue148/O2.java @@ -3,10 +3,9 @@ import io.github.classgraph.issues.issue148.O1.SI; /** - * The Class O2. + * O2. */ public class O2 { - /** The x. */ SI x = new SI() { }; diff --git a/src/test/java/io/github/classgraph/issues/issue151/Issue151Test.java b/src/test/java/io/github/classgraph/issues/issue151/Issue151Test.java index 55199927e..ee90e9f9a 100644 --- a/src/test/java/io/github/classgraph/issues/issue151/Issue151Test.java +++ b/src/test/java/io/github/classgraph/issues/issue151/Issue151Test.java @@ -35,17 +35,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.MethodInfo; import io.github.classgraph.ScanResult; /** - * The Class Issue151Test. + * Issue151Test. */ public class Issue151Test { - /** * Issue 151 test. */ @@ -54,7 +53,7 @@ public void issue151Test() { // Scans io.github.classgraph.issues.issue146.CompiledWithJDK8, which is in // src/test/resources final String pkg = Issue151Test.class.getPackage().getName(); - try (ScanResult scanResult = new ClassGraph().whitelistPackages(pkg) // + try (ScanResult scanResult = new ClassGraph().acceptPackages(pkg) // .enableMethodInfo() // .enableAnnotationInfo() // .scan()) { @@ -63,9 +62,9 @@ public void issue151Test() { .getMethodInfo("method") // .get(0); assertThat(methodInfo.toString()) // - .isEqualTo("public void method(@" + ParamAnnotation0.class.getName() + " java.lang.String, @" - + ParamAnnotation1.class.getName() + " @" + ParamAnnotation2.class.getName() - + " java.lang.String)"); + .isEqualTo("public void method(@" + ParamAnnotation0.class.getName() + + " final java.lang.String annotatedValue0, @" + ParamAnnotation1.class.getName() + " @" + + ParamAnnotation2.class.getName() + " final java.lang.String annotatedValue1)"); } } diff --git a/src/test/java/io/github/classgraph/issues/issue152/Issue152Test.java b/src/test/java/io/github/classgraph/issues/issue152/Issue152Test.java index caae746b3..ff5bdac79 100644 --- a/src/test/java/io/github/classgraph/issues/issue152/Issue152Test.java +++ b/src/test/java/io/github/classgraph/issues/issue152/Issue152Test.java @@ -34,17 +34,16 @@ import java.util.Map; import java.util.Set; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; import io.github.classgraph.ScanResult; /** - * The Class Issue152Test. + * Issue152Test. */ public class Issue152Test { - /** The test field. */ public Map> testField; @@ -90,7 +89,7 @@ public static class TestType { @Test public void issue152Test() { final String pkg = Issue152Test.class.getPackage().getName(); - try (ScanResult scanResult = new ClassGraph().whitelistPackages(pkg) // + try (ScanResult scanResult = new ClassGraph().acceptPackages(pkg) // .enableMethodInfo() // .enableFieldInfo() // .scan()) { @@ -99,12 +98,15 @@ public void issue152Test() { .getMethodInfo("testMethod") // .get(0).toString()) // .isEqualTo("public java.util.Set testMethod(" - + "java.util.List, java.util.Map>, double[][][], int, " - + TestType.class.getName() + "[], java.util.Set, java.util.List, java.util.Map, java.util.Set[])"); + + "final java.util.List param0, " + + "final java.util.Map> param2, " + + "final double[][][] param3, final int param4, final " + + TestType.class.getName() + "[] param5, " + "final java.util.Set param6, " + "final java.util.List param7, " + + "final java.util.Map param8, " + + "final java.util.Set[] param9)"); assertThat(classInfo // .getFieldInfo("testField").toString()) // .isEqualTo("public java.util.Map> classes = Arrays.asList(TestA.class, TestAB.class); @@ -67,7 +66,7 @@ public class Issue167Test { */ @Test public void scanPackagesTest1() { - try (ScanResult scanResult = new ClassGraph().whitelistPackagesNonRecursive(packages.toArray(new String[0])) + try (ScanResult scanResult = new ClassGraph().acceptPackagesNonRecursive(packages.toArray(new String[0])) .enableClassInfo().scan()) { assertEquals(classNames, scanResult.getAllClasses().getNames()); } @@ -81,7 +80,7 @@ public void scanPackagesTest2() { final List reversedPackages = new ArrayList<>(packages); Collections.reverse(reversedPackages); try (ScanResult scanResult = new ClassGraph() - .whitelistPackagesNonRecursive(reversedPackages.toArray(new String[0])).enableClassInfo().scan()) { + .acceptPackagesNonRecursive(reversedPackages.toArray(new String[0])).enableClassInfo().scan()) { assertEquals(classNames, scanResult.getAllClasses().getNames()); } } diff --git a/src/test/java/io/github/classgraph/issues/issue167/a/TestA.java b/src/test/java/io/github/classgraph/issues/issue167/a/TestA.java index f903e02f7..9f45e648a 100644 --- a/src/test/java/io/github/classgraph/issues/issue167/a/TestA.java +++ b/src/test/java/io/github/classgraph/issues/issue167/a/TestA.java @@ -1,7 +1,7 @@ package io.github.classgraph.issues.issue167.a; /** - * The Class TestA. + * TestA. */ public class TestA { } diff --git a/src/test/java/io/github/classgraph/issues/issue167/a/b/TestAB.java b/src/test/java/io/github/classgraph/issues/issue167/a/b/TestAB.java index 71ab74389..830322bca 100644 --- a/src/test/java/io/github/classgraph/issues/issue167/a/b/TestAB.java +++ b/src/test/java/io/github/classgraph/issues/issue167/a/b/TestAB.java @@ -1,7 +1,7 @@ package io.github.classgraph.issues.issue167.a.b; /** - * The Class TestAB. + * TestAB. */ public class TestAB { } diff --git a/src/test/java/io/github/classgraph/issues/issue171/Issue171Test.java b/src/test/java/io/github/classgraph/issues/issue171/Issue171Test.java index 23ac60cfa..c801beea7 100644 --- a/src/test/java/io/github/classgraph/issues/issue171/Issue171Test.java +++ b/src/test/java/io/github/classgraph/issues/issue171/Issue171Test.java @@ -5,16 +5,15 @@ import java.net.URL; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue171Test. + * Issue171Test. */ public class Issue171Test { - /** * Spring boot fully executable jar. */ @@ -23,8 +22,8 @@ public void springBootFullyExecutableJar() { final URL jarURL = Issue171Test.class.getClassLoader().getResource("spring-boot-fully-executable-jar.jar"); try (ScanResult scanResult = new ClassGraph() - .whitelistPackagesNonRecursive("hello", "org.springframework.boot") - .overrideClasspath(jarURL + "!BOOT-INF/classes") // + .acceptPackagesNonRecursive("hello", "org.springframework.boot") + .overrideClasspath("jar:" + jarURL + "!/BOOT-INF/classes") // .scan()) { final List classNames = scanResult.getAllClasses().getNames(); assertThat(classNames).contains("hello.HelloController", diff --git a/src/test/java/io/github/classgraph/issues/issue175/Issue175Test.java b/src/test/java/io/github/classgraph/issues/issue175/Issue175Test.java index fb9e980e9..d3020717b 100644 --- a/src/test/java/io/github/classgraph/issues/issue175/Issue175Test.java +++ b/src/test/java/io/github/classgraph/issues/issue175/Issue175Test.java @@ -35,7 +35,7 @@ import java.util.ArrayList; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; @@ -43,10 +43,9 @@ import io.github.classgraph.ScanResult; /** - * The Class Issue175Test. + * Issue175Test. */ public class Issue175Test { - /** * Test synthetic. */ @@ -57,7 +56,7 @@ public void testSynthetic() { final URL aJarURL = classLoader.getResource(aJarName); final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); - try (ScanResult result = new ClassGraph().whitelistPackages("net.corda.core.contracts") // + try (ScanResult result = new ClassGraph().acceptPackages("net.corda.core.contracts") // .overrideClassLoaders(overrideClassLoader).ignoreParentClassLoaders().ignoreMethodVisibility() .ignoreFieldVisibility().enableMethodInfo().enableFieldInfo().scan()) { final List methods = new ArrayList<>(); @@ -67,7 +66,7 @@ public void testSynthetic() { methods.add(method.toString()); } } - assertThat(methods).containsExactlyInAnyOrder( // + assertThat(methods).containsOnly( // "protected (synthetic java.lang.String $enum$name, synthetic int $enum$ordinal)", "public static net.corda.core.contracts.ComponentGroupEnum[] values()", "public static net.corda.core.contracts.ComponentGroupEnum valueOf(java.lang.String)"); @@ -84,7 +83,7 @@ public void testMandated() { final URL aJarURL = classLoader.getResource(aJarName); final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); - try (ScanResult result = new ClassGraph().whitelistPackages("net.corda.core") // + try (ScanResult result = new ClassGraph().acceptPackages("net.corda.core") // .overrideClassLoaders(overrideClassLoader).ignoreParentClassLoaders().enableAllInfo().scan()) { final List methods = new ArrayList<>(); for (final String className : result.getAllClasses().getNames()) { @@ -93,7 +92,7 @@ public void testMandated() { methods.add(method.toString()); } } - assertThat(methods).containsExactlyInAnyOrder( + assertThat(methods).containsOnly( "@org.jetbrains.annotations.NotNull public static final rx.Observable toObservable(@org.jetbrains.annotations.NotNull mandated net.corda.core.concurrent.CordaFuture $receiver)", "@org.jetbrains.annotations.NotNull public static final net.corda.core.concurrent.CordaFuture toFuture(@org.jetbrains.annotations.NotNull mandated rx.Observable $receiver)"); } @@ -109,7 +108,7 @@ public void testMismatchedTypes() { final URL aJarURL = classLoader.getResource(aJarName); final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); - try (ScanResult result = new ClassGraph().whitelistPackages("net.corda.core") // + try (ScanResult result = new ClassGraph().acceptPackages("net.corda.core") // .overrideClassLoaders(overrideClassLoader).ignoreParentClassLoaders().enableAllInfo().scan()) { final List methods = new ArrayList<>(); for (final String className : result.getAllClasses().getNames()) { @@ -118,7 +117,7 @@ public void testMismatchedTypes() { methods.add(method.toString()); } } - assertThat(methods).containsExactlyInAnyOrder( + assertThat(methods).containsOnly( "public static final W match(@org.jetbrains.annotations.NotNull mandated java.util.concurrent.Future $receiver, @org.jetbrains.annotations.NotNull kotlin.jvm.functions.Function1 success, @org.jetbrains.annotations.NotNull kotlin.jvm.functions.Function1 failure)", "@org.jetbrains.annotations.NotNull public static final net.corda.core.concurrent.CordaFuture firstOf(@org.jetbrains.annotations.NotNull net.corda.core.concurrent.CordaFuture[] futures, @org.jetbrains.annotations.NotNull kotlin.jvm.functions.Function1, ? extends W> handler)", "public static synthetic void shortCircuitedTaskFailedMessage$annotations()", @@ -136,7 +135,7 @@ public void testResultTypesNotReconciled1() { final URL aJarURL = classLoader.getResource(aJarName); final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); - try (ScanResult result = new ClassGraph().whitelistPackages("net.corda.core.contracts") // + try (ScanResult result = new ClassGraph().acceptPackages("net.corda.core.contracts") // .overrideClassLoaders(overrideClassLoader).ignoreParentClassLoaders().enableAllInfo().scan()) { final List methods = new ArrayList<>(); for (final String className : result.getAllClasses().getNames()) { @@ -145,7 +144,7 @@ public void testResultTypesNotReconciled1() { methods.add(method.toString()); } } - assertThat(methods).containsExactlyInAnyOrder("private final java.lang.String commandDataToString()", + assertThat(methods).containsOnly("private final java.lang.String commandDataToString()", "@org.jetbrains.annotations.NotNull public java.lang.String toString()", "@org.jetbrains.annotations.NotNull public final T getValue()", "@org.jetbrains.annotations.NotNull public final java.util.List getSigners()", @@ -170,7 +169,7 @@ public void testResultTypesNotReconciled2() { final URL aJarURL = classLoader.getResource(aJarName); final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); - try (ScanResult result = new ClassGraph().whitelistPackages("net.corda.testing.node") // + try (ScanResult result = new ClassGraph().acceptPackages("net.corda.testing.node") // .overrideClassLoaders(overrideClassLoader).ignoreParentClassLoaders().enableAllInfo().scan()) { final List methods = new ArrayList<>(); for (final String className : result.getAllClasses().getNames()) { @@ -179,7 +178,7 @@ public void testResultTypesNotReconciled2() { methods.add(method.toString()); } } - assertThat(methods).containsExactlyInAnyOrder("public final int getNextNodeId()", + assertThat(methods).containsOnly("public final int getNextNodeId()", "private final void setNextNodeId(int )", "@org.jetbrains.annotations.NotNull public final net.corda.testing.node.InMemoryMessagingNetwork getMessagingNetwork()", "@org.jetbrains.annotations.NotNull public final java.util.List getNodes()", @@ -232,7 +231,7 @@ public void testAttributeParameterMismatch() { final URL aJarURL = classLoader.getResource(aJarName); final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); - try (ScanResult result = new ClassGraph().whitelistPackages("net.corda.core.node.services.vault") // + try (ScanResult result = new ClassGraph().acceptPackages("net.corda.core.node.services.vault") // .overrideClassLoaders(overrideClassLoader).ignoreParentClassLoaders().enableAllInfo().scan()) { final List methods = new ArrayList<>(); for (final String className : result.getAllClasses().getNames()) { @@ -241,7 +240,7 @@ public void testAttributeParameterMismatch() { methods.add(method.toString()); } } - assertThat(methods).containsExactlyInAnyOrder( // + assertThat(methods).containsOnly( // "protected (synthetic java.lang.String $enum$name, synthetic int $enum$ordinal, @org.jetbrains.annotations.NotNull java.lang.String columnName)", "public static net.corda.core.node.services.vault.AttachmentSort$AttachmentSortAttribute[] values()", "public static net.corda.core.node.services.vault.AttachmentSort$AttachmentSortAttribute valueOf(java.lang.String)", @@ -259,7 +258,7 @@ public void testResultTypeReconciliationIssue() { final URL aJarURL = classLoader.getResource(aJarName); final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); - try (ScanResult result = new ClassGraph().whitelistPackages("net.corda.client.jackson") // + try (ScanResult result = new ClassGraph().acceptPackages("net.corda.client.jackson") // .overrideClassLoaders(overrideClassLoader).ignoreParentClassLoaders().enableAllInfo().scan()) { final List methods = new ArrayList<>(); for (final String className : result.getAllClasses().getNames()) { @@ -268,13 +267,13 @@ public void testResultTypeReconciliationIssue() { methods.add(method.toString()); } } - assertThat(methods).containsExactlyInAnyOrder( + assertThat(methods).containsOnly( "@org.jetbrains.annotations.NotNull protected final com.google.common.collect.Multimap getMethodMap()", "@org.jetbrains.annotations.NotNull public final java.util.Map> getMethodParamNames()", "@org.jetbrains.annotations.NotNull public java.util.List paramNamesFromMethod(@org.jetbrains.annotations.NotNull java.lang.reflect.Method method)", "@org.jetbrains.annotations.NotNull public java.util.List paramNamesFromConstructor(@org.jetbrains.annotations.NotNull java.lang.reflect.Constructor ctor)", - "@org.jetbrains.annotations.NotNull public final net.corda.client.jackson.StringToMethodCallParser.ParsedMethodCall parse(@org.jetbrains.annotations.Nullable T target, @org.jetbrains.annotations.NotNull java.lang.String command)", - "@org.jetbrains.annotations.NotNull public final java.lang.Object[] parseArguments(@org.jetbrains.annotations.NotNull java.lang.String methodNameHint, @org.jetbrains.annotations.NotNull java.util.List>> parameters, @org.jetbrains.annotations.NotNull java.lang.String args)", + "@org.jetbrains.annotations.NotNull public final net.corda.client.jackson.StringToMethodCallParser$ParsedMethodCall parse(@org.jetbrains.annotations.Nullable T target, @org.jetbrains.annotations.NotNull java.lang.String command) throws net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException", + "@org.jetbrains.annotations.NotNull public final java.lang.Object[] parseArguments(@org.jetbrains.annotations.NotNull java.lang.String methodNameHint, @org.jetbrains.annotations.NotNull java.util.List>> parameters, @org.jetbrains.annotations.NotNull java.lang.String args) throws net.corda.client.jackson.StringToMethodCallParser$UnparseableCallException", "@org.jetbrains.annotations.NotNull public final java.util.Map getAvailableCommands()", "@kotlin.jvm.JvmOverloads public (@org.jetbrains.annotations.NotNull java.lang.Class targetType, @org.jetbrains.annotations.NotNull com.fasterxml.jackson.databind.ObjectMapper om)", "@kotlin.jvm.JvmOverloads public synthetic (java.lang.Class, com.fasterxml.jackson.databind.ObjectMapper, int, kotlin.jvm.internal.DefaultConstructorMarker)", @@ -295,7 +294,7 @@ public void testParameterArityMismatch() { final URL aJarURL = classLoader.getResource(aJarName); final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); - try (ScanResult result = new ClassGraph().whitelistPackages("net.corda.core.node.services.vault") // + try (ScanResult result = new ClassGraph().acceptPackages("net.corda.core.node.services.vault") // .overrideClassLoaders(overrideClassLoader).ignoreParentClassLoaders().enableAllInfo().scan()) { final List methods = new ArrayList<>(); for (final String className : result.getAllClasses().getNames()) { @@ -304,7 +303,7 @@ public void testParameterArityMismatch() { methods.add(method.toString()); } } - assertThat(methods).containsExactlyInAnyOrder( + assertThat(methods).containsOnly( "@org.jetbrains.annotations.NotNull public static , P extends net.corda.core.node.services.vault.BaseQueryCriteriaParser, S extends net.corda.core.node.services.vault.BaseSort> java.util.Collection visit(@org.jetbrains.annotations.NotNull net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria$AndVisitor, P parser)"); } } @@ -319,7 +318,7 @@ public void testBareTypeIssue() { final URL aJarURL = classLoader.getResource(aJarName); final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); - try (ScanResult result = new ClassGraph().whitelistPackages("net.corda.client.jackson") // + try (ScanResult result = new ClassGraph().acceptPackages("net.corda.client.jackson") // .overrideClassLoaders(overrideClassLoader).ignoreParentClassLoaders().enableAllInfo().scan()) { final List methods = new ArrayList<>(); for (final String className : result.getAllClasses().getNames()) { @@ -328,7 +327,7 @@ public void testBareTypeIssue() { methods.add(method.toString()); } } - assertThat(methods).containsExactlyInAnyOrder( + assertThat(methods).containsOnly( "@org.jetbrains.annotations.Nullable public final java.lang.Object invoke()", "@org.jetbrains.annotations.Nullable public java.lang.Object call()", "@org.jetbrains.annotations.NotNull public final java.lang.reflect.Method getMethod()", diff --git a/src/test/java/io/github/classgraph/issues/issue193/Issue193Test.java b/src/test/java/io/github/classgraph/issues/issue193/Issue193Test.java index 5ea9a9149..fc50d7061 100644 --- a/src/test/java/io/github/classgraph/issues/issue193/Issue193Test.java +++ b/src/test/java/io/github/classgraph/issues/issue193/Issue193Test.java @@ -36,19 +36,16 @@ import java.net.URLClassLoader; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.ops4j.pax.url.mvn.MavenResolvers; import io.github.classgraph.ClassGraph; -import io.github.classgraph.ClassInfo; -import io.github.classgraph.ClassInfoList.ClassInfoFilter; import io.github.classgraph.ScanResult; /** - * The Class Issue193Test. + * Issue193Test. */ public class Issue193Test { - /** * Issue 193 test. * @@ -68,17 +65,13 @@ public void issue193Test() throws IOException { // Scan the classpath -- used to throw an exception for Stack, since companion object inherits // from different class try (ScanResult scanResult = new ClassGraph() // - .whitelistPackages("scala.collection.immutable") // + .acceptPackages("scala.collection.immutable") // .overrideClassLoaders(classLoader) // .scan()) { final List classes = scanResult // .getAllClasses() // - .filter(new ClassInfoFilter() { - @Override - public boolean accept(final ClassInfo ci) { - return ci.getName().endsWith("$"); - } - }).getNames(); + .filter(ci -> ci.getName().endsWith("$")) // + .getNames(); assertThat(classes).contains("scala.collection.immutable.Stack$"); } } diff --git a/src/test/java/io/github/classgraph/issues/issue209/Issue209Test.java b/src/test/java/io/github/classgraph/issues/issue209/Issue209Test.java index b588d7daf..70515dbb2 100644 --- a/src/test/java/io/github/classgraph/issues/issue209/Issue209Test.java +++ b/src/test/java/io/github/classgraph/issues/issue209/Issue209Test.java @@ -33,27 +33,26 @@ import java.net.URL; import java.net.URLClassLoader; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue209Test. + * Issue209Test. */ public class Issue209Test { - /** * Test spring boot jar with lib jars. */ @Test public void testSpringBootJarWithLibJars() { - try (ScanResult result = new ClassGraph().whitelistPackages( // + try (ScanResult result = new ClassGraph().acceptPackages( // "org.springframework.boot.loader.util", "com.foo", "issue209lib") // .overrideClassLoaders(new URLClassLoader( new URL[] { Issue209Test.class.getClassLoader().getResource("issue209.jar") })) // .scan()) { - assertThat(result.getAllClasses().getNames()).containsExactlyInAnyOrder( + assertThat(result.getAllClasses().getNames()).containsOnly( // Test reading from / "org.springframework.boot.loader.util.SystemPropertyUtils", // Test reading from /BOOT-INF/classes diff --git a/src/test/java/io/github/classgraph/issues/issue216/Issue216Test.java b/src/test/java/io/github/classgraph/issues/issue216/Issue216Test.java index ad3db2326..f8818b24a 100644 --- a/src/test/java/io/github/classgraph/issues/issue216/Issue216Test.java +++ b/src/test/java/io/github/classgraph/issues/issue216/Issue216Test.java @@ -32,7 +32,7 @@ import javax.persistence.Entity; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; @@ -40,24 +40,23 @@ import io.github.classgraph.ScanResult; /** - * The Class Issue216Test. + * Issue216Test. */ @Entity public class Issue216Test { - /** * Test spring boot jar with lib jars. */ @Test public void testSpringBootJarWithLibJars() { - try (ScanResult result = new ClassGraph().whitelistPackages(Issue216Test.class.getPackage().getName()) + try (ScanResult result = new ClassGraph().acceptPackages(Issue216Test.class.getPackage().getName()) .enableAllInfo().scan()) { assertThat(result.getAllClasses().filter(new ClassInfoFilter() { @Override public boolean accept(final ClassInfo ci) { - return ci.hasAnnotation(Entity.class.getName()); + return ci.hasAnnotation(Entity.class); } - }).getNames()).containsExactlyInAnyOrder(Issue216Test.class.getName()); + }).getNames()).containsOnly(Issue216Test.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue223/Issue223Test.java b/src/test/java/io/github/classgraph/issues/issue223/Issue223Test.java index f16aeaa5e..95ae80910 100644 --- a/src/test/java/io/github/classgraph/issues/issue223/Issue223Test.java +++ b/src/test/java/io/github/classgraph/issues/issue223/Issue223Test.java @@ -32,7 +32,7 @@ import javax.persistence.Entity; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; @@ -41,11 +41,10 @@ import io.github.classgraph.ScanResult; /** - * The Class Issue223Test. + * Issue223Test. */ @Entity public class Issue223Test { - /** * The Interface InnerInterface. */ @@ -57,7 +56,7 @@ public interface InnerInterface { */ @Test public void testClassloadInnerClasses() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Issue223Test.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue223Test.class.getPackage().getName()) .enableAllInfo().scan()) { final ClassInfoList innerClasses = scanResult.getAllClasses().filter(new ClassInfoFilter() { @Override @@ -73,10 +72,12 @@ public boolean accept(final ClassInfo ci) { } } assertThat(innerInterface).isNotNull(); - assertThat(innerInterface.getName()).isEqualTo(InnerInterface.class.getName()); - assertThat(innerInterface.isInterface()).isTrue(); - final Class innerClassRef = innerInterface.loadClass(); - assertThat(innerClassRef).isNotNull(); + if (innerInterface != null) { + assertThat(innerInterface.getName()).isEqualTo(InnerInterface.class.getName()); + assertThat(innerInterface.isInterface()).isTrue(); + final Class innerClassRef = innerInterface.loadClass(); + assertThat(innerClassRef).isNotNull(); + } } } } diff --git a/src/test/java/io/github/classgraph/issues/issue238/Issue238Test.java b/src/test/java/io/github/classgraph/issues/issue238/Issue238Test.java index a745e872b..7d197efdd 100644 --- a/src/test/java/io/github/classgraph/issues/issue238/Issue238Test.java +++ b/src/test/java/io/github/classgraph/issues/issue238/Issue238Test.java @@ -34,17 +34,16 @@ import javax.persistence.Entity; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue238Test. + * Issue238Test. */ @Entity public class Issue238Test { - /** * The Class B. */ @@ -92,7 +91,7 @@ public static class F extends A { */ @Test public void testSuperclassInheritanceOrder() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Issue238Test.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue238Test.class.getPackage().getName()) .enableAllInfo().scan()) { final List classNames = scanResult.getAllClasses().get(E.class.getName()).getSuperclasses() .getNames(); diff --git a/src/test/java/io/github/classgraph/issues/issue245/Issue245Test.java b/src/test/java/io/github/classgraph/issues/issue245/Issue245Test.java index ea7054c0b..cb25917a3 100644 --- a/src/test/java/io/github/classgraph/issues/issue245/Issue245Test.java +++ b/src/test/java/io/github/classgraph/issues/issue245/Issue245Test.java @@ -32,16 +32,15 @@ import java.net.URL; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue245Test. + * Issue245Test. */ public class Issue245Test { - /** * Test custom package root. */ @@ -53,10 +52,10 @@ public void testCustomPackageRoot() { try (ScanResult scanResult = new ClassGraph() // .overrideClasspath(jarURL.toString() + "!/META-INF/maven") // - .whitelistPaths("org.springframework/gs-spring-boot") // + .acceptPaths("org.springframework/gs-spring-boot") // .disableNestedJarScanning() // .scan()) { - assertThat(scanResult.getAllResources().getPaths()).containsExactlyInAnyOrder( + assertThat(scanResult.getAllResources().getPaths()).containsOnly( "org.springframework/gs-spring-boot/pom.xml", "org.springframework/gs-spring-boot/pom.properties"); } diff --git a/src/test/java/io/github/classgraph/issues/issue246/Issue246Test.java b/src/test/java/io/github/classgraph/issues/issue246/Issue246Test.java index 9dfcc4279..688b91104 100644 --- a/src/test/java/io/github/classgraph/issues/issue246/Issue246Test.java +++ b/src/test/java/io/github/classgraph/issues/issue246/Issue246Test.java @@ -28,25 +28,24 @@ */ package io.github.classgraph.issues.issue246; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue246Test. + * Issue246Test. */ public class Issue246Test { - /** * Test method parameter annotations. */ @Test public void testMethodParameterAnnotations() { try (ScanResult scanResult = new ClassGraph() // - .whitelistClasses(Issue246Test.class.getName()) // + .acceptClasses(Issue246Test.class.getName()) // .enableAllInfo() // .scan()) { assertEquals(0, // diff --git a/src/test/java/io/github/classgraph/issues/issue255/Issue255Test.java b/src/test/java/io/github/classgraph/issues/issue255/Issue255Test.java index cf5869afe..894970894 100644 --- a/src/test/java/io/github/classgraph/issues/issue255/Issue255Test.java +++ b/src/test/java/io/github/classgraph/issues/issue255/Issue255Test.java @@ -30,36 +30,35 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import java.io.IOException; + +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; -import io.github.classgraph.Resource; import io.github.classgraph.ResourceList; -import io.github.classgraph.ResourceList.ByteArrayConsumer; import io.github.classgraph.ScanResult; /** - * The Class Issue255Test. + * Issue255Test. */ public class Issue255Test { /** * Issue 255 test. + * + * @throws IOException + * If an I/O exception occurs. */ @Test - public void issue255Test() { + public void issue255Test() throws IOException { final String dirPath = Issue255Test.class.getClassLoader().getResource("issue255").getPath() + "/test%20percent%20encoding"; try (ScanResult scanResult = new ClassGraph().overrideClasspath(dirPath).scan()) { final ResourceList resources = scanResult.getAllResources(); assertThat(resources.size()).isEqualTo(1); - resources.forEachByteArray(new ByteArrayConsumer() { - @Override - public void accept(final Resource resource, final byte[] byteArray) { - assertThat(new String(byteArray)).isEqualTo("Issue 255"); - } - }); + resources.forEachByteArrayThrowingIOException( + (resource, byteArray) -> assertThat(new String(byteArray)).isEqualTo("Issue 255")); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue260/Issue260Test.java b/src/test/java/io/github/classgraph/issues/issue260/Issue260Test.java index c2c3f3817..ed3688923 100644 --- a/src/test/java/io/github/classgraph/issues/issue260/Issue260Test.java +++ b/src/test/java/io/github/classgraph/issues/issue260/Issue260Test.java @@ -30,22 +30,21 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue260Test. + * Issue260Test. */ public class Issue260Test { - /** * Issue 260 test. */ @Test public void issue260Test() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Issue260Test.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue260Test.class.getPackage().getName()) .enableAllInfo().scan()) { // Should be no exception here assertThat(true).isTrue(); diff --git a/src/test/java/io/github/classgraph/issues/issue260/Outer.java b/src/test/java/io/github/classgraph/issues/issue260/Outer.java index 8e1807f16..57c666d52 100644 --- a/src/test/java/io/github/classgraph/issues/issue260/Outer.java +++ b/src/test/java/io/github/classgraph/issues/issue260/Outer.java @@ -1,10 +1,9 @@ package io.github.classgraph.issues.issue260; /** - * The Class Outer. + * Outer. */ public class Outer { - /** * Creates the anonymous. * diff --git a/src/test/java/io/github/classgraph/issues/issue260/P.java b/src/test/java/io/github/classgraph/issues/issue260/P.java index dbcb3fff5..d02db6449 100644 --- a/src/test/java/io/github/classgraph/issues/issue260/P.java +++ b/src/test/java/io/github/classgraph/issues/issue260/P.java @@ -1,7 +1,7 @@ package io.github.classgraph.issues.issue260; /** - * The Class P. + * P. */ public abstract class P { diff --git a/src/test/java/io/github/classgraph/issues/issue261/Issue261Test.java b/src/test/java/io/github/classgraph/issues/issue261/Issue261Test.java index 1a20c963b..d2415fad0 100644 --- a/src/test/java/io/github/classgraph/issues/issue261/Issue261Test.java +++ b/src/test/java/io/github/classgraph/issues/issue261/Issue261Test.java @@ -30,16 +30,15 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue261Test. + * Issue261Test. */ public class Issue261Test { - /** * The Class SuperSuperCls. */ @@ -63,11 +62,10 @@ private static class Cls extends SuperCls { */ @Test public void issue261Test() { - // Whitelist only the class Cls, so that SuperCls and SuperSuperCls are external classes - try (ScanResult scanResult = new ClassGraph().whitelistClasses(Cls.class.getName()).enableAllInfo() - .scan()) { - assertThat(scanResult.getSubclasses(SuperSuperCls.class.getName()).getNames()) - .containsExactlyInAnyOrder(SuperCls.class.getName(), Cls.class.getName()); + // Accept only the class Cls, so that SuperCls and SuperSuperCls are external classes + try (ScanResult scanResult = new ClassGraph().acceptClasses(Cls.class.getName()).enableAllInfo().scan()) { + assertThat(scanResult.getSubclasses(SuperSuperCls.class).getNames()) + .containsOnly(SuperCls.class.getName(), Cls.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoaders.java b/src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoaders.java index 1c0f7df2a..1a6e1b294 100644 --- a/src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoaders.java +++ b/src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoaders.java @@ -40,10 +40,9 @@ import io.github.classgraph.ScanResult; /** - * The Class ClassLoadingWorksWithParentLastLoaders. + * ClassLoadingWorksWithParentLastLoaders. */ public class ClassLoadingWorksWithParentLastLoaders { - /** * Assert correct class loaders. * @@ -62,7 +61,7 @@ public void assertCorrectClassLoaders(final String parentClassLoader, final Stri .isEqualTo(expectedClassLoader); assertThat(a.getClass().getClassLoader().getClass().getSimpleName()).isEqualTo(expectedClassLoader); - final ClassGraph classGraph = new ClassGraph().whitelistPackages("com.xyz.meta").enableAllInfo(); + final ClassGraph classGraph = new ClassGraph().acceptPackages("com.xyz.meta").enableAllInfo(); // ClassGraph is in that setup not part of the RestartClass loader. That one takes by default only // URLs from the current project into consideration and can only be modified by adding additional diff --git a/src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoadersStub.java b/src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoadersStubTest.java similarity index 92% rename from src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoadersStub.java rename to src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoadersStubTest.java index c303f099f..e745b9e2d 100644 --- a/src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoadersStub.java +++ b/src/test/java/io/github/classgraph/issues/issue267/ClassLoadingWorksWithParentLastLoadersStubTest.java @@ -34,23 +34,23 @@ import java.lang.reflect.Method; import java.net.URL; -import org.junit.Test; +import org.junit.jupiter.api.Test; import com.xyz.meta.A; /** - * The Class ClassLoadingWorksWithParentLastLoadersStub. + * ClassLoadingWorksWithParentLastLoadersStub. */ -public class ClassLoadingWorksWithParentLastLoadersStub { +public class ClassLoadingWorksWithParentLastLoadersStubTest { /** * Same class loader that found A class should load it. * - * @throws Exception - * the exception + * @throws Throwable + * the throwable */ @Test - public void sameClassLoaderThatFoundAClassShouldLoadIt() throws Exception { + public void sameClassLoaderThatFoundAClassShouldLoadIt() throws Throwable { final String currentClassLoadersName = Thread.currentThread().getContextClassLoader().getClass() .getSimpleName(); @@ -60,6 +60,9 @@ public void sameClassLoaderThatFoundAClassShouldLoadIt() throws Exception { final TestLauncher launcher = new TestLauncher(currentClassLoadersName); launcher.start(); launcher.join(); + if (launcher.thrown != null) { + throw launcher.thrown; + } } } @@ -67,6 +70,8 @@ class TestLauncher extends Thread { private final String parentClassLoader; + Throwable thrown; + TestLauncher(final String parentClassLoader) { this.parentClassLoader = parentClassLoader; setDaemon(false); @@ -82,8 +87,8 @@ public void run() { String.class); mainMethod.invoke(mainClass.getDeclaredConstructor().newInstance(), parentClassLoader, "FakeRestartClassLoader"); - } catch (final Throwable ex) { - getUncaughtExceptionHandler().uncaughtException(this, ex); + } catch (final Throwable t) { + thrown = t; } } } diff --git a/src/test/java/io/github/classgraph/issues/issue277/Issue227Test.java b/src/test/java/io/github/classgraph/issues/issue277/Issue227Test.java index f0de7f22c..3de570a0d 100644 --- a/src/test/java/io/github/classgraph/issues/issue277/Issue227Test.java +++ b/src/test/java/io/github/classgraph/issues/issue277/Issue227Test.java @@ -1,6 +1,6 @@ package io.github.classgraph.issues.issue277; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; @@ -8,20 +8,19 @@ * https://github.com/classgraph/classgraph/issues/277 */ public class Issue227Test { - /** - * Test no args blacklist lib or ext jars. + * Test no args reject lib or ext jars. */ @Test - public void testNoArgsBlacklistLibOrExtJars() { - new ClassGraph().blacklistLibOrExtJars(); + public void testNoArgsRejectLibOrExtJars() { + new ClassGraph().rejectLibOrExtJars(); } /** - * Test no args whitelist lib or ext jars. + * Test no args accept lib or ext jars. */ @Test - public void testNoArgsWhitelistLibOrExtJars() { - new ClassGraph().whitelistLibOrExtJars(); + public void testNoArgsAcceptLibOrExtJars() { + new ClassGraph().acceptLibOrExtJars(); } } diff --git a/src/test/java/io/github/classgraph/issues/issue286/Issue286Test.java b/src/test/java/io/github/classgraph/issues/issue286/Issue286Test.java index 2d9ef4b2c..248621441 100644 --- a/src/test/java/io/github/classgraph/issues/issue286/Issue286Test.java +++ b/src/test/java/io/github/classgraph/issues/issue286/Issue286Test.java @@ -32,20 +32,21 @@ import java.net.URL; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue286Test. + * Issue286Test. */ public class Issue286Test { - /** * Issue 286 test. */ - @Test(timeout = 1000) + @Test + @Timeout(value = 1) public void issue286Test() { final URL jarURL = getClass().getClassLoader().getResource("issue286.jar"); assertThat(jarURL).isNotNull(); diff --git a/src/test/java/io/github/classgraph/issues/issue289/Issue289.java b/src/test/java/io/github/classgraph/issues/issue289/Issue289Test.java similarity index 74% rename from src/test/java/io/github/classgraph/issues/issue289/Issue289.java rename to src/test/java/io/github/classgraph/issues/issue289/Issue289Test.java index e7cd883d8..5e188a3c6 100644 --- a/src/test/java/io/github/classgraph/issues/issue289/Issue289.java +++ b/src/test/java/io/github/classgraph/issues/issue289/Issue289Test.java @@ -5,25 +5,24 @@ import java.net.URL; import java.net.URLClassLoader; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ResourceList; import io.github.classgraph.ScanResult; /** - * The Class Issue289. + * Issue289. */ -public class Issue289 { - +public class Issue289Test { /** * Issue 289. */ @Test - public void issue289() throws Exception { + public void issue289() { try (ScanResult scanResult = new ClassGraph() .overrideClassLoaders( - new URLClassLoader(new URL[] { Issue289.class.getClassLoader().getResource("zip64.zip") })) + new URLClassLoader(new URL[] { Issue289Test.class.getClassLoader().getResource("zip64.zip") })) .scan()) { for (int i = 0; i < 90000; i++) { final ResourceList resources = scanResult.getResourcesWithPath(i + ""); diff --git a/src/test/java/io/github/classgraph/issues/issue303/Issue303Test.java b/src/test/java/io/github/classgraph/issues/issue303/Issue303Test.java index c06a90e37..6d22b0c31 100644 --- a/src/test/java/io/github/classgraph/issues/issue303/Issue303Test.java +++ b/src/test/java/io/github/classgraph/issues/issue303/Issue303Test.java @@ -32,16 +32,15 @@ import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue303Test. + * Issue303Test. */ public class Issue303Test { - /** The Constant PACKAGE_NAME. */ private static final String PACKAGE_NAME = "io.github.classgraph"; @@ -56,12 +55,12 @@ public void testPackageInfoClasses() { final List packageClassNamesNonRecursive0; final List packageClassNamesNonRecursive1; final List packageClassNamesNonRecursive2; - try (ScanResult scanResult = new ClassGraph().whitelistPackages(PACKAGE_NAME).enableAllInfo().scan()) { + try (ScanResult scanResult = new ClassGraph().acceptPackages(PACKAGE_NAME).enableAllInfo().scan()) { packageClassNamesRecursive = scanResult.getPackageInfo(PACKAGE_NAME).getClassInfoRecursive().getNames(); packageClassNamesNonRecursive0 = scanResult.getPackageInfo(PACKAGE_NAME).getClassInfo().getNames(); allClassNamesRecursive = scanResult.getAllClasses().getNames(); } - try (ScanResult scanResult = new ClassGraph().whitelistPackagesNonRecursive(PACKAGE_NAME).enableAllInfo() + try (ScanResult scanResult = new ClassGraph().acceptPackagesNonRecursive(PACKAGE_NAME).enableAllInfo() .scan()) { packageClassNamesNonRecursive1 = scanResult.getPackageInfo(PACKAGE_NAME).getClassInfoRecursive() .getNames(); diff --git a/src/test/java/io/github/classgraph/issues/issue305/Issue305.java b/src/test/java/io/github/classgraph/issues/issue305/Issue305.java deleted file mode 100644 index ba348be52..000000000 --- a/src/test/java/io/github/classgraph/issues/issue305/Issue305.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.classgraph.issues.issue305; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.logging.ConsoleHandler; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.junit.Test; - -import io.github.classgraph.ClassGraph; -import io.github.classgraph.ScanResult; - -/** - * The Class Issue305. - */ -public class Issue305 { - /** Test that multi-line continuations in manifest file values are correctly assembled into a string. */ - @Test - public void issue305() throws Exception { - ConsoleHandler errPrintStreamHandler = null; - final Logger rootLogger = Logger.getLogger(""); - try { - // Record log output - final ByteArrayOutputStream err = new ByteArrayOutputStream(); - System.setErr(new PrintStream(err)); - final Logger log = Logger.getLogger(ClassGraph.class.getName()); - if (!log.isLoggable(Level.INFO)) { - throw new Exception("Could not create log"); - } - errPrintStreamHandler = new ConsoleHandler(); - errPrintStreamHandler.setLevel(Level.INFO); - rootLogger.addHandler(errPrintStreamHandler); - - try (ScanResult scanResult = new ClassGraph() - .overrideClassLoaders(new URLClassLoader(new URL[] { - Issue305.class.getClassLoader().getResource("class-path-manifest-entry.jar") })) - .verbose().scan()) { - } - - final String systemErrMessages = new String(err.toByteArray()); - assertThat(systemErrMessages.indexOf("Found Class-Path entry in manifest file: " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/charsets.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/deploy.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/access-bridge-64.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/cldrdata.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/dnsns.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/jaccess.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/jfxrt.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/localedata.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/nashorn.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/sunec.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/sunjce_provider.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/sunmscapi.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/sunpkcs11.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/zipfs.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/javaws.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/jce.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/jfr.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/jfxswt.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/jsse.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/management-agent.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/plugin.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/resources.jar " - + "file:/C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/rt.jar " - + "file:/Z:/classgraphtest/target/classes/ " - + "file:/C:/Users/flame/.m2/repository/io/github/classgraph/classgraph/4.6.19/classgraph-4.6.19.jar " - + "file:/C:/Program%20Files/JetBrains/IntelliJ%20IDEA%20Community%20Edition%202018.2.1/lib/idea_rt.jar")) - .isGreaterThan(0); - - } finally { - rootLogger.removeHandler(errPrintStreamHandler); - // Set to System.err - System.setErr(System.err); - } - } -} diff --git a/src/test/java/io/github/classgraph/issues/issue305/Issue305Test.java b/src/test/java/io/github/classgraph/issues/issue305/Issue305Test.java new file mode 100644 index 000000000..b8e033592 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue305/Issue305Test.java @@ -0,0 +1,90 @@ +package io.github.classgraph.issues.issue305; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Issue305. + */ +public class Issue305Test { + private ConsoleHandler errPrintStreamHandler = null; + private final Logger rootLogger = Logger.getLogger(""); + + /** Reset encapsulation circumvention method after each test. */ + @AfterEach + void resetAfterTest() { + rootLogger.removeHandler(errPrintStreamHandler); + // Set to System.err + System.setErr(System.err); + } + + /** + * Test that multi-line continuations in manifest file values are correctly assembled into a string. + * + * @throws Exception + * the exception + */ + @Test + public void issue305() throws Exception { + // Record log output + final ByteArrayOutputStream err = new ByteArrayOutputStream(); + System.setErr(new PrintStream(err)); + final Logger log = Logger.getLogger(ClassGraph.class.getName()); + if (!log.isLoggable(Level.INFO)) { + throw new Exception("Could not create log"); + } + errPrintStreamHandler = new ConsoleHandler(); + errPrintStreamHandler.setLevel(Level.INFO); + rootLogger.addHandler(errPrintStreamHandler); + + try (ScanResult scanResult = new ClassGraph() + .overrideClassLoaders(new URLClassLoader( + new URL[] { Issue305Test.class.getClassLoader().getResource("class-path-manifest-entry.jar") })) + // This .verbose() is needed (stderr is captured) + .verbose().scan()) { + } + + final String systemErrMessages = new String(err.toByteArray()); + assertThat(systemErrMessages.indexOf("Found Class-Path entry in manifest file: " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/charsets.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/deploy.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/access-bridge-64.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/cldrdata.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/dnsns.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/jaccess.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/jfxrt.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/localedata.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/nashorn.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/sunec.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/sunjce_provider.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/sunmscapi.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/sunpkcs11.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/ext/zipfs.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/javaws.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/jce.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/jfr.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/jfxswt.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/jsse.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/management-agent.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/plugin.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/resources.jar " + + "file:///C:/Program%20Files/Java/jdk1.8.0_162/jre/lib/rt.jar " + + "file:///Z:/classgraphtest/target/classes/ " + + "file:///C:/Users/flame/.m2/repository/io/github/classgraph/classgraph/4.6.19/classgraph-4.6.19.jar " + + "file:///C:/Program%20Files/JetBrains/IntelliJ%20IDEA%20Community%20Edition%202018.2.1/lib/idea_rt.jar")) + .isGreaterThan(0); + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue310/Issue310.java b/src/test/java/io/github/classgraph/issues/issue310/Issue310Test.java similarity index 75% rename from src/test/java/io/github/classgraph/issues/issue310/Issue310.java rename to src/test/java/io/github/classgraph/issues/issue310/Issue310Test.java index 6d52ae365..4602a8372 100644 --- a/src/test/java/io/github/classgraph/issues/issue310/Issue310.java +++ b/src/test/java/io/github/classgraph/issues/issue310/Issue310Test.java @@ -2,16 +2,15 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue310. + * Issue310. */ -public class Issue310 { - +public class Issue310Test { /** The Constant A. */ static final double A = 3.0; @@ -36,12 +35,12 @@ public void issue310() { // the same after the first and second deserialization, because overrideClasspath is set by the first // serialization for consistency.) final String classfileURL = getClass().getClassLoader() - .getResource(Issue310.class.getName().replace('.', '/') + ".class").toString(); + .getResource(Issue310Test.class.getName().replace('.', '/') + ".class").toString(); final String classpathBase = classfileURL.substring(0, - classfileURL.length() - (Issue310.class.getName().length() + 6)); + classfileURL.length() - (Issue310Test.class.getName().length() + 6)); try (ScanResult scanResult1 = new ClassGraph().overrideClasspath(classpathBase) - .whitelistClasses(Issue310.class.getName()).enableAllInfo().scan()) { - assertThat(scanResult1.getClassInfo(Issue310.class.getName()).getFieldInfo("B")).isNotNull(); + .acceptClasses(Issue310Test.class.getName()).enableAllInfo().scan()) { + assertThat(scanResult1.getClassInfo(Issue310Test.class.getName()).getFieldInfo("B")).isNotNull(); final String json1 = scanResult1.toJSON(2); assertThat(json1).isNotEmpty(); try (ScanResult scanResult2 = ScanResult.fromJSON(scanResult1.toJSON())) { diff --git a/src/test/java/io/github/classgraph/issues/issue314/Issue314.java b/src/test/java/io/github/classgraph/issues/issue314/Issue314Test.java similarity index 70% rename from src/test/java/io/github/classgraph/issues/issue314/Issue314.java rename to src/test/java/io/github/classgraph/issues/issue314/Issue314Test.java index e8b2537df..e1056b89f 100644 --- a/src/test/java/io/github/classgraph/issues/issue314/Issue314.java +++ b/src/test/java/io/github/classgraph/issues/issue314/Issue314Test.java @@ -2,16 +2,15 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue314. + * Issue314. */ -public class Issue314 { - +public class Issue314Test { /** * The Class A. */ @@ -33,11 +32,11 @@ public void issue314() { // the same after the first and second deserialization, because overrideClasspath is set by the first // serialization for consistency.) final String classfileURL = getClass().getClassLoader() - .getResource(Issue314.class.getName().replace('.', '/') + ".class").toString(); + .getResource(Issue314Test.class.getName().replace('.', '/') + ".class").toString(); final String classpathBase = classfileURL.substring(0, - classfileURL.length() - (Issue314.class.getName().length() + 6)); + classfileURL.length() - (Issue314Test.class.getName().length() + 6)); try (ScanResult scanResult1 = new ClassGraph().overrideClasspath(classpathBase) - .whitelistPackages(Issue314.class.getPackage().getName()).enableAllInfo().scan()) { + .acceptPackages(Issue314Test.class.getPackage().getName()).enableAllInfo().scan()) { assertThat(scanResult1.getClassInfo(A.class.getName())).isNotNull(); assertThat(scanResult1.getClassInfo(B.class.getName())).isNotNull(); final String json1 = scanResult1.toJSON(2); @@ -45,8 +44,8 @@ public void issue314() { try (final ScanResult scanResult2 = ScanResult.fromJSON(scanResult1.toJSON())) { final String json2 = scanResult2.toJSON(2); assertThat(json1).isEqualTo(json2); - assertThat(scanResult1.getSubclasses(A.class.getName()).getNames()).containsOnly(B.class.getName()); - assertThat(scanResult2.getSubclasses(A.class.getName()).getNames()).containsOnly(B.class.getName()); + assertThat(scanResult1.getSubclasses(A.class).getNames()).containsOnly(B.class.getName()); + assertThat(scanResult2.getSubclasses(A.class).getNames()).containsOnly(B.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue318/Issue318.java b/src/test/java/io/github/classgraph/issues/issue318/Issue318Test.java similarity index 70% rename from src/test/java/io/github/classgraph/issues/issue318/Issue318.java rename to src/test/java/io/github/classgraph/issues/issue318/Issue318Test.java index 324cc4584..1ff94c85c 100644 --- a/src/test/java/io/github/classgraph/issues/issue318/Issue318.java +++ b/src/test/java/io/github/classgraph/issues/issue318/Issue318Test.java @@ -8,16 +8,15 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue314. + * Unit test. */ -public class Issue318 { - +public class Issue318Test { /** * The Interface MyAnn. */ @@ -41,20 +40,6 @@ public class Issue318 { MyAnn[] value(); } - // /** - // * The Interface MyAnnRepeating. - // */ - // @Retention(RetentionPolicy.RUNTIME) - // @Target({ ElementType.TYPE }) - // @interface MyAnnRepeating2 { - // /** - // * Value. - // * - // * @return the my ann[] - // */ - // MyAnn[] value(); - // } - /** * The Class With0MyAnn. */ @@ -90,14 +75,14 @@ class With3MyAnn { */ @Test public void issue318() { - try (final ScanResult scanResult = new ClassGraph().whitelistPackages(Issue318.class.getPackage().getName()) + try (final ScanResult scanResult = new ClassGraph().acceptPackages(Issue318Test.class.getPackage().getName()) .enableAnnotationInfo().enableClassInfo().ignoreClassVisibility() // //.verbose() // .scan()) { - assertThat(scanResult.getClassesWithAnnotation(MyAnn.class.getName()).getNames()).containsOnly( + assertThat(scanResult.getClassesWithAnnotation(MyAnn.class).getNames()).containsOnly( With1MyAnn.class.getName(), With2MyAnn.class.getName(), With3MyAnn.class.getName()); - assertThat(scanResult.getClassInfo(With3MyAnn.class.getName()) - .getAnnotationInfoRepeatable(MyAnn.class.getName()).size()).isEqualTo(3); + assertThat(scanResult.getClassInfo(With3MyAnn.class.getName()).getAnnotationInfoRepeatable(MyAnn.class) + .size()).isEqualTo(3); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue329/Issue329Test.java b/src/test/java/io/github/classgraph/issues/issue329/Issue329Test.java new file mode 100644 index 000000000..1c2c1d895 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue329/Issue329Test.java @@ -0,0 +1,37 @@ +package io.github.classgraph.issues.issue329; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; + +/** + * Unit test. + */ +public class Issue329Test { + /** The Class Foo. */ + public class Foo { + /** Constructor. */ + public Foo() { + new Bar(); + } + } + + /** The Class Bar. */ + public class Bar { + } + + /** Test. */ + @Test + public void test() { + try (ScanResult scanResult = new ClassGraph().enableAllInfo().enableInterClassDependencies() + .enableExternalClasses().acceptClasses(Foo.class.getName()).scan()) { + final ClassInfo classInfo = scanResult.getClassInfo(Foo.class.getName()); + assertThat(classInfo.getClassDependencies().getNames()).containsOnly(Issue329Test.class.getName(), + Bar.class.getName()); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue339/Issue339Test.java b/src/test/java/io/github/classgraph/issues/issue339/Issue339Test.java new file mode 100644 index 000000000..b9c864c31 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue339/Issue339Test.java @@ -0,0 +1,66 @@ +package io.github.classgraph.issues.issue339; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.AnnotationParameterValueList; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; + +/** + * Unit test. + */ +public class Issue339Test { + /** + * Grade. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Documented + //@Repeatable(Grades.class) + public @interface Grade { + /** + * Points. + * + * @return the double + */ + double points(); + + /** + * Max points. + * + * @return the double + */ + double maxPoints() default 0.0; + } + + /** The Class Cls. */ + public class Cls { + /** Method with annotation. */ + @Grade(points = 0.4, maxPoints = 0.4) + public void method() { + } + } + + /** Test. */ + @Test + public void test() { + try (ScanResult scanResult = new ClassGraph().enableAllInfo().enableExternalClasses() + .acceptClasses(Cls.class.getName()).scan()) { + final ClassInfo classInfo = scanResult.getClassInfo(Cls.class.getName()); + final AnnotationParameterValueList annotationParamVals = classInfo.getMethodInfo("method").get(0) + .getAnnotationInfo().get(0).getParameterValues(); + assertThat(Math.abs((Double) annotationParamVals.get("points").getValue() - 0.4)).isLessThan(1.0e-12); + assertThat(Math.abs((Double) annotationParamVals.get("maxPoints").getValue() - 0.4)) + .isLessThan(1.0e-12); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue340/Issue340Test.java b/src/test/java/io/github/classgraph/issues/issue340/Issue340Test.java new file mode 100644 index 000000000..52b836d54 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue340/Issue340Test.java @@ -0,0 +1,44 @@ +package io.github.classgraph.issues.issue340; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.Resource; +import io.github.classgraph.ScanResult; +import nonapi.io.github.classgraph.utils.FastPathResolver; +import nonapi.io.github.classgraph.utils.VersionFinder; +import nonapi.io.github.classgraph.utils.VersionFinder.OperatingSystem; + +/** + * Unit test. + */ +public class Issue340Test { + /** Test. */ + @Test + public void test() { + // Test path resolution + assertThat(FastPathResolver.resolve("", "../../x")).isEqualTo("x"); + assertThat(FastPathResolver.resolve("/", "../../x")).isEqualTo("/x"); + assertThat(FastPathResolver.resolve("/x", "y")).isEqualTo("/x/y"); + assertThat(FastPathResolver.resolve("/x", "../y")).isEqualTo("/y"); + assertThat(FastPathResolver.resolve("/x", "../../y")).isEqualTo("/y"); + assertThat(FastPathResolver.resolve("/x/y/z", "..//..////w")).isEqualTo("/x/w"); + assertThat(FastPathResolver.resolve("/x/y/z", "//p//q")) + .isEqualTo(VersionFinder.OS == OperatingSystem.Windows ? "//p/q" : "/p/q"); + + try (ScanResult scanResult = new ClassGraph() + .overrideClasspath(getClass().getClassLoader().getResource("issue340.jar").getPath()).scan()) { + // issue340.jar contains Bundle-ClassPath that points to jar2 and jar4. + // jar2 has a Class-Path entry that points to jar1; jar4 has a Class-Path entry that points to jar3. + // jar2 and jar4 also have an invalid Class-Path entry that tries to escape the parent jar root. + // jar1 and jar2 are deflated, jar3 and jar4 are stored. + assertThat(scanResult.getAllResources().stream().map(Resource::getPath) + .filter(path -> path.startsWith("file")).collect(Collectors.toList())).containsOnly("file1", + "file2", "file3", "file4"); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue345/Issue345Test.java b/src/test/java/io/github/classgraph/issues/issue345/Issue345Test.java new file mode 100644 index 000000000..32c9aa186 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue345/Issue345Test.java @@ -0,0 +1,157 @@ +package io.github.classgraph.issues.issue345; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.Resource; +import io.github.classgraph.ScanResult; + +/** + * Issue345. + */ +public class Issue345Test { + /** + * Superclass. + */ + private static class Super { + } + + /** + * Subclass. + */ + public static class Sub extends Super { + } + + /** + * Test that private superclasses have their {@link Resource} reference set with .ignoreClassVisibility(). + */ + @Test + public void withIgnoreClassVisibility() { + try (ScanResult scanResult = new ClassGraph().acceptClasses(Super.class.getName(), Sub.class.getName()) + .ignoreClassVisibility().scan()) { + final ClassInfo subClassInfo = scanResult.getClassInfo(Sub.class.getName()); + assertThat(subClassInfo).isNotNull(); + assertThat(subClassInfo.getResource()).isNotNull(); + final ClassInfo superClassInfo = scanResult.getClassInfo(Super.class.getName()); + assertThat(superClassInfo).isNotNull(); + assertThat(superClassInfo.getResource()).isNotNull(); + } + } + + /** + * Test that private superclasses do not have their {@link Resource} reference set without + * .ignoreClassVisibility(). + */ + @Test + public void withoutIgnoreClassVisibility() { + try (ScanResult scanResult = new ClassGraph().acceptClasses(Super.class.getName(), Sub.class.getName()) + .scan()) { + final ClassInfo subClassInfo = scanResult.getClassInfo(Sub.class.getName()); + assertThat(subClassInfo).isNotNull(); + assertThat(subClassInfo.getResource()).isNotNull(); + final ClassInfo superClassInfo = scanResult.getClassInfo(Super.class.getName()); + assertThat(superClassInfo).isNotNull(); + assertThat(superClassInfo.getResource()).isNull(); + } + } + + /** + * Test that extending scanning to superclasses causes the {@link Resource} reference to be set. + */ + @Test + public void testExtensionToParent() { + try (ScanResult scanResult = new ClassGraph().acceptClasses(Sub.class.getName()).ignoreClassVisibility() + .scan()) { + final ClassInfo superClassInfo = scanResult.getClassInfo(Super.class.getName()); + assertThat(superClassInfo).isNotNull(); + assertThat(superClassInfo.getResource()).isNotNull(); + } + } + + /** + * Test that extending scanning to outer class causes the {@link Resource} reference to be set. + */ + @Test + public void testExtensionToOuterClass() { + try (ScanResult scanResult = new ClassGraph().acceptClasses(Super.class.getName()).ignoreClassVisibility() + .scan()) { + final ClassInfo outerClassInfo = scanResult.getClassInfo(Issue345Test.class.getName()); + assertThat(outerClassInfo).isNotNull(); + assertThat(outerClassInfo.getResource()).isNotNull(); + } + } + + /** + * Test that scanning is not extended to inner class, because the {@link Resource} reference is not set. + */ + @Test + public void testNonExtensionToInnerClass() { + try (ScanResult scanResult = new ClassGraph().acceptClasses(Issue345Test.class.getName()) + .ignoreClassVisibility().scan()) { + final ClassInfo innerClassInfo = scanResult.getClassInfo(Super.class.getName()); + assertThat(innerClassInfo).isNotNull(); + assertThat(innerClassInfo.getResource()).isNull(); + } + } + + /** + * Test that overriding classloaders does not allow other classloaders to be scanned. + */ + @Test + public void issue345b() { + // Find URL of this class' classpath element + URL classpathURL; + try (ScanResult scanResult = new ClassGraph().acceptClasses(Issue345Test.class.getName()).scan()) { + classpathURL = scanResult.getClassInfo(Issue345Test.class.getName()).getClasspathElementURL(); + } + // Use this to create an override URLClassLoader + try (ScanResult scanResult = new ClassGraph().enableClassInfo() + .overrideClassLoaders(new URLClassLoader(new URL[] { classpathURL })).ignoreParentClassLoaders() + .scan()) { + // Assert that this class is found in its own classloader + assertThat(scanResult.getClassInfo(Issue345Test.class.getName())).isNotNull(); + // But that other classpath elements on the classpath are not found + assertThat(scanResult.getClassInfo(Test.class.getName())).isNull(); + } + } + + /** + * A. + */ + private static class A { + } + + /** + * B. + */ + abstract static class B extends A { + } + + /** + * C. + */ + public static class C extends B { + } + + /** + * Test inner class modifiers are picked up from the InnerClasses attribute of classfiles. + */ + @Test + public void issue345c() { + try (ScanResult scanResult = new ClassGraph().enableClassInfo() + .acceptPackages(Issue345Test.class.getPackage().getName()).ignoreClassVisibility().scan()) { + final ClassInfo ciA = scanResult.getClassInfo(A.class.getName()); + assertThat(ciA.getModifiersStr()).isEqualTo("private static"); + final ClassInfo ciB = scanResult.getClassInfo(B.class.getName()); + assertThat(ciB.getModifiersStr()).isEqualTo("abstract static"); + final ClassInfo ciC = scanResult.getClassInfo(C.class.getName()); + assertThat(ciC.getModifiersStr()).isEqualTo("public static"); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue348/Issue348Test.java b/src/test/java/io/github/classgraph/issues/issue348/Issue348Test.java new file mode 100644 index 000000000..f0a89abb6 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue348/Issue348Test.java @@ -0,0 +1,55 @@ +package io.github.classgraph.issues.issue348; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Issue345. + */ +public class Issue348Test { + /** Test for wildcarded jars. */ + @Test + public void testWildcard() { + try (ScanResult scanResult1 = new ClassGraph().acceptPathsNonRecursive("").scan()) { + // Find all resources within classpath elements with ".jar" extension + final List jarResourceUris = scanResult1.getResourcesWithExtension("jar").stream() + .map(r -> r.getURI().toString().replace(":///", ":/").replace("://", ":/")) + .collect(Collectors.toList()); + assertThat(jarResourceUris).isNotEmpty(); + + try (ScanResult scanResult2 = new ClassGraph().overrideClasspath(jarResourceUris) + .acceptJars("issue*.jar").scan()) { + // Find all classpath element URIs for non-nested jars + final List cpUris = scanResult2.getClasspathURIs().stream().map(URI::toString) + .filter(u -> !u.contains("!")).map(u -> u.replace(":///", ":/").replace("://", ":/")) + .collect(Collectors.toList()); + assertThat(cpUris).isNotEmpty(); + + // Check that cpUris is a non-empty subset of jarResourceUris + final Set jarResourceUrisMinusCpUris = new LinkedHashSet<>(jarResourceUris); + jarResourceUrisMinusCpUris.removeAll(cpUris); + assertThat(jarResourceUrisMinusCpUris).isNotEmpty(); + assertThat(jarResourceUrisMinusCpUris.size()).isLessThan(jarResourceUris.size()); + final Set cpUrisMinusJarResourceUris = new LinkedHashSet<>(cpUris); + cpUrisMinusJarResourceUris.removeAll(jarResourceUris); + assertThat(cpUrisMinusJarResourceUris).isEmpty(); + + // Check that cpUris all end with "issue*.jar" + for (final String uri : cpUris) { + final String leaf = uri.substring(uri.lastIndexOf('/') + 1); + assertThat(leaf).matches("issue.*\\.jar"); + } + } + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue350/Issue350Test.java b/src/test/java/io/github/classgraph/issues/issue350/Issue350Test.java new file mode 100644 index 000000000..6f7405cbc --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue350/Issue350Test.java @@ -0,0 +1,87 @@ +package io.github.classgraph.issues.issue350; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Unit test. + */ +public class Issue350Test { + + /** + * The Interface SuperclassAnnotation. + */ + @Retention(RetentionPolicy.RUNTIME) + public static @interface SuperclassAnnotation { + } + + /** + * The Class Pub. + */ + public static class Pub { + + /** The annotated public field. */ + @SuperclassAnnotation + public int annotatedPublicField; + + /** + * Annotated public method. + */ + @SuperclassAnnotation + public void annotatedPublicMethod() { + } + } + + /** + * The Class Priv. + */ + public static class Priv { + /** */ + @SuperclassAnnotation + private int annotatedPrivateField; + + /** */ + @SuperclassAnnotation + private void annotatedPrivateMethod() { + } + } + + /** + * The Class PubSub. + */ + public static class PubSub extends Pub { + } + + /** + * The Class PrivSub. + */ + public static class PrivSub extends Priv { + } + + /** Test finding subclasses of classes with annotated methods or fields. */ + @Test + public void test() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue350Test.class.getPackage().getName()) + .enableClassInfo().enableFieldInfo().enableMethodInfo().enableAnnotationInfo().scan()) { + assertThat(scanResult.getClassesWithFieldAnnotation(SuperclassAnnotation.class).getNames()) + .containsOnly(Pub.class.getName(), PubSub.class.getName()); + assertThat(scanResult.getClassesWithMethodAnnotation(SuperclassAnnotation.class).getNames()) + .containsOnly(Pub.class.getName(), PubSub.class.getName()); + } + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue350Test.class.getPackage().getName()) + .enableClassInfo().enableFieldInfo().enableMethodInfo().enableAnnotationInfo() + .ignoreFieldVisibility().ignoreMethodVisibility().scan()) { + assertThat(scanResult.getClassesWithFieldAnnotation(SuperclassAnnotation.class).getNames()) + .containsOnly(Pub.class.getName(), PubSub.class.getName(), Priv.class.getName()); + assertThat(scanResult.getClassesWithMethodAnnotation(SuperclassAnnotation.class).getNames()) + .containsOnly(Pub.class.getName(), PubSub.class.getName(), Priv.class.getName()); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue352/Issue352Test.java b/src/test/java/io/github/classgraph/issues/issue352/Issue352Test.java new file mode 100644 index 000000000..795b50de9 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue352/Issue352Test.java @@ -0,0 +1,54 @@ +package io.github.classgraph.issues.issue352; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.ops4j.pax.url.mvn.MavenResolvers; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import io.github.classgraph.issues.issue107.Issue107Test; + +/** + * Unit test. + */ +public class Issue352Test { + + /** + * Test *. + * + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Test + public void test() throws IOException { + final File resolvedFile = MavenResolvers.createMavenResolver(null, null).resolve("com.sun.istack", + "istack-commons-runtime", null, null, "3.0.7"); + assertThat(resolvedFile).isFile(); + + // Test that module-info.class is not included in resource list if the root package ("") is not accepted + try (ScanResult scanResult = new ClassGraph().overrideClasspath(resolvedFile).acceptPackagesNonRecursive("") + .enableClassInfo().scan()) { + assertThat(scanResult.getAllResources().getPaths()).contains("module-info.class"); + } + try (ScanResult scanResult = new ClassGraph().overrideClasspath(resolvedFile) + .acceptPackages("com.sun.istack").enableClassInfo().scan()) { + assertThat(scanResult.getAllResources().getPaths()).doesNotContain("module-info.class"); + } + + // Test that package-info.class is only included in resource list for accepted packages + final String pkgInfoPath = Issue107Test.class.getPackage().getName().replace('.', '/') + + "/package-info.class"; + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue107Test.class.getPackage().getName()) + .enableClassInfo().scan()) { + assertThat(scanResult.getAllResources().getPaths()).contains(pkgInfoPath); + } + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue352Test.class.getPackage().getName()) + .enableClassInfo().scan()) { + assertThat(scanResult.getAllResources().getPaths()).doesNotContain(pkgInfoPath); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue355/Issue355Test.java b/src/test/java/io/github/classgraph/issues/issue355/Issue355Test.java new file mode 100644 index 000000000..adb2df02d --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue355/Issue355Test.java @@ -0,0 +1,99 @@ +package io.github.classgraph.issues.issue355; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.AnnotationClassRef; +import io.github.classgraph.ArrayClassInfo; +import io.github.classgraph.ArrayTypeSignature; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.MethodParameterInfo; +import io.github.classgraph.ScanResult; + +/** + * Unit test. + */ +public class Issue355Test { + + /** + * Annotation parameter class. + */ + public class X { + } + + /** + * Annotation with class reference array typed param. + */ + @Retention(RetentionPolicy.RUNTIME) + public @interface Ann { + + /** + * Annotation parameter. + * + * @return the class[] + */ + public Class[] value(); + } + + /** + * Annotated with class reference array. + */ + @Ann({ X.class }) + public class Y { + + /** + * method with array-typed param. + * + * @param x + * the x + */ + public void y(final X[] x) { + } + } + + /** + * Test. + * + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Test + public void test() throws IOException { + try (ScanResult scanResult = new ClassGraph() + .acceptPackagesNonRecursive(Issue355Test.class.getPackage().getName()).enableClassInfo() + .enableInterClassDependencies().scan()) { + final ClassInfo y = scanResult.getClassInfo(Y.class.getName()); + final ClassInfo x = scanResult.getClassInfo(X.class.getName()); + assertThat(y).isNotNull(); + assertThat(x).isNotNull(); + + // Test array-typed annotation parameter + final Object annParamVal = ((Object[]) y.getAnnotationInfo().get(0).getParameterValues().get(0) + .getValue())[0]; + assertThat(annParamVal).isInstanceOf(AnnotationClassRef.class); + final AnnotationClassRef annClassRef = (AnnotationClassRef) annParamVal; + assertThat(annClassRef.getClassInfo().getName()).isEqualTo(X.class.getName()); + + // Test class dep from annotation param of array element type shows up in class deps + final ClassInfoList yDeps = scanResult.getClassDependencyMap().get(y); + assertThat(yDeps).isNotNull(); + assertThat(yDeps).contains(x); + + // Test array-typed method parameter + final MethodParameterInfo yParam = y.getMethodInfo().get(0).getParameterInfo()[0]; + final ArrayTypeSignature paramTypeSignature = (ArrayTypeSignature) yParam + .getTypeSignatureOrTypeDescriptor(); + final ArrayClassInfo arrayClassInfo = paramTypeSignature.getArrayClassInfo(); + assertThat(arrayClassInfo.getElementClassInfo().equals(x)); + assertThat(arrayClassInfo.loadClass()).isEqualTo(X[].class); + assertThat(arrayClassInfo.loadElementClass()).isEqualTo(X.class); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue364/Issue364Test.java b/src/test/java/io/github/classgraph/issues/issue364/Issue364Test.java new file mode 100644 index 000000000..5458f9ef9 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue364/Issue364Test.java @@ -0,0 +1,139 @@ +/* + * This file is part of ClassGraph. + * + * Author: James Ward + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue364; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.attribute.PosixFilePermission; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Issue364Test. + */ +public class Issue364Test { + + /** + * Test No Permissions. + */ + @Test + public void testNoPermissions() { + final ClassLoader classLoader = Issue364Test.class.getClassLoader(); + final String aJarName = "issue364-no-permissions.jar"; + final URL aJarURL = classLoader.getResource(aJarName); + final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); + + try (ScanResult result = new ClassGraph().overrideClassLoaders(overrideClassLoader) + .ignoreParentClassLoaders().scan()) { + assertThat(result.getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/all") + .get(0).getLastModified()).isEqualTo(1434543812000L); + assertThat(result.getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/all") + .get(0).getPosixFilePermissions()).isNull(); + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/groupreadwrite") + .get(0).getLastModified()).isEqualTo(1434557162000L); + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/groupreadwrite") + .get(0).getPosixFilePermissions()).isNull(); + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/owneronlyread") + .get(0).getLastModified()).isEqualTo(1434557150000L); + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/owneronlyread") + .get(0).getPosixFilePermissions()).isNull(); + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/ownerreadwrite") + .get(0).getLastModified()).isEqualTo(1434543812000L); + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/ownerreadwrite") + .get(0).getPosixFilePermissions()).isNull(); + } + } + + /** + * Test Permissions. + */ + @Test + public void testPermissions() { + final ClassLoader classLoader = Issue364Test.class.getClassLoader(); + final String aJarName = "issue364-permissions.jar"; + final URL aJarURL = classLoader.getResource(aJarName); + final URLClassLoader overrideClassLoader = new URLClassLoader(new URL[] { aJarURL }); + + try (ScanResult result = new ClassGraph().overrideClassLoaders(overrideClassLoader) + .ignoreParentClassLoaders().scan()) { + assertThat(result.getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/all") + .get(0).getLastModified()).isEqualTo(1434543812000L); + assertThat(result.getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/all") + .get(0).getPosixFilePermissions()).containsOnly(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, + PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE); + + assertThat(result.getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/execute") + .get(0).getLastModified()).isEqualTo(1434557130000L); + assertThat(result.getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/execute") + .get(0).getPosixFilePermissions()).containsOnly(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_EXECUTE); + + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/groupreadwrite") + .get(0).getLastModified()).isEqualTo(1434557162000L); + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/groupreadwrite") + .get(0).getPosixFilePermissions()).containsOnly(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_WRITE); + + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/owneronlyread") + .get(0).getLastModified()).isEqualTo(1434557152000L); + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/owneronlyread") + .get(0).getPosixFilePermissions()).containsOnly(PosixFilePermission.OWNER_READ); + + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/ownerreadwrite") + .get(0).getLastModified()).isEqualTo(1434543812000L); + assertThat(result + .getResourcesWithPath("META-INF/resources/webjars/permissions-jar/1.0.0/bin/ownerreadwrite") + .get(0).getPosixFilePermissions()).containsOnly(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue368/Issue368Test.java b/src/test/java/io/github/classgraph/issues/issue368/Issue368Test.java new file mode 100644 index 000000000..cbe03f932 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue368/Issue368Test.java @@ -0,0 +1,68 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue368; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import nonapi.io.github.classgraph.json.JSONDeserializer; +import nonapi.io.github.classgraph.json.JSONSerializer; + +/** + * Issue368Test. + */ +@SuppressWarnings("null") +public class Issue368Test { + + /** + * InnerClass. + */ + public static class InnerClass { + /** The inner class field. */ + public Class innerClassField = Issue368Test.class; + } + + /** + * Issue 368 test. + */ + @Test + public void issue368Test() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue368Test.class.getPackage().getName()) + .enableAllInfo().scan()) { + final String json = JSONSerializer.serializeObject(new InnerClass()); + assertThat(json) + .isEqualTo("{\"innerClassField\":\"io.github.classgraph.issues.issue368.Issue368Test\"}"); + final InnerClass deserialized = JSONDeserializer.deserializeObject(InnerClass.class, json); + assertThat(deserialized.innerClassField == Issue368Test.class); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue37/Issue37Test.java b/src/test/java/io/github/classgraph/issues/issue37/Issue37Test.java index 884e0c3b0..745734faa 100644 --- a/src/test/java/io/github/classgraph/issues/issue37/Issue37Test.java +++ b/src/test/java/io/github/classgraph/issues/issue37/Issue37Test.java @@ -33,7 +33,7 @@ import java.util.ArrayList; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; @@ -42,7 +42,7 @@ import io.github.classgraph.ScanResult; /** - * The Class Issue37Test. + * Issue37Test. */ public class Issue37Test { /** @@ -60,7 +60,7 @@ public Issue37Test() { public void issue37Test() { final List methodNames = new ArrayList<>(); final String pkg = Issue37Test.class.getPackage().getName(); - try (ScanResult scanResult = new ClassGraph().whitelistPackages(pkg) // + try (ScanResult scanResult = new ClassGraph().acceptPackages(pkg) // .enableMethodInfo() // .scan()) { final ClassInfoList classes = scanResult.getAllClasses(); diff --git a/src/test/java/io/github/classgraph/issues/issue370/Issue370Test.java b/src/test/java/io/github/classgraph/issues/issue370/Issue370Test.java new file mode 100644 index 000000000..24d88eebb --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue370/Issue370Test.java @@ -0,0 +1,63 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue370; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.AnnotationInfo; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.ScanResult; +import io.github.classgraph.issues.issue370.annotations.ApiOperation; +import io.github.classgraph.issues.issue370.impl.ClassWithAnnotation; + +/** + * Unit Test. + */ +public class Issue370Test { + /** + * Unit test. + */ + @Test + public void issue370Test() { + try (ScanResult scanResult = new ClassGraph().enableAllInfo() + .acceptPackages(ClassWithAnnotation.class.getPackage().getName()).scan()) { + final ClassInfo clazzInfo = scanResult.getClassInfo(ClassWithAnnotation.class.getName()); + assertThat(clazzInfo).isNotNull(); + for (final MethodInfo methodInfo : clazzInfo.getMethodInfo().filter(MethodInfo::isPublic)) { + final AnnotationInfo annotationInfo = methodInfo.getAnnotationInfo(ApiOperation.class); + final String value = annotationInfo.getParameterValues().get("notes").getValue().toString(); + assertThat(value).isEqualTo("${snippetclassifications.findById}"); + } + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue370/annotations/ApiOperation.java b/src/test/java/io/github/classgraph/issues/issue370/annotations/ApiOperation.java new file mode 100644 index 000000000..f57f9f236 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue370/annotations/ApiOperation.java @@ -0,0 +1,27 @@ +package io.github.classgraph.issues.issue370.annotations; + +/** + * ApiOperation. + */ +public @interface ApiOperation { + /** + * Value. + * + * @return the string + */ + String value(); + + /** + * Notes. + * + * @return the string + */ + String notes(); + + /** + * Extensions. + * + * @return an optional array of extensions + */ + Extension[] extensions() default @Extension(properties = @ExtensionProperty(name = "", value = "")); +} \ No newline at end of file diff --git a/src/test/java/io/github/classgraph/issues/issue370/annotations/Extension.java b/src/test/java/io/github/classgraph/issues/issue370/annotations/Extension.java new file mode 100644 index 000000000..253c3fc2f --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue370/annotations/Extension.java @@ -0,0 +1,21 @@ +package io.github.classgraph.issues.issue370.annotations; + +/** + * Extension. + */ +public @interface Extension { + /** + * An option name for these extensions. + * + * @return an option name for these extensions - will be prefixed with "x-" + */ + String name() default ""; + + /** + * The extension properties. + * + * @return the actual extension properties + * @see ExtensionProperty + */ + ExtensionProperty[] properties(); +} \ No newline at end of file diff --git a/src/test/java/io/github/classgraph/issues/issue370/annotations/ExtensionProperty.java b/src/test/java/io/github/classgraph/issues/issue370/annotations/ExtensionProperty.java new file mode 100644 index 000000000..9de61e244 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue370/annotations/ExtensionProperty.java @@ -0,0 +1,21 @@ +package io.github.classgraph.issues.issue370.annotations; + +/** + * ExtensionProperty. + */ +public @interface ExtensionProperty { + + /** + * The name of the property. + * + * @return the name of the property + */ + String name(); + + /** + * The value of the property. + * + * @return the value of the property + */ + String value(); +} \ No newline at end of file diff --git a/src/test/java/io/github/classgraph/issues/issue370/impl/ClassWithAnnotation.java b/src/test/java/io/github/classgraph/issues/issue370/impl/ClassWithAnnotation.java new file mode 100644 index 000000000..2d85404db --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue370/impl/ClassWithAnnotation.java @@ -0,0 +1,15 @@ +package io.github.classgraph.issues.issue370.impl; + +import io.github.classgraph.issues.issue370.annotations.ApiOperation; + +/** + * ClassWithAnnotation. + */ +public class ClassWithAnnotation { + /** + * Do something. + */ + @ApiOperation(value = "", notes = "${snippetclassifications.findById}") + public void doSomething() { + } +} \ No newline at end of file diff --git a/src/test/java/io/github/classgraph/issues/issue38/ImplementsSuppressWarnings.java b/src/test/java/io/github/classgraph/issues/issue38/ImplementsSuppressWarnings.java index 8ab56e910..224e40f06 100644 --- a/src/test/java/io/github/classgraph/issues/issue38/ImplementsSuppressWarnings.java +++ b/src/test/java/io/github/classgraph/issues/issue38/ImplementsSuppressWarnings.java @@ -3,11 +3,10 @@ import java.lang.annotation.Annotation; /** - * The Class ImplementsSuppressWarnings. + * ImplementsSuppressWarnings. */ @SuppressWarnings("all") public class ImplementsSuppressWarnings implements SuppressWarnings { - /* (non-Javadoc) * @see java.lang.annotation.Annotation#annotationType() */ diff --git a/src/test/java/io/github/classgraph/issues/issue38/Issue38Test.java b/src/test/java/io/github/classgraph/issues/issue38/Issue38Test.java index e3a6bab0d..f42ebf12d 100644 --- a/src/test/java/io/github/classgraph/issues/issue38/Issue38Test.java +++ b/src/test/java/io/github/classgraph/issues/issue38/Issue38Test.java @@ -4,16 +4,15 @@ import java.lang.annotation.Annotation; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue38Test. + * Issue38Test. */ -public class Issue38Test { - +class Issue38Test { /** * The Class AnnotationLiteral. * @@ -27,11 +26,11 @@ public static abstract class AnnotationLiteral implements * Test implements suppress warnings. */ @Test - public void testImplementsSuppressWarnings() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Issue38Test.class.getPackage().getName()) + void testImplementsSuppressWarnings() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue38Test.class.getPackage().getName()) .scan()) { - assertThat(scanResult.getClassesImplementing(SuppressWarnings.class.getName()).getNames()) - .containsExactlyInAnyOrder(ImplementsSuppressWarnings.class.getName()); + assertThat(scanResult.getClassesImplementing(SuppressWarnings.class).getNames()) + .containsOnly(ImplementsSuppressWarnings.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue38/SomeAnnotationLiteral.java b/src/test/java/io/github/classgraph/issues/issue38/SomeAnnotationLiteral.java index 62229b544..17659d2d0 100644 --- a/src/test/java/io/github/classgraph/issues/issue38/SomeAnnotationLiteral.java +++ b/src/test/java/io/github/classgraph/issues/issue38/SomeAnnotationLiteral.java @@ -5,11 +5,10 @@ import io.github.classgraph.issues.issue38.Issue38Test.AnnotationLiteral; /** - * The Class SomeAnnotationLiteral. + * SomeAnnotationLiteral. */ @SuppressWarnings("all") public class SomeAnnotationLiteral extends AnnotationLiteral implements SomeAnnotation { - /* (non-Javadoc) * @see io.github.classgraph.issues.issue38.SomeAnnotation#value() */ diff --git a/src/test/java/io/github/classgraph/issues/issue384/Issue384Test.java b/src/test/java/io/github/classgraph/issues/issue384/Issue384Test.java new file mode 100644 index 000000000..9cfd42c93 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue384/Issue384Test.java @@ -0,0 +1,68 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue384; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.AbstractMap.SimpleEntry; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import io.github.classgraph.features.CustomURLScheme; + +/** + * Test. + */ +class Issue384Test { + @BeforeAll + static void setup() { + new CustomURLScheme(); + } + + /** + * Test. + */ + @Test + void issue384Test() throws MalformedURLException { + final String filePath = Issue384Test.class.getClassLoader().getResource("nested-jars-level1.zip").getPath(); + final String customSchemeURL = CustomURLScheme.SCHEME + ":" + filePath; + final URL url = new URL(customSchemeURL); + try (ScanResult scanResult = new ClassGraph().enableURLScheme(CustomURLScheme.SCHEME).overrideClasspath(url) + .scan()) { + assertThat(scanResult.getAllResources().getPaths()).containsExactly("level2.jar"); + assertThat(CustomURLScheme.remappedURLs.entrySet().iterator().next()) + .isEqualTo(new SimpleEntry<>(customSchemeURL, "file:" + filePath)); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue387/Issue387Test.java b/src/test/java/io/github/classgraph/issues/issue387/Issue387Test.java new file mode 100644 index 000000000..3c2b294a5 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue387/Issue387Test.java @@ -0,0 +1,70 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue387; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.AbstractMap.SimpleEntry; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import io.github.classgraph.features.CustomURLScheme; + +/** + * Test. + */ +class Issue387Test { + @BeforeAll + static void setup() { + new CustomURLScheme(); + } + + /** + * Test. + */ + @Test + void issue387Test() throws MalformedURLException { + final String filePath = Issue387Test.class.getClassLoader().getResource("nested-jars-level1.zip").getPath(); + final String customSchemeURL = CustomURLScheme.SCHEME + ":" + filePath; + final URL url = new URL(customSchemeURL); + final URLClassLoader classLoader = new URLClassLoader(new URL[] { url }, null); + try (ScanResult scanResult = new ClassGraph().enableURLScheme(CustomURLScheme.SCHEME) + .overrideClassLoaders(classLoader).scan()) { + assertThat(scanResult.getAllResources().getPaths()).containsExactly("level2.jar"); + assertThat(CustomURLScheme.remappedURLs.entrySet().iterator().next()) + .isEqualTo(new SimpleEntry<>(customSchemeURL, "file:" + filePath)); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue402/Foo.java b/src/test/java/io/github/classgraph/issues/issue402/Foo.java new file mode 100644 index 000000000..7fa409281 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue402/Foo.java @@ -0,0 +1,6 @@ +package io.github.classgraph.issues.issue402; + +class Foo { + class Bar { + } +} \ No newline at end of file diff --git a/src/test/java/io/github/classgraph/issues/issue402/Outer.java b/src/test/java/io/github/classgraph/issues/issue402/Outer.java new file mode 100644 index 000000000..054d7084f --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue402/Outer.java @@ -0,0 +1,24 @@ +package io.github.classgraph.issues.issue402; + +/** + * Nested types. + */ +class Outer { + class Middle { + class Inner1 { + } + } + + static class MiddleStatic { + class Inner2 { + } + + static class InnerStatic { + } + } + + class MiddleGeneric { + class InnerGeneric { + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue402/TypeAnnotationTest.java b/src/test/java/io/github/classgraph/issues/issue402/TypeAnnotationTest.java new file mode 100644 index 000000000..5709d4c0a --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue402/TypeAnnotationTest.java @@ -0,0 +1,217 @@ +package io.github.classgraph.issues.issue402; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassRefTypeSignature; +import io.github.classgraph.FieldInfo; +import io.github.classgraph.ScanResult; + +/** + * TypeAnnotationTest. + */ +class TypeAnnotationTest { + @Target({ ElementType.FIELD, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface A { + } + + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface B { + } + + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface C { + } + + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface D { + } + + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface E { + } + + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface F { + } + + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface G { + } + + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface H { + } + + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface I { + } + + @A + Map<@B ? extends @C String, @D List<@E Object>> map; + + @I + String @F [] @G [] @H [] arr; + + @A + List<@B Comparable<@F Object @C [] @D [] @E []>> comparable; + + @A + Outer.@B Middle.@C Inner1 inner1; + + Outer.@A MiddleStatic.@B Inner2 inner2; + + Outer.MiddleStatic.@A InnerStatic inner3; + + Outer.MiddleGeneric<@A Foo.@B Bar>.InnerGeneric<@D String @C []> inner4; + + static class X2 { + class Y2 { + class Z2 { + } + } + } + + static class X3 { + interface Y3 { + class Z3 { + } + } + } + + static class X4 { + static class Y4 { + class Z4 { + } + } + } + + List<@A X.@B Y.@C Z> xyz; + + List<@A X2.@B Y2.@C Z2> xyz2; + + List xyz3; + + List xyz4; + + static class U { + } + + interface V { + } + + <@A T extends @B U> @D U t(@E final T t) { + return null; + } + + static class P<@A T extends @B U & @C V> { + public void explicitReceiver(@F P this) { + } + } + + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface Size { + int max(); + } + + @A + class Person { + List<@Size(max = 50) String> emails; + } + + /** + * Convert class names to short names. + * + * @param type + * the type + * @return the class names as short names + */ + private static String shortNames(final Object type) { + return type.toString().replace(TypeAnnotationTest.class.getName() + ".", "") + .replace(TypeAnnotationTest.class.getName() + "$", "") + .replace(TypeAnnotationTest.class.getPackage().getName() + ".", "").replace("java.lang.", "") + .replace("java.util.", ""); + } + + /** Test field type annotations. */ + @Test + void typeAnnotations() { + try (ScanResult scanResult = new ClassGraph() + .acceptPackages(TypeAnnotationTest.class.getPackage().getName()).enableAllInfo().scan()) { + final ClassInfo classInfo = scanResult.getClassInfo(TypeAnnotationTest.class.getName()); + + final FieldInfo mapField = classInfo.getFieldInfo("map"); + assertThat(shortNames(mapField)).isEqualTo("@A Map<@B ? extends @C String, @D List<@E Object>> map"); + assertThat(mapField.toStringWithSimpleNames()) + .isEqualTo("@A Map<@B ? extends @C String, @D List<@E Object>> map"); + + final FieldInfo arrField = classInfo.getFieldInfo("arr"); + assertThat(shortNames(arrField)).isEqualTo("@I String @F [] @G [] @H [] arr"); + assertThat(arrField.toStringWithSimpleNames()).isEqualTo("@I String @F [] @G [] @H [] arr"); + + final FieldInfo comparableField = classInfo.getFieldInfo("comparable"); + assertThat(shortNames(comparableField)) + .isEqualTo("@A List<@B Comparable<@F Object @C [] @D [] @E []>> comparable"); + assertThat(comparableField.toStringWithSimpleNames()) + .isEqualTo("@A List<@B Comparable<@F Object @C [] @D [] @E []>> comparable"); + + final FieldInfo inner1Field = classInfo.getFieldInfo("inner1"); + assertThat(shortNames(inner1Field)).isEqualTo("@A Outer$@B Middle$@C Inner1 inner1"); + assertThat(inner1Field.toStringWithSimpleNames()).isEqualTo("@A @C Inner1 inner1"); + + assertThat(shortNames(classInfo.getFieldInfo("inner2"))) + .isEqualTo("Outer$@A MiddleStatic$@B Inner2 inner2"); + + assertThat(shortNames(classInfo.getFieldInfo("inner3"))) + .isEqualTo("Outer$MiddleStatic$@A InnerStatic inner3"); + + assertThat(shortNames(classInfo.getFieldInfo("inner4"))) + .isEqualTo("Outer$MiddleGeneric<@A Foo$@B Bar>$InnerGeneric<@D String @C []> inner4"); + + final FieldInfo xyzField = classInfo.getFieldInfo("xyz"); + assertThat(shortNames(xyzField)).isEqualTo("List<@A X$@B Y$@C Z> xyz"); + assertThat(xyzField.toStringWithSimpleNames()).isEqualTo("List<@C Z> xyz"); + + assertThat(shortNames(classInfo.getFieldInfo("xyz2"))).isEqualTo("List<@A X2$@B Y2$@C Z2> xyz2"); + + assertThat(shortNames(classInfo.getFieldInfo("xyz3"))).isEqualTo("List xyz3"); + + assertThat(shortNames(classInfo.getFieldInfo("xyz4"))).isEqualTo("List xyz4"); + + assertThat(shortNames(classInfo.getMethodInfo("t").get(0))) + .isEqualTo("<@A T extends @B U> @D U t(final @E T t)"); + + assertThat(classInfo.getMethodInfo("t").get(0).toStringWithSimpleNames()) + .isEqualTo("<@A T extends @B U> @D U t(final @E T t)"); + + final ClassInfo personClassInfo = scanResult.getClassInfo(Person.class.getName()); + + final FieldInfo emailsField = personClassInfo.getFieldInfo("emails"); + assertThat(shortNames(emailsField)).isEqualTo("List<@Size(max=50) String> emails"); + assertThat(emailsField.toStringWithSimpleNames()).isEqualTo("List<@Size(max=50) String> emails"); + + assertThat(shortNames(((ClassRefTypeSignature) emailsField.getTypeSignatureOrTypeDescriptor()) + .getTypeArguments().get(0).getTypeSignature().getTypeAnnotationInfo().get(0))) + .isEqualTo("@Size(max=50)"); + + assertThat(shortNames(personClassInfo)).isEqualTo("@A class Person"); + assertThat(personClassInfo.toStringWithSimpleNames()).isEqualTo("@A class Person"); + + final ClassInfo pClassInfo = scanResult.getClassInfo(P.class.getName()); + + assertThat(shortNames(pClassInfo)).isEqualTo("static class P<@A T extends @B U & @C V>"); + + assertThat(shortNames(pClassInfo.getMethodInfo("explicitReceiver").get(0) + .getTypeSignatureOrTypeDescriptor().getReceiverTypeAnnotationInfo().get(0))).isEqualTo("@F"); + + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue402/X.java b/src/test/java/io/github/classgraph/issues/issue402/X.java new file mode 100644 index 000000000..e15126639 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue402/X.java @@ -0,0 +1,8 @@ +package io.github.classgraph.issues.issue402; + +class X { + class Y { + class Z { + } + } +} \ No newline at end of file diff --git a/src/test/java/io/github/classgraph/issues/issue407/Issue407Test.java b/src/test/java/io/github/classgraph/issues/issue407/Issue407Test.java new file mode 100644 index 000000000..90296f6e1 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue407/Issue407Test.java @@ -0,0 +1,77 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue407; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.ops4j.pax.url.mvn.MavenResolvers; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Test. + */ +public class Issue407Test { + /** + * Test. + * + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Test + public void issue407Test() throws IOException { + // Resolve and download scala-library + final File resolvedFile = MavenResolvers.createMavenResolver(null, null).resolve("com.google.guava", + "guava", null, null, "25.0-jre"); + assertThat(resolvedFile).isFile(); + + // Create a new custom class loader + final ClassLoader classLoader = new URLClassLoader(new URL[] { resolvedFile.toURI().toURL() }, null); + + // Scan the classpath -- used to throw an exception for Stack, since companion object inherits + // from different class + try (ScanResult scanResult = new ClassGraph() // + .acceptPackages("com.google.thirdparty.publicsuffix") // + .overrideClassLoaders(classLoader) // + .scan()) { + final List classNames = scanResult // + .getAllClasses() // + .getNames(); + assertThat(classNames).contains("com.google.thirdparty.publicsuffix.PublicSuffixPatterns"); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue420/Issue420Test.java b/src/test/java/io/github/classgraph/issues/issue420/Issue420Test.java new file mode 100644 index 000000000..f4cecb075 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue420/Issue420Test.java @@ -0,0 +1,145 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue420; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import com.google.common.jimfs.Jimfs; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Issue193Test. + */ +public class Issue420Test { + /** + * Test accessing a jar over Jimfs. + * + * @throws IOException + * If an I/O exception occurred. + * @throws URISyntaxException + * If a URI is bad. + */ + @Test + public void testScanningFileBackedByFileSystem() throws IOException, URISyntaxException { + try (FileSystem memFs = Jimfs.newFileSystem()) { + final Path jarPath = Paths + .get(getClass().getClassLoader().getResource("multi-release-jar.jar").toURI()); + final Path memFsPath = memFs.getPath("multi-release-jar.jar"); + final Path memFsCopyOfJar = Files.copy(jarPath, memFsPath); + final URL memFsCopyOfJarURL = memFsCopyOfJar.toUri().toURL(); + + try (URLClassLoader childClassLoader = new URLClassLoader(new URL[] { memFsCopyOfJarURL }, + getClass().getClassLoader())) { + final ClassGraph classGraph = new ClassGraph().enableURLScheme(memFsCopyOfJarURL.getProtocol()) + .overrideClassLoaders(childClassLoader).ignoreParentClassLoaders().acceptPackages("mrj") + .enableAllInfo(); + try (ScanResult scanResult = classGraph.scan()) { + assertThat(scanResult.getClassInfo("mrj.Cls")).isNotNull(); + } + } + } + } + + /** + * Test accessing a package hierarchy in Jimfs. + * + * @param packageRootPrefix + * The package root prefix. + * @throws IOException + * If an I/O exception occurred. + * @throws URISyntaxException + * If a URI is bad. + */ + private void testDir(final String packageRootPrefix) throws IOException, URISyntaxException { + try (FileSystem memFs = Jimfs.newFileSystem()) { + final String packageName = "io.github.classgraph.issues.issue146"; + final String className = "CompiledWithJDK8"; + final String packagePath = packageName.replace('.', '/'); + final String classFullyQualifiedName = packageName + ".CompiledWithJDK8"; + final String classFilePath = classFullyQualifiedName.replace('.', '/') + ".class"; + final Path jarPath = Paths.get(Issue420Test.class.getClassLoader().getResource(classFilePath).toURI()); + final Path memFsDirPath = memFs.getPath(packageRootPrefix + packagePath); + Files.createDirectories(memFsDirPath); + final Path memFsFilePath = memFs.getPath(memFsDirPath + "/" + className + ".class"); + final Path memFsCopyOfClassFile = Files.copy(jarPath, memFsFilePath); + assertThat(Files.exists(memFsCopyOfClassFile)); + final Path memFsRoot = memFs.getPath(""); + final URL memFsRootURL = memFsRoot.toUri().toURL(); + try (URLClassLoader childClassLoader = new URLClassLoader(new URL[] { memFsRootURL }, + getClass().getClassLoader())) { + final ClassGraph classGraph = new ClassGraph().enableURLScheme(memFsRootURL.getProtocol()) + .overrideClassLoaders(childClassLoader).ignoreParentClassLoaders() + .acceptPackages(packageName).enableAllInfo(); + try (ScanResult scanResult = classGraph.scan()) { + assertThat(scanResult.getClassInfo(classFullyQualifiedName)).isNotNull(); + } + } + } + } + + /** + * Test accessing a package hierarchy rooted at the default dir of "work/" in Jimfs. + * + * @throws IOException + * If an I/O exception occurred. + * @throws URISyntaxException + * If a URI is bad. + */ + @Test + public void testScanningDirBackedByFileSystem() throws IOException, URISyntaxException { + testDir(""); + } + + /** + * Test accessing a package hierarchy rooted at "work/classes/" (i.e. with an automatically-detected package + * root) in Jimfs. + * + * @throws IOException + * If an I/O exception occurred. + * @throws URISyntaxException + * If a URI is bad. + */ + @Test + public void testScanningDirBackedByFileSystemWithPackageRoot() throws IOException, URISyntaxException { + testDir("classes/"); + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue431/Issue431Test.java b/src/test/java/io/github/classgraph/issues/issue431/Issue431Test.java new file mode 100644 index 000000000..07f33c51e --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue431/Issue431Test.java @@ -0,0 +1,101 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue431; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; + +/** + * Issue431Test. + */ +public class Issue431Test { + /** + * Class X. + */ + public static class X { + /** a */ + static final int a = Integer.MAX_VALUE; + /** b */ + static final long b = 2L; + /** c */ + static final short c = (short) 3; + /** d */ + static final char d = 'd'; + /** e */ + static final boolean e = true; + /** f */ + static final byte f = (byte) 10; + /** g */ + static final float g = 1.0f; + /** h */ + static final float h = 0.0f; + /** i */ + static final double i = 1.0d; + } + + /** + * Test field equality. + * + * @param fieldName + * The field name + * @param classInfo1 + * The first ClassInfo + * @param classInfo2 + * The second ClassInfo + */ + private void testFieldEquality(final String fieldName, final ClassInfo classInfo1, final ClassInfo classInfo2) { + assertThat(Objects.equals(classInfo1.getFieldInfo(fieldName).getConstantInitializerValue(), + classInfo2.getFieldInfo(fieldName).getConstantInitializerValue())).isTrue(); + } + + /** Test serializing and deserializing primitive types. */ + @Test + public void primitiveTypeSerialization() { + final ClassGraph classGraph = new ClassGraph().acceptPackages(Issue431Test.class.getPackage().getName()) + .enableAllInfo(); + try (ScanResult scanResult1 = classGraph.scan()) { + final ClassInfo classInfo1 = scanResult1.getClassInfo(X.class.getName()); + assertThat(classInfo1).isNotNull(); + final String jsonResult = scanResult1.toJSON(2); + final ScanResult scanResult2 = ScanResult.fromJSON(jsonResult); + final ClassInfo classInfo2 = scanResult2.getClassInfo(X.class.getName()); + assertThat(classInfo2).isNotNull(); + for (char fieldName = 'a'; fieldName <= 'i'; fieldName++) { + testFieldEquality("" + fieldName, classInfo1, classInfo2); + } + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue46/Issue46Test.java b/src/test/java/io/github/classgraph/issues/issue46/Issue46Test.java index 25317d0f8..3b9f79483 100644 --- a/src/test/java/io/github/classgraph/issues/issue46/Issue46Test.java +++ b/src/test/java/io/github/classgraph/issues/issue46/Issue46Test.java @@ -30,25 +30,25 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue46Test. + * Issue46Test. */ public class Issue46Test { - /** * Issue 46 test. */ @Test public void issue46Test() { - final String jarPath = Issue46Test.class.getClassLoader().getResource("nested-jars-level1.zip").getPath() - + "!level2.jar!level3.jar!classpath1/classpath2"; + final String jarPath = "jar:file://" + + Issue46Test.class.getClassLoader().getResource("nested-jars-level1.zip").getPath() + + "!/level2.jar!/level3.jar!/classpath1/classpath2"; try (ScanResult scanResult = new ClassGraph().overrideClasspath(jarPath).enableClassInfo().scan()) { - assertThat(scanResult.getAllClasses().getNames()).containsExactlyInAnyOrder("com.test.Test"); + assertThat(scanResult.getAllClasses().getNames()).containsOnly("com.test.Test"); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue468/Issue468Test.java b/src/test/java/io/github/classgraph/issues/issue468/Issue468Test.java new file mode 100644 index 000000000..cfa422a70 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue468/Issue468Test.java @@ -0,0 +1,87 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue468; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.FileNotFoundException; +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ResourceList; +import io.github.classgraph.ScanResult; + +/** + * Issue468Test. + */ +public class Issue468Test { + /** Scan */ + private static void scan(final ClassGraph classGraph) { + try (ScanResult scanResult = classGraph.scan()) { + final ResourceList resources = scanResult.getAllResources(); + assertThat(resources.size()).isEqualTo(1); + assertThat(resources.getPaths()).containsExactly("innerfile"); + } + } + + /** + * Test '+' signs in URLs. + * + * @throws Exception + * the exception + */ + @Test + public void testPlusSigns() throws Exception { + final URL url = Issue468Test.class.getClassLoader().getResource("issue468/x+y/z+w.jar"); + if (url == null) { + throw new FileNotFoundException(); + } + scan(new ClassGraph().acceptPackagesNonRecursive("").overrideClasspath(url)); + } + + /** + * Test "file:" URIs as strings, with and without the scheme. + * + * @throws Exception + * the exception + */ + @Test + public void testFileURIs() throws Exception { + final URL url = Issue468Test.class.getClassLoader().getResource("issue468/x+y/z+w.jar"); + if (url == null) { + throw new FileNotFoundException(); + } + final String urlStr = url.toString(); + scan(new ClassGraph().acceptPackagesNonRecursive("").overrideClasspath(urlStr)); + assertThat(urlStr).startsWith("file:"); + scan(new ClassGraph().acceptPackagesNonRecursive("").overrideClasspath(urlStr.substring(5))); + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue495/Issue495Test.java b/src/test/java/io/github/classgraph/issues/issue495/Issue495Test.java new file mode 100644 index 000000000..02855031d --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue495/Issue495Test.java @@ -0,0 +1,74 @@ +/* + * This file is part of ClassGraph. + * + * Author: Luke Hutchison + * + * Hosted at: https://github.com/classgraph/classgraph + * + * -- + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Luke Hutchison + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package io.github.classgraph.issues.issue495; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; + +/** + * Test. + */ +public class Issue495Test { + /** + * Test. + * + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Test + public void testScalaTypeSignatures() throws Exception { + final URL resourceURL = Issue495Test.class.getClassLoader().getResource("scalapackage.zip"); + assertThat(resourceURL).isNotNull(); + assertThat(new File(resourceURL.toURI())).canRead(); + final ClassLoader classLoader = new URLClassLoader(new URL[] { resourceURL }, null); + try (ScanResult scanResult = new ClassGraph() // + .enableClassInfo().enableInterClassDependencies() // + .acceptPackages("scalapackage") // + .overrideClassLoaders(classLoader) // + .scan()) { + final ClassInfoList allClasses = scanResult.getAllClasses(); + assertThat(allClasses.getNames()).containsOnly("scalapackage.ScalaClass"); + final ClassInfo scalaClassInfo = allClasses.get(0); + assertThat(scalaClassInfo.getTypeSignature()).isNotNull(); + final Class scalaClassClass = scalaClassInfo.loadClass(); + assertThat(scalaClassClass).isNotNull(); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue600/Issue600Test.java b/src/test/java/io/github/classgraph/issues/issue600/Issue600Test.java new file mode 100644 index 000000000..65dd4caf5 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue600/Issue600Test.java @@ -0,0 +1,100 @@ +package io.github.classgraph.issues.issue600; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.Resource; +import io.github.classgraph.ResourceList; +import io.github.classgraph.ScanResult; + +class Issue600Test { + private static final int BUFFER_SIZE = 8192; + private static final int EOF = -1; + + private final ClassGraph classGraph = new ClassGraph().enableClassInfo() + .acceptPackages(getClass().getPackage().getName()); + + @Test + void testResourcesCanBeOpened() { + try (ScanResult scanResult = classGraph.scan()) { + final ResourceList resources = scanResult.getAllResources(); + assertFalse(resources.isEmpty(), "Test is meaningless without resources to open."); + + // Check we can open the resources. + assertOpenCloseResources(resources); + + // And check we can reopen the resources. + assertOpenCloseResources(resources); + } + } + + @Test + void testResourcesCanBeRead() { + try (ScanResult scanResult = classGraph.scan()) { + final ResourceList resources = scanResult.getAllResources(); + assertFalse(resources.isEmpty(), "Test is meaningless without resources to open."); + + // Check we can read the resources. + assertReadCloseResources(resources); + + // Check we can reread the resources. + assertReadCloseResources(resources); + } + } + + private void assertOpenCloseResources(final ResourceList resources) { + for (final Resource resource : resources) { + assertDoesNotThrow(new Executable() { + @Override + public void execute() throws Throwable { + try (InputStream input = resource.open()) { + assertThat(consume(input)).isGreaterThan(0); + } + } + }, "Resource " + resource.getPath() + " should be closed."); + } + } + + private int consume(final InputStream input) throws IOException { + final byte[] buffer = new byte[BUFFER_SIZE]; + int totalBytes = 0; + int bytesRead; + while ((bytesRead = input.read(buffer)) != EOF) { + totalBytes += bytesRead; + } + return totalBytes; + } + + private void assertReadCloseResources(final ResourceList resources) { + for (final Resource resource : resources) { + assertDoesNotThrow(new Executable() { + @Override + public void execute() throws Throwable { + final ByteBuffer buffer = resource.read(); + try { + assertTrue(buffer.hasRemaining()); + } finally { + resource.close(); + } + } + }, "Resource " + resource.getPath() + " should be closed."); + } + } + + public interface Api { + } + + @SuppressWarnings("unused") + public static class Example implements Api { + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue673/Issue673Test.java b/src/test/java/io/github/classgraph/issues/issue673/Issue673Test.java new file mode 100644 index 000000000..cd8ecfb91 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue673/Issue673Test.java @@ -0,0 +1,37 @@ +package io.github.classgraph.issues.issue673; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URL; +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +class Issue673Test { + @Test + void testResourcesCanBeRead() { + // a has Class-Path manifest entry that points to b, b points to c + final URL aURL = Issue673Test.class.getClassLoader().getResource("issue673/a.zip"); + assertThat(aURL != null); + final URL bURL = Issue673Test.class.getClassLoader().getResource("issue673/b.zip"); + assertThat(bURL != null); + + // This succeeded before issue 673 was fixed + try (ScanResult scanResult = new ClassGraph().overrideClasspath(bURL, aURL).scan()) { + assertThat(scanResult.getClasspathFiles().stream().map(f -> f.getName()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList("b.zip", "c.zip", "a.zip")); + assertThat(scanResult.getAllResources().getPaths()).contains("C"); + } + + // This failed before issue 673 was fixed + try (ScanResult scanResult = new ClassGraph().overrideClasspath(aURL, bURL).scan()) { + assertThat(scanResult.getClasspathFiles().stream().map(f -> f.getName()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList("a.zip", "b.zip", "c.zip")); + assertThat(scanResult.getAllResources().getPaths()).contains("C"); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue694/Issue694Test.java b/src/test/java/io/github/classgraph/issues/issue694/Issue694Test.java new file mode 100644 index 000000000..c6b966da6 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue694/Issue694Test.java @@ -0,0 +1,44 @@ +package io.github.classgraph.issues.issue694; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.ScanResult; + +public class Issue694Test { + static class TestClass { + } + + public static > C test(final C collection) { + return collection; + } + + @Test + void methodWithParam() { + final ScanResult scan = new ClassGraph().acceptClasses(Issue694Test.class.getName()).enableAnnotationInfo() + .enableMethodInfo().scan(); + + final List foundMethods = new ArrayList<>(); + final List foundMethodInfo = new ArrayList<>(); + for (final ClassInfo info : scan.getAllStandardClasses()) { + for (final MethodInfo methodInfo : info.getDeclaredMethodInfo()) { + foundMethodInfo.add(methodInfo.toString()); + final Method method = methodInfo.loadClassAndGetMethod(); + foundMethods.add(method.toString()); + } + } + assertThat(foundMethodInfo).containsOnly( + "public static > C test(final C collection)"); + assertThat(foundMethods).containsOnly( + "public static java.util.Collection io.github.classgraph.issues.issue694.Issue694Test.test(java.util.Collection)"); + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue696/Issue696Test.java b/src/test/java/io/github/classgraph/issues/issue696/Issue696Test.java new file mode 100644 index 000000000..43525e894 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue696/Issue696Test.java @@ -0,0 +1,48 @@ +package io.github.classgraph.issues.issue696; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodParameterInfo; +import io.github.classgraph.ScanResult; +import io.github.classgraph.issues.issue696.Issue696Test.BrokenAnnotation.Dynamic; + +public class Issue696Test { + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + public static @interface Foo { + } + + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + public static @interface Bar { + } + + public static class BrokenAnnotation { + public class Dynamic { + public Dynamic(@Foo final String param1, @Bar final String param2) { + } + } + } + + @Test + void genericSuperclass() { + final ScanResult scanResult = new ClassGraph().acceptPackages(Issue696Test.class.getPackage().getName()) + .enableMethodInfo().enableAnnotationInfo().scan(); + final ClassInfo dynamic = scanResult.getClassInfo(Dynamic.class.getName()); + final MethodParameterInfo[] paramInfo = dynamic.getConstructorInfo().get(0).getParameterInfo(); + // Inner classes have an initial "mandated" param + assertThat(paramInfo.length).isEqualTo(3); + assertThat(paramInfo[0].getAnnotationInfo()).isEmpty(); + assertThat(paramInfo[1].getAnnotationInfo().get(0).getName()).isEqualTo(Foo.class.getName()); + assertThat(paramInfo[2].getAnnotationInfo().get(0).getName()).isEqualTo(Bar.class.getName()); + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue706/Issue706Test.java b/src/test/java/io/github/classgraph/issues/issue706/Issue706Test.java new file mode 100644 index 000000000..0e91258d7 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue706/Issue706Test.java @@ -0,0 +1,31 @@ +package io.github.classgraph.issues.issue706; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; +import io.github.classgraph.TypeArgument; +import io.github.classgraph.TypeVariableSignature; + +public class Issue706Test { + static public class GenericBase { + } + + static public class GenericBypass extends GenericBase { + } + + @Test + void genericSuperclass() { + final ScanResult scanResult = new ClassGraph().acceptPackages(Issue706Test.class.getPackage().getName()) + .enableClassInfo().scan(); + final ClassInfo bypassCls = scanResult.getClassInfo(GenericBypass.class.getName()); + final TypeArgument superclassArg = bypassCls.getTypeSignature().getSuperclassSignature() + .getSuffixTypeArguments().get(0).get(0); + final TypeVariableSignature superclassArgTVar = (TypeVariableSignature) superclassArg.getTypeSignature(); + final Object bypassTParamFromSuperclassArg = superclassArgTVar.resolve(); + assertThat(bypassTParamFromSuperclassArg.toString()).isEqualTo("T"); + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue735/Issue735Test.java b/src/test/java/io/github/classgraph/issues/issue735/Issue735Test.java new file mode 100644 index 000000000..ed634aaa0 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue735/Issue735Test.java @@ -0,0 +1,37 @@ +package io.github.classgraph.issues.issue735; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; + +public class Issue735Test { + interface Base { + T get(); + } + + static class Derived1 implements Base { + public String get() { + return null; + } + } + + static abstract class Derived2 implements Base { + } + + @Test + void genericSuperclass() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue735Test.class.getPackage().getName()) + .enableAllInfo().ignoreClassVisibility().ignoreMethodVisibility().scan()) { + final ClassInfo ci1 = scanResult.getClassInfo(Derived1.class.getName()); + assertThat(ci1.getMethodInfo().get(0).getTypeSignatureOrTypeDescriptor().getResultType().toString()) + .isEqualTo(String.class.getName()); + final ClassInfo ci2 = scanResult.getClassInfo(Derived2.class.getName()); + assertThat(ci2.getMethodInfo().get(0).getTypeSignatureOrTypeDescriptor().getResultType().toString()) + .isEqualTo("T"); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue739/Issue739Test.java b/src/test/java/io/github/classgraph/issues/issue739/Issue739Test.java new file mode 100644 index 000000000..760cc9548 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue739/Issue739Test.java @@ -0,0 +1,36 @@ +package io.github.classgraph.issues.issue739; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.Resource; +import io.github.classgraph.ScanResult; + +public class Issue739Test { + @Test + void wildcardPathSupport() { + final String relPath = "src/test/resources/"; + final HashSet paths = new HashSet<>(); + try (ScanResult scanResult = new ClassGraph().overrideClasspath(relPath + "*").scan()) { + scanResult.getAllResources().forEach(new Consumer() { + @Override + public void accept(final Resource r) { + final String path = r.toString(); + final int idx = path.indexOf(relPath); + if (idx >= 0) { + paths.add(path.substring(idx + relPath.length())); + } + } + }); + } + assertThat(paths).contains("issue673/a.zip"); + assertThat(paths).contains("multi-release-jar.src.zip!/multi-release-jar/src/main/java-9/module-info.java"); + assertThat(paths).contains("zip64.zip!/10046"); + assertThat(paths).contains("record.jar!/pkg/Record.class"); + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue74/Issue74Test.java b/src/test/java/io/github/classgraph/issues/issue74/Issue74Test.java index 74a43438d..99a8e6f6f 100644 --- a/src/test/java/io/github/classgraph/issues/issue74/Issue74Test.java +++ b/src/test/java/io/github/classgraph/issues/issue74/Issue74Test.java @@ -2,16 +2,15 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue74Test. + * Issue74Test. */ public class Issue74Test { - /** * The Interface Function. */ @@ -41,11 +40,11 @@ public class ImplementsFunction implements Function { */ @Test public void issue74() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(Issue74Test.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue74Test.class.getPackage().getName()) .scan()) { - assertThat(scanResult.getClassesImplementing(Function.class.getName()).getNames()) - .containsExactlyInAnyOrder(FunctionAdapter.class.getName(), ImplementsFunction.class.getName(), - ExtendsFunctionAdapter.class.getName()); + assertThat(scanResult.getClassesImplementing(Function.class).getNames()).containsOnly( + FunctionAdapter.class.getName(), ImplementsFunction.class.getName(), + ExtendsFunctionAdapter.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue741/TypeArgumentAnnotationTest.java b/src/test/java/io/github/classgraph/issues/issue741/TypeArgumentAnnotationTest.java new file mode 100644 index 000000000..e9364df0c --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue741/TypeArgumentAnnotationTest.java @@ -0,0 +1,62 @@ +package io.github.classgraph.issues.issue741; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.AnnotationInfoList; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassRefTypeSignature; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.MethodParameterInfo; +import io.github.classgraph.ScanResult; +import io.github.classgraph.TypeArgument; + +public class TypeArgumentAnnotationTest { + @Target({ ElementType.FIELD, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface A { + } + + @Target({ ElementType.FIELD, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface B { + String value(); + } + + @Target({ ElementType.FIELD, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface C { + Class t(); + } + + @Target({ ElementType.FIELD, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface D { + int n(); + } + + static class U { + } + + void setValueList(final List<@A @B("foo") @C(t = U.class) @D(n = 50) ?> valueList) { + } + + @Test + void typeArgumentAnnotation() { + try (final ScanResult scanResult = new ClassGraph() + .acceptPackages(TypeArgumentAnnotationTest.class.getPackage().getName()).enableAllInfo().scan()) { + final ClassInfo cls = scanResult.getClassInfo(TypeArgumentAnnotationTest.class.getName()); + final MethodInfo method = cls.getMethodInfo().get("setValueList").get(0); + final MethodParameterInfo parameterInfo = method.getParameterInfo()[0]; + final TypeArgument typeArgument = ((ClassRefTypeSignature) parameterInfo + .getTypeSignatureOrTypeDescriptor()).getTypeArguments().get(0); + final AnnotationInfoList annotationInfoList = typeArgument.getTypeAnnotationInfo(); + assertThat(annotationInfoList.get(0).toStringWithSimpleNames()).isEqualTo("@A"); + assertThat(annotationInfoList.get(1).toStringWithSimpleNames()).isEqualTo("@B(\"foo\")"); + assertThat(annotationInfoList.get(2).toStringWithSimpleNames()).isEqualTo("@C(t=U.class)"); + assertThat(annotationInfoList.get(3).toStringWithSimpleNames()).isEqualTo("@D(n=50)"); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue741/TypeParameterAnnotationTest.java b/src/test/java/io/github/classgraph/issues/issue741/TypeParameterAnnotationTest.java new file mode 100644 index 000000000..d00915824 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue741/TypeParameterAnnotationTest.java @@ -0,0 +1,58 @@ +package io.github.classgraph.issues.issue741; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.AnnotationInfoList; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.ScanResult; +import io.github.classgraph.TypeParameter; + +public class TypeParameterAnnotationTest { + @Target({ ElementType.FIELD, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface A { + } + + @Target({ ElementType.FIELD, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface B { + String value(); + } + + @Target({ ElementType.FIELD, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface C { + Class t(); + } + + @Target({ ElementType.FIELD, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + private static @interface D { + int n(); + } + + static class U { + } + + <@A @B("foo") @C(t = U.class) @D(n = 50) T> void setValue(final T value) { + } + + @Test + void typeParameterAnnotation() { + try (final ScanResult scanResult = new ClassGraph() + .acceptPackages(TypeParameterAnnotationTest.class.getPackage().getName()).enableAllInfo().scan()) { + final ClassInfo cls = scanResult.getClassInfo(TypeParameterAnnotationTest.class.getName()); + final MethodInfo method = cls.getMethodInfo().get("setValue").get(0); + final TypeParameter typeParameter = method.getTypeSignatureOrTypeDescriptor().getTypeParameters() + .get(0); + final AnnotationInfoList annotationInfoList = typeParameter.getTypeAnnotationInfo(); + assertThat(annotationInfoList.get(0).toStringWithSimpleNames()).isEqualTo("@A"); + assertThat(annotationInfoList.get(1).toStringWithSimpleNames()).isEqualTo("@B(\"foo\")"); + assertThat(annotationInfoList.get(2).toStringWithSimpleNames()).isEqualTo("@C(t=U.class)"); + assertThat(annotationInfoList.get(3).toStringWithSimpleNames()).isEqualTo("@D(n=50)"); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue766/Issue766Test.java b/src/test/java/io/github/classgraph/issues/issue766/Issue766Test.java new file mode 100644 index 000000000..30bc6df14 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue766/Issue766Test.java @@ -0,0 +1,37 @@ +package io.github.classgraph.issues.issue766; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URL; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +public class Issue766Test { + + @Test + public void testURLs() { + final URL url = Issue766Test.class.getResource("/issue766/ProjectWithAnnotations.iar"); + + final String fileUrl = "file:" + url.getPath(); + final String jarFileUrl = "jar:file:" + url.getPath(); + final String jarUrl = "jar:///" + url.getPath(); + + assertThat(scan("javax.annotation.ManagedBean", fileUrl)).containsOnly("ch.ivyteam.test.MyManagedBean"); + assertThat(scan("javax.annotation.ManagedBean", jarFileUrl)).containsOnly("ch.ivyteam.test.MyManagedBean"); + assertThat(scan("javax.annotation.ManagedBean", jarUrl)).containsOnly("ch.ivyteam.test.MyManagedBean"); + } + + public static Set scan(final String annotation, final String urlStr) { + final ClassGraph classGraph = new ClassGraph().overrideClasspath(urlStr).disableNestedJarScanning() + .enableAnnotationInfo(); + try (ScanResult result = classGraph.scan()) { + return result.getClassesWithAnnotation(annotation).getStandardClasses().getNames().stream() + .collect(Collectors.toSet()); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue772/ExampleA.java b/src/test/java/io/github/classgraph/issues/issue772/ExampleA.java new file mode 100644 index 000000000..2c6917afd --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue772/ExampleA.java @@ -0,0 +1,12 @@ +package io.github.classgraph.issues.issue772; + +/** + * Test case A for selecting the 'Close' method of Child. Rather simple case of symmetrical extending classes. + */ +@SuppressWarnings("unused") +public abstract class ExampleA implements AutoCloseable { + + public abstract static class Child extends ExampleA implements MyCloseable { + + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue772/ExampleB.java b/src/test/java/io/github/classgraph/issues/issue772/ExampleB.java new file mode 100644 index 000000000..461e394af --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue772/ExampleB.java @@ -0,0 +1,9 @@ +package io.github.classgraph.issues.issue772; + +@SuppressWarnings("unused") +public abstract class ExampleB implements MyCloseable { + + public abstract static class Child extends ExampleB implements AutoCloseable { + + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue772/ExampleC.java b/src/test/java/io/github/classgraph/issues/issue772/ExampleC.java new file mode 100644 index 000000000..0eed9b13a --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue772/ExampleC.java @@ -0,0 +1,11 @@ +package io.github.classgraph.issues.issue772; + +@SuppressWarnings("unused") +public abstract class ExampleC implements AutoCloseable { + + public abstract void close(); + + public abstract static class Child extends ExampleC implements MyCloseable { + + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue772/MethodOverrideOrderTest.java b/src/test/java/io/github/classgraph/issues/issue772/MethodOverrideOrderTest.java new file mode 100644 index 000000000..9ff864286 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue772/MethodOverrideOrderTest.java @@ -0,0 +1,77 @@ +package io.github.classgraph.issues.issue772; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodInfoList; +import io.github.classgraph.ScanResult; + +/** + * Tests the order of classes overriding one another when selecting methods + */ +public class MethodOverrideOrderTest { + + private static ScanResult scanResult; + + @BeforeAll + public static void setup() { + scanResult = new ClassGraph().acceptPackages(MethodOverrideOrderTest.class.getPackage().getName()) + .enableMethodInfo().scan(); + } + + @AfterAll + public static void teardown() { + scanResult.close(); + scanResult = null; + } + + /** + * Tests if the correct method is selected if a class implements from two interfaces that inherit from another. + * Case of the child class implementing the inherited interface. + */ + @Test + public void interfaceMethodOrderingA() { + final ClassInfo classInfo = scanResult.getClassInfo("io.github.classgraph.issues.issue772.ExampleA$Child"); + assertThat(classInfo).isNotNull(); + final MethodInfoList closeMethods = classInfo.getMethodInfo("close"); + assertThat(closeMethods.size()).isEqualTo(1); + assertThat(closeMethods.get(0).getClassInfo().getName()) + .isEqualTo("io.github.classgraph.issues.issue772.MyCloseable"); + //Reflection in JDK8 will source the method AutoCloseable as well, works as expected from at least JDK11+ + // ClassLoader.getSystemClassLoader().loadClass("io.github.classgraph.issues.issue772.ExampleA$Child").getMethod("close") + } + + /** + * Tests if the correct method is selected if a class implements from two interfaces that inherit from another. + * Case of the child class implementing the inherited interface. + */ + @Test + public void interfaceMethodOrderingB() { + final ClassInfo classInfo = scanResult.getClassInfo("io.github.classgraph.issues.issue772.ExampleB$Child"); + assertThat(classInfo).isNotNull(); + final MethodInfoList closeMethods = classInfo.getMethodInfo("close"); + assertThat(closeMethods.size()).isEqualTo(1); + assertThat(closeMethods.get(0).getClassInfo().getName()) + .isEqualTo("io.github.classgraph.issues.issue772.MyCloseable"); + } + + /** + * Tests if the correct method is selected if a class implements from two interfaces that inherit from another. + * Case of the child class implementing the inherited interface. + */ + @Test + public void interfaceMethodOrderingC() { + final ClassInfo classInfo = scanResult.getClassInfo("io.github.classgraph.issues.issue772.ExampleC$Child"); + assertThat(classInfo).isNotNull(); + final MethodInfoList closeMethods = classInfo.getMethodInfo("close"); + assertThat(closeMethods.size()).isEqualTo(1); + assertThat(closeMethods.get(0).getClassInfo().getName()) + .isEqualTo("io.github.classgraph.issues.issue772.ExampleC"); + } + +} diff --git a/src/test/java/io/github/classgraph/issues/issue772/MyCloseable.java b/src/test/java/io/github/classgraph/issues/issue772/MyCloseable.java new file mode 100644 index 000000000..574fe96f8 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue772/MyCloseable.java @@ -0,0 +1,13 @@ +package io.github.classgraph.issues.issue772; + +import java.io.Closeable; + +/** + * An interface overriding another, its methods should always be 'preferred' before methods from Closeable + */ +public interface MyCloseable extends Closeable { + + //override close without the exception, good candidate for a default method in JDK8+ + @Override + void close(); +} diff --git a/src/test/java/io/github/classgraph/issues/issue78/Issue78Test.java b/src/test/java/io/github/classgraph/issues/issue78/Issue78Test.java index 7fc451210..0ce505995 100644 --- a/src/test/java/io/github/classgraph/issues/issue78/Issue78Test.java +++ b/src/test/java/io/github/classgraph/issues/issue78/Issue78Test.java @@ -2,24 +2,22 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue78Test. + * Issue78Test. */ public class Issue78Test { - /** * Issue 78. */ @Test public void issue78() { - try (ScanResult scanResult = new ClassGraph().whitelistClasses(Issue78Test.class.getName()).scan()) { - assertThat(scanResult.getAllClasses().getNames()) - .containsExactlyInAnyOrder(Issue78Test.class.getName()); + try (ScanResult scanResult = new ClassGraph().acceptClasses(Issue78Test.class.getName()).scan()) { + assertThat(scanResult.getAllClasses().getNames()).containsOnly(Issue78Test.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue780/Issue780Test.java b/src/test/java/io/github/classgraph/issues/issue780/Issue780Test.java new file mode 100644 index 000000000..5e9ba2d5b --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue780/Issue780Test.java @@ -0,0 +1,22 @@ +package io.github.classgraph.issues.issue780; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +public class Issue780Test { + /** + * Issue 780. + */ + @Test + public void getResourcesWithPathShouldNeverReturnNull() { + try (ScanResult result = new ClassGraph().scan()) { + for (int i = 0; i < 10; i++) { + assertThat(result.getResourcesWithPath("/some/non/existing/path")).isNotNull().isEmpty(); + } + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue797/Issue797Test.java b/src/test/java/io/github/classgraph/issues/issue797/Issue797Test.java new file mode 100644 index 000000000..6ec88436b --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue797/Issue797Test.java @@ -0,0 +1,35 @@ +package io.github.classgraph.issues.issue797; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.ScanResult; + +public class Issue797Test { + /** + * Issue 797. + */ + @Test + public void getResourcesWithPathShouldNeverReturnNull() { + // Jar is precompiled, since it uses a JDK 17 feature (records) + final URL url = Issue797Test.class.getResource("/issue797.jar"); + try (ScanResult result = new ClassGraph().overrideClasspath(url).enableAllInfo().scan()) { + final ClassInfo bar = result.getClassInfo("io.github.classgraph.issues.issue797.Bar"); + assertThat(bar.toString()).isEqualTo( + "public final record io.github.classgraph.issues.issue797.Bar(" + "java.lang.String baz, " + + "java.util.List<@jakarta.validation.constraints.NotNull java.lang.String> value) " + + "extends java.lang.Record"); + final MethodInfo baz = bar.getMethodInfo("baz").get(0); + assertThat(baz.toString()).isEqualTo("public java.lang.String baz()"); + final MethodInfo value = bar.getMethodInfo("value").get(0); + assertThat(value.toString()).isEqualTo( + "public java.util.List<@jakarta.validation.constraints.NotNull java.lang.String> " + "value()"); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue80/Issue80Test.java b/src/test/java/io/github/classgraph/issues/issue80/Issue80Test.java index 7b4dcd686..99d904c66 100644 --- a/src/test/java/io/github/classgraph/issues/issue80/Issue80Test.java +++ b/src/test/java/io/github/classgraph/issues/issue80/Issue80Test.java @@ -2,16 +2,15 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue80Test. + * Issue80Test. */ public class Issue80Test { - /** * Issue 80. */ diff --git a/src/test/java/io/github/classgraph/issues/issue804/Issue804Test.java b/src/test/java/io/github/classgraph/issues/issue804/Issue804Test.java new file mode 100644 index 000000000..c0f9cc890 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue804/Issue804Test.java @@ -0,0 +1,67 @@ +package io.github.classgraph.issues.issue804; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Issue 804. + */ +public class Issue804Test { + + private static final String NESTED_EXAMPLE_CLASS = "org.springframework.util.ResourceUtils"; + + @Test + void scanningNestedJarsInPathsContainingSpacesShouldNeverFail(@TempDir final Path tempDir) throws IOException { + final Path targetJar = createSpringBootJarInExampleDirectory(tempDir, "directory with spaces"); + + try (ScanResult scanResult = scanJar(targetJar)) { + assertThat(scanResult.getClassInfo(NESTED_EXAMPLE_CLASS)).isNotNull(); + } + } + + @Test + void scanningNestedJarsInPathsContainingHashesShouldNeverFail(@TempDir final Path tempDir) throws IOException { + final Path targetJar = createSpringBootJarInExampleDirectory(tempDir, "directory-without-spaces#123"); + + try (ScanResult scanResult = scanJar(targetJar)) { + assertThat(scanResult.getClassInfo(NESTED_EXAMPLE_CLASS)).isNotNull(); + } + } + + @Test + void scanningNestedJarsInPathsContainingSpacesAndHashesShouldNeverFail(@TempDir final Path tempDir) + throws IOException { + final Path targetJar = createSpringBootJarInExampleDirectory(tempDir, "directory with spaces #123"); + + try (ScanResult scanResult = scanJar(targetJar)) { + assertThat(scanResult.getClassInfo(NESTED_EXAMPLE_CLASS)).isNotNull(); + } + } + + private Path createSpringBootJarInExampleDirectory(final Path temporaryDirectory, final String directoryName) + throws IOException { + final Path directoryWithSpaces = temporaryDirectory.resolve(directoryName); + Files.createDirectories(directoryWithSpaces); + final Path nestedJar = directoryWithSpaces.resolve("spring-boot-fully-executable-jar.jar"); + try (InputStream nestedJarsExample = Issue804Test.class.getClassLoader() + .getResourceAsStream("spring-boot-fully-executable-jar.jar")) { + Files.copy(nestedJarsExample, nestedJar); + } + return nestedJar; + } + + private ScanResult scanJar(final Path targetJar) { + return new ClassGraph().enableClassInfo().overrideClasspath(targetJar.toUri()).scan(); + } + +} diff --git a/src/test/java/io/github/classgraph/issues/issue83/Issue83Test.java b/src/test/java/io/github/classgraph/issues/issue83/Issue83Test.java index bf4a1a556..4db37dcd3 100644 --- a/src/test/java/io/github/classgraph/issues/issue83/Issue83Test.java +++ b/src/test/java/io/github/classgraph/issues/issue83/Issue83Test.java @@ -6,7 +6,7 @@ import java.util.ArrayList; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.Resource; @@ -14,22 +14,21 @@ import io.github.classgraph.ScanResult; /** - * The Class Issue83Test. + * Issue83Test. */ public class Issue83Test { - /** The Constant jarPathURL. */ private static final URL jarPathURL = Issue83Test.class.getClassLoader().getResource("nested-jars-level1.zip"); /** - * Jar whitelist. + * Jar accept. */ @Test - public void jarWhitelist() { + public void jarAccept() { assertThat(jarPathURL).isNotNull(); final List paths = new ArrayList<>(); try (ScanResult scanResult = new ClassGraph().overrideClasspath(jarPathURL) - .whitelistJars("nested-jars-level1.zip").scan()) { + .acceptJars("nested-jars-level1.zip").scan()) { final ResourceList resources = scanResult.getAllResources(); for (final Resource res : resources) { paths.add(res.getPath()); @@ -39,14 +38,14 @@ public void jarWhitelist() { } /** - * Jar blacklist. + * Jar reject. */ @Test - public void jarBlacklist() { + public void jarReject() { assertThat(jarPathURL).isNotNull(); final ArrayList paths = new ArrayList<>(); try (ScanResult scanResult = new ClassGraph().overrideClasspath(jarPathURL) - .blacklistJars("nested-jars-level1.zip").scan()) { + .rejectJars("nested-jars-level1.zip").scan()) { final ResourceList resources = scanResult.getAllResources(); for (final Resource res : resources) { paths.add(res.getPath()); diff --git a/src/test/java/io/github/classgraph/issues/issue854/Issue854Test.java b/src/test/java/io/github/classgraph/issues/issue854/Issue854Test.java new file mode 100644 index 000000000..71f277fb8 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue854/Issue854Test.java @@ -0,0 +1,36 @@ +package io.github.classgraph.issues.issue854; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassRefTypeSignature; +import io.github.classgraph.ScanResult; + +class Issue854Test { + @Test + void getFullyQualifiedClassName() { + final ClassLoader mainClassLoader = Issue854Test.class.getClassLoader(); + final ScanResult scanResult = new ClassGraph().enableClassInfo().enableAnnotationInfo() + .ignoreClassVisibility().ignoreFieldVisibility().ignoreMethodVisibility() + .overrideClassLoaders(mainClassLoader).acceptPackages("com.google.common.collect").scan(); + + final String anonymousClass = "com.google.common.collect.TreeRangeMap$SubRangeMap$1"; + final ClassInfo classInfo = scanResult.getClassInfo(anonymousClass); + final ClassRefTypeSignature signature = classInfo.getTypeSignatureOrTypeDescriptor() + .getSuperclassSignature(); + + // Before the fix to 854, this would give the following, because type parameter token parsing + // did not stop at '.': + // com.google.common.collect.TreeRangeMap$SubRangeMap.SubRangeMapAsMap + // But the fully-qualified class name in the classfile is: + // com.google.common.collect.TreeRangeMap$SubRangeMap$SubRangeMapAsMap + final String subRangeMapAsMapClassName = signature.getFullyQualifiedClassName(); + assertThat(subRangeMapAsMapClassName) + .isEqualTo("com.google.common.collect.TreeRangeMap$SubRangeMap$SubRangeMapAsMap"); + assertNotNull(scanResult.getClassInfo(subRangeMapAsMapClassName)); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/classgraph/issues/issue897/Issue897Test.java b/src/test/java/io/github/classgraph/issues/issue897/Issue897Test.java new file mode 100644 index 000000000..ef0a60a93 --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue897/Issue897Test.java @@ -0,0 +1,57 @@ +package io.github.classgraph.issues.issue897; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.Target; +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.Resource; +import io.github.classgraph.ScanResult; + +/** + * Issue897. + */ +public class Issue897Test { + /** + * Inner class that uses another inner class. + */ + private class Inner1 { + public Inner1(@Anno Inner2 i) {} + } + + /** + * Other inner class. + */ + public class Inner2 { + } + + /** + * Type-use anntotation. + */ + @Target(TYPE_USE) + @interface Anno {} + + /** + * Test that the annotation is attached to the first "source-code" parameter of the Inner1 + * constructor, not to the compiler-generated parameter for the enclosing class. + */ + @Test + public void annotationOnInnerClassConstructor() { + try (ScanResult scanResult = new ClassGraph().acceptClasses(Inner1.class.getName()) + .ignoreClassVisibility().enableMethodInfo().enableAnnotationInfo().scan()) { + final ClassInfo classInfo = scanResult.getClassInfo(Inner1.class.getName()); + final MethodInfo methodInfo = classInfo.getDeclaredConstructorInfo().get(0); + // TODO: Attach the annotation to the source-code parameter instead of crashing. + Assertions.assertThrows(IllegalArgumentException.class, + () -> methodInfo.getTypeSignatureOrTypeDescriptor()); + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue920/Issue920Test.java b/src/test/java/io/github/classgraph/issues/issue920/Issue920Test.java new file mode 100644 index 000000000..209b73c2a --- /dev/null +++ b/src/test/java/io/github/classgraph/issues/issue920/Issue920Test.java @@ -0,0 +1,35 @@ +package io.github.classgraph.issues.issue920; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.MethodInfoList; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Modifier; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * ClassGraph used to return incorrect modifiers for non-public constructors if + * there is a public constructor of same signature in the superclass AND `ignoreMethodVisibility` has not been set. + * In that case it will instead return the super's constructor's modifiers. + */ +public class Issue920Test { + @Test + void test() { + MethodInfoList constructors = new ClassGraph() + .enableAnnotationInfo() + .enableSystemJarsAndModules() + .enableClassInfo() + .enableMethodInfo() + .scan() + .getClassInfo("java.io.ObjectOutputStream") + .getConstructorInfo(); + for (MethodInfo constructor : constructors) { + if (constructor.getParameterInfo().length == 0) { + // The no args constructor of ObjectOutputStream is protected + assertEquals(Modifier.PROTECTED, constructor.getModifiers(), "The no-args constructor of ObjectOutputStream should read as `protected`"); + } + } + } +} diff --git a/src/test/java/io/github/classgraph/issues/issue93/Issue93.java b/src/test/java/io/github/classgraph/issues/issue93/Issue93Test.java similarity index 68% rename from src/test/java/io/github/classgraph/issues/issue93/Issue93.java rename to src/test/java/io/github/classgraph/issues/issue93/Issue93Test.java index 8c11940a3..cf03090f8 100644 --- a/src/test/java/io/github/classgraph/issues/issue93/Issue93.java +++ b/src/test/java/io/github/classgraph/issues/issue93/Issue93Test.java @@ -5,18 +5,17 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue93. + * Issue93. */ -public class Issue93 { - +public class Issue93Test { /** The Constant PKG. */ - private static final String PKG = Issue93.class.getPackage().getName(); + private static final String PKG = Issue93Test.class.getPackage().getName(); /** * The Interface RetentionClass. @@ -49,12 +48,12 @@ static class RetentionRuntimeAnnotated { /** Test that both CLASS-retained and RUNTIME-retained annotations are visible by default. */ @Test public void classRetentionIsDefault() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(PKG).enableAnnotationInfo() + try (ScanResult scanResult = new ClassGraph().acceptPackages(PKG).enableAnnotationInfo() .ignoreClassVisibility().scan()) { - assertThat(scanResult.getClassesWithAnnotation(RetentionClass.class.getName()).getNames()) - .containsExactlyInAnyOrder(RetentionClassAnnotated.class.getName()); - assertThat(scanResult.getClassesWithAnnotation(RetentionRuntime.class.getName()).getNames()) - .containsExactlyInAnyOrder(RetentionRuntimeAnnotated.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(RetentionClass.class).getNames()) + .containsOnly(RetentionClassAnnotated.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(RetentionRuntime.class).getNames()) + .containsOnly(RetentionRuntimeAnnotated.class.getName()); } } @@ -64,11 +63,11 @@ public void classRetentionIsDefault() { */ @Test public void classRetentionIsNotVisibleWithRetentionPolicyRUNTIME() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(PKG).enableAnnotationInfo() + try (ScanResult scanResult = new ClassGraph().acceptPackages(PKG).enableAnnotationInfo() .ignoreClassVisibility().disableRuntimeInvisibleAnnotations().scan()) { - assertThat(scanResult.getClassesWithAnnotation(RetentionClass.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesWithAnnotation(RetentionRuntime.class.getName()).getNames()) - .containsExactlyInAnyOrder(RetentionRuntimeAnnotated.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(RetentionClass.class).getNames()).isEmpty(); + assertThat(scanResult.getClassesWithAnnotation(RetentionRuntime.class).getNames()) + .containsOnly(RetentionRuntimeAnnotated.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/issues/issue99/Issue99Test.java b/src/test/java/io/github/classgraph/issues/issue99/Issue99Test.java index 8df525d45..22b73516e 100644 --- a/src/test/java/io/github/classgraph/issues/issue99/Issue99Test.java +++ b/src/test/java/io/github/classgraph/issues/issue99/Issue99Test.java @@ -30,36 +30,35 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; /** - * The Class Issue99Test. + * Issue99Test. */ public class Issue99Test { - /** The Constant jarPath. */ private static final String jarPath = Issue99Test.class.getClassLoader().getResource("nested-jars-level1.zip") .getPath() + "!level2.jar!level3.jar!classpath1/classpath2"; /** - * Test without blacklist. + * Test without reject. */ @Test - public void testWithoutBlacklist() { + public void testWithoutReject() { try (ScanResult scanResult = new ClassGraph().overrideClasspath(jarPath).enableClassInfo().scan()) { - assertThat(scanResult.getAllClasses().getNames()).containsExactlyInAnyOrder("com.test.Test"); + assertThat(scanResult.getAllClasses().getNames()).containsOnly("com.test.Test"); } } /** - * Test with blacklist. + * Test with reject. */ @Test - public void testWithBlacklist() { - try (ScanResult scanResult = new ClassGraph().overrideClasspath(jarPath).blacklistJars("level3.jar") + public void testWithReject() { + try (ScanResult scanResult = new ClassGraph().overrideClasspath(jarPath).rejectJars("level3.jar") .enableClassInfo().scan()) { assertThat(scanResult.getAllClasses().getNames()).isEmpty(); } diff --git a/src/test/java/io/github/classgraph/json/AnnotationDefaultVals.java b/src/test/java/io/github/classgraph/json/AnnotationDefaultVals.java new file mode 100644 index 000000000..5b9854bde --- /dev/null +++ b/src/test/java/io/github/classgraph/json/AnnotationDefaultVals.java @@ -0,0 +1,52 @@ +package io.github.classgraph.json; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + */ +@SuppressWarnings("unused") +public class AnnotationDefaultVals { + @Retention(RetentionPolicy.RUNTIME) + @interface MyAnnotation { + String msg() default "hello"; + } + + @MyAnnotation + class MyClass { + } + + /** + * Test serialize then deserialize scan result. + */ + @Test + public void testSerializeThenDeserializeWithAnnotation() { + // Get URL base for overriding classpath (otherwise the JSON representation of the ScanResult won't be + // the same after the first and second deserialization, because overrideClasspath is set by the first + // serialization for consistency.) + final String classfileURL = getClass().getClassLoader() + .getResource(AnnotationDefaultVals.class.getName().replace('.', '/') + ".class").toString(); + final String classpathBase = classfileURL.substring(0, + classfileURL.length() - (AnnotationDefaultVals.class.getName().length() + 6)); + try (ScanResult scanResult = new ClassGraph().overrideClasspath(classpathBase) + .acceptPackagesNonRecursive(AnnotationDefaultVals.class.getPackage().getName()) + .ignoreClassVisibility().enableAllInfo().scan()) { + assertThat(scanResult.getClassInfo(MyClass.class.getName()).getAnnotationInfo().get(0) + .getDefaultParameterValues().get(0).getValue()).isEqualTo("hello"); + final int indent = 2; + final String scanResultJSON = scanResult.toJSON(indent); + final ScanResult scanResultDeserialized = ScanResult.fromJSON(scanResultJSON); + final String scanResultReserializedJSON = scanResultDeserialized.toJSON(indent); + assertThat(scanResultReserializedJSON).isEqualTo(scanResultJSON); + assertThat(scanResultDeserialized.getClassInfo(MyClass.class.getName()).getAnnotationInfo().get(0) + .getDefaultParameterValues().get(0).getValue()).isEqualTo("hello"); + } + } +} diff --git a/src/test/java/io/github/classgraph/json/JSONSerializationTest.java b/src/test/java/io/github/classgraph/json/JSONSerializationTest.java index 068aa96cb..5f0e2a986 100644 --- a/src/test/java/io/github/classgraph/json/JSONSerializationTest.java +++ b/src/test/java/io/github/classgraph/json/JSONSerializationTest.java @@ -7,19 +7,19 @@ import java.util.List; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import nonapi.io.github.classgraph.json.JSONDeserializer; import nonapi.io.github.classgraph.json.JSONSerializer; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; /** - * The Class JSONSerializationTest. + * JSONSerializationTest. */ @SuppressWarnings("unused") public class JSONSerializationTest { - /** * The Class A. * @@ -39,6 +39,7 @@ private static class A { /** * Constructor. */ + @SuppressWarnings("null") public A() { } @@ -70,6 +71,7 @@ private static class B { /** * Constructor. */ + @SuppressWarnings("null") public B() { } @@ -148,6 +150,7 @@ private static class D { /** * Constructor. */ + @SuppressWarnings("null") public D() { } @@ -250,13 +253,14 @@ public void testJSON() { final H h = new H(); h.g = new G(); + final ReflectionUtils reflectionUtils = new ReflectionUtils(); final String json0 = JSONSerializer.serializeFromField(h, "g", 0, false); final String expected = // - "{\"e\":{\"q\":{\"b\":3,\"a\":{\"x\":[3],\"y\":\"x\"},\"arr\":[3,3,3]},\"map\":{\"3\":3}," - + "\"list\":[3,3,3],\"c\":{\"b\":5,\"a\":{\"x\":[5],\"y\":\"x\"},\"arr\":[5,5,5]}," - + "\"z\":42},\"f\":{\"z\":1.5,\"q\":{\"b\":1.5,\"a\":{\"x\":[1.5],\"y\":\"x\"}," - + "\"arr\":[1.5,1.5,1.5]},\"map\":{\"1.5\":1.5},\"list\":[1.5,1.5,1.5],\"wxy\":123}}"; + "{\"e\":{\"list\":[3,3,3],\"map\":{\"3\":3},\"q\":{\"b\":3,\"a\":{\"x\":[3],\"y\":\"x\"}," + + "\"arr\":[3,3,3]}," + "\"c\":{\"b\":5,\"a\":{\"x\":[5],\"y\":\"x\"},\"arr\":[5,5,5]}," + + "\"z\":42},\"f\":{\"list\":[1.5,1.5,1.5],\"map\":{\"1.5\":1.5}," + "\"q\":{\"b\":1.5," + + "\"a\":{\"x\":[1.5],\"y\":\"x\"},\"arr\":[1.5,1.5,1.5]},\"z\":1.5,\"wxy\":123}}"; assertThat(json0).isEqualTo(expected); @@ -280,7 +284,7 @@ public void testSerializeThenDeserializeScanResult() { final String classpathBase = classfileURL.substring(0, classfileURL.length() - (JSONSerializationTest.class.getName().length() + 6)); try (ScanResult scanResult = new ClassGraph().overrideClasspath(classpathBase) - .whitelistPackagesNonRecursive(JSONSerializationTest.class.getPackage().getName()) + .acceptPackagesNonRecursive(JSONSerializationTest.class.getPackage().getName()) .ignoreClassVisibility().scan()) { final int indent = 2; final String scanResultJSON = scanResult.toJSON(indent); diff --git a/src/test/java/io/github/classgraph/test/ClassGraphTest.java b/src/test/java/io/github/classgraph/test/ClassGraphTest.java index c9ed595ea..eeeed4ab0 100644 --- a/src/test/java/io/github/classgraph/test/ClassGraphTest.java +++ b/src/test/java/io/github/classgraph/test/ClassGraphTest.java @@ -30,48 +30,48 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.FieldInfo; import io.github.classgraph.Resource; -import io.github.classgraph.ResourceList.ByteArrayConsumer; +import io.github.classgraph.ResourceList.ByteArrayConsumerThrowsIOException; import io.github.classgraph.ScanResult; -import io.github.classgraph.test.blacklisted.BlacklistedAnnotation; -import io.github.classgraph.test.blacklisted.BlacklistedSubclass; -import io.github.classgraph.test.blacklisted.BlacklistedSubinterface; -import io.github.classgraph.test.blacklisted.BlacklistedSuperclass; -import io.github.classgraph.test.whitelisted.Cls; -import io.github.classgraph.test.whitelisted.ClsSub; -import io.github.classgraph.test.whitelisted.ClsSubSub; -import io.github.classgraph.test.whitelisted.Iface; -import io.github.classgraph.test.whitelisted.IfaceSub; -import io.github.classgraph.test.whitelisted.IfaceSubSub; -import io.github.classgraph.test.whitelisted.Impl1; -import io.github.classgraph.test.whitelisted.Impl1Sub; -import io.github.classgraph.test.whitelisted.Impl1SubSub; -import io.github.classgraph.test.whitelisted.Impl2; -import io.github.classgraph.test.whitelisted.Impl2Sub; -import io.github.classgraph.test.whitelisted.Impl2SubSub; -import io.github.classgraph.test.whitelisted.StaticField; -import io.github.classgraph.test.whitelisted.Whitelisted; -import io.github.classgraph.test.whitelisted.WhitelistedInterface; -import io.github.classgraph.test.whitelisted.blacklistedsub.BlacklistedSub; +import io.github.classgraph.test.accepted.Accepted; +import io.github.classgraph.test.accepted.AcceptedInterface; +import io.github.classgraph.test.accepted.Cls; +import io.github.classgraph.test.accepted.ClsSub; +import io.github.classgraph.test.accepted.ClsSubSub; +import io.github.classgraph.test.accepted.Iface; +import io.github.classgraph.test.accepted.IfaceSub; +import io.github.classgraph.test.accepted.IfaceSubSub; +import io.github.classgraph.test.accepted.Impl1; +import io.github.classgraph.test.accepted.Impl1Sub; +import io.github.classgraph.test.accepted.Impl1SubSub; +import io.github.classgraph.test.accepted.Impl2; +import io.github.classgraph.test.accepted.Impl2Sub; +import io.github.classgraph.test.accepted.Impl2SubSub; +import io.github.classgraph.test.accepted.StaticField; +import io.github.classgraph.test.accepted.rejectedsub.RejectedSub; +import io.github.classgraph.test.rejected.RejectedAnnotation; +import io.github.classgraph.test.rejected.RejectedSubclass; +import io.github.classgraph.test.rejected.RejectedSubinterface; +import io.github.classgraph.test.rejected.RejectedSuperclass; /** - * The Class ClassGraphTest. + * ClassGraphTest. */ public class ClassGraphTest { - /** The Constant ROOT_PACKAGE. */ private static final String ROOT_PACKAGE = ClassGraphTest.class.getPackage().getName(); - /** The Constant WHITELIST_PACKAGE. */ - private static final String WHITELIST_PACKAGE = Whitelisted.class.getPackage().getName(); + /** The Constant ACCEPT_PACKAGE. */ + private static final String ACCEPT_PACKAGE = Accepted.class.getPackage().getName(); /** * Scan. @@ -84,38 +84,38 @@ public void scan() { assertThat(allClasses).contains(ClassGraph.class.getName()); assertThat(allClasses).contains(ClassGraphTest.class.getName()); assertThat(allClasses).doesNotContain(String.class.getName()); - assertThat(allClasses).contains(BlacklistedSub.class.getName()); + assertThat(allClasses).contains(RejectedSub.class.getName()); } } /** - * Scan with whitelist. + * Scan with accept. */ @Test - public void scanWithWhitelist() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).scan()) { + public void scanWithAccept() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { final List allClasses = scanResult.getAllClasses().getNames(); assertThat(allClasses).contains(Cls.class.getName()); assertThat(allClasses).doesNotContain(ClassGraph.class.getName()); assertThat(allClasses).doesNotContain(ClassGraphTest.class.getName()); assertThat(allClasses).doesNotContain(String.class.getName()); - assertThat(allClasses).contains(BlacklistedSub.class.getName()); + assertThat(allClasses).contains(RejectedSub.class.getName()); } } /** - * Scan with whitelist and blacklist. + * Scan with accept and reject. */ @Test - public void scanWithWhitelistAndBlacklist() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE) - .blacklistPackages(BlacklistedSub.class.getPackage().getName()).scan()) { + public void scanWithAcceptAndReject() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE) + .rejectPackages(RejectedSub.class.getPackage().getName()).scan()) { final List allClasses = scanResult.getAllClasses().getNames(); assertThat(allClasses).contains(Cls.class.getName()); assertThat(allClasses).doesNotContain(ClassGraph.class.getName()); assertThat(allClasses).doesNotContain(ClassGraphTest.class.getName()); assertThat(allClasses).doesNotContain(String.class.getName()); - assertThat(allClasses).doesNotContain(BlacklistedSub.class.getName()); + assertThat(allClasses).doesNotContain(RejectedSub.class.getName()); } } @@ -124,8 +124,8 @@ public void scanWithWhitelistAndBlacklist() { */ @Test public void scanSubAndSuperclasses() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).scan()) { - final List subclasses = scanResult.getSubclasses(Cls.class.getName()).getNames(); + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { + final List subclasses = scanResult.getSubclasses(Cls.class).getNames(); assertThat(subclasses).doesNotContain(Cls.class.getName()); assertThat(subclasses).contains(ClsSub.class.getName()); assertThat(subclasses).contains(ClsSubSub.class.getName()); @@ -141,8 +141,8 @@ public void scanSubAndSuperclasses() { */ @Test public void scanSubAndSuperinterface() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).scan()) { - final List subinterfaces = scanResult.getClassesImplementing(Iface.class.getName()).getNames(); + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { + final List subinterfaces = scanResult.getClassesImplementing(Iface.class).getNames(); assertThat(subinterfaces).doesNotContain(Iface.class.getName()); assertThat(subinterfaces).contains(IfaceSub.class.getName()); assertThat(subinterfaces).contains(IfaceSubSub.class.getName()); @@ -158,48 +158,46 @@ public void scanSubAndSuperinterface() { */ @Test public void scanTransitiveImplements() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).scan()) { - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { + assertThat(scanResult.getClassesImplementing(Iface.class).getNames()) .doesNotContain(Iface.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSubSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSubSub.class).getNames()) .doesNotContain(Cls.class.getName()); - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()) - .contains(Impl1.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(Iface.class).getNames()).contains(Impl1.class.getName()); + assertThat(scanResult.getClassesImplementing(IfaceSub.class).getNames()) .contains(Impl1.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSubSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSubSub.class).getNames()) .contains(Impl1.class.getName()); - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(Iface.class).getNames()) .contains(Impl1Sub.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSub.class).getNames()) .contains(Impl1Sub.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSubSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSubSub.class).getNames()) .contains(Impl1Sub.class.getName()); - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(Iface.class).getNames()) .contains(Impl1SubSub.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSub.class).getNames()) .contains(Impl1SubSub.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSubSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSubSub.class).getNames()) .contains(Impl1SubSub.class.getName()); - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()) - .contains(Impl2.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(Iface.class).getNames()).contains(Impl2.class.getName()); + assertThat(scanResult.getClassesImplementing(IfaceSub.class).getNames()) .doesNotContain(Impl2.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSubSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSubSub.class).getNames()) .doesNotContain(Impl2.class.getName()); - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(Iface.class).getNames()) .contains(Impl2Sub.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSub.class).getNames()) .doesNotContain(Impl2Sub.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSubSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSubSub.class).getNames()) .doesNotContain(Impl2Sub.class.getName()); - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(Iface.class).getNames()) .contains(Impl2SubSub.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSub.class).getNames()) .contains(Impl2SubSub.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSubSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSubSub.class).getNames()) .contains(Impl2SubSub.class.getName()); } } @@ -209,79 +207,74 @@ public void scanTransitiveImplements() { */ @Test public void testExternalSuperclassReturned() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).scan()) { - assertThat(scanResult.getSuperclasses(Whitelisted.class.getName()).getNames()) - .containsExactly(BlacklistedSuperclass.class.getName()); - assertThat(scanResult.getSubclasses(Whitelisted.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesImplementing(WhitelistedInterface.class.getName()).getNames()) - .isEmpty(); - assertThat(scanResult.getClassesImplementing(WhitelistedInterface.class.getName()).getNames()) - .isEmpty(); + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { + assertThat(scanResult.getSuperclasses(Accepted.class.getName()).getNames()) + .containsExactly(RejectedSuperclass.class.getName()); + assertThat(scanResult.getSubclasses(Accepted.class).getNames()).isEmpty(); + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()).isEmpty(); + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()).isEmpty(); } } /** - * Test whitelisted without exception without strict whitelist. + * Test accepted without exception without strict accept. */ @Test - public void testWhitelistedWithoutExceptionWithoutStrictWhitelist() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).enableExternalClasses() + public void testAcceptedWithoutExceptionWithoutStrictAccept() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).enableExternalClasses() .scan()) { - assertThat(scanResult.getSuperclasses(Whitelisted.class.getName()).getNames()) - .containsExactly(BlacklistedSuperclass.class.getName()); + assertThat(scanResult.getSuperclasses(Accepted.class.getName()).getNames()) + .containsExactly(RejectedSuperclass.class.getName()); } } /** - * Test can query with blacklisted annotation. + * Test can query with rejected annotation. */ - public void testCanQueryWithBlacklistedAnnotation() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).scan()) { - assertThat(scanResult.getSuperclasses(Whitelisted.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesWithAnnotation(BlacklistedAnnotation.class.getName()).getNames()) - .containsExactly(Whitelisted.class.getName()); + public void testCanQueryWithRejectedAnnotation() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { + assertThat(scanResult.getSuperclasses(Accepted.class.getName()).getNames()).isEmpty(); + assertThat(scanResult.getClassesWithAnnotation(RejectedAnnotation.class).getNames()) + .containsExactly(Accepted.class.getName()); } } /** - * Test blacklisted placeholder not returned. + * Test rejected placeholder not returned. */ @Test - public void testBlacklistedPlaceholderNotReturned() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(ROOT_PACKAGE) - .blacklistPackages(BlacklistedAnnotation.class.getPackage().getName()).enableAnnotationInfo() - .scan()) { - assertThat(scanResult.getSuperclasses(Whitelisted.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getSubclasses(Whitelisted.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesImplementing(WhitelistedInterface.class.getName()).getNames()) - .isEmpty(); - assertThat(scanResult.getClassesImplementing(WhitelistedInterface.class.getName()).getNames()) - .isEmpty(); - assertThat(scanResult.getAnnotationsOnClass(WhitelistedInterface.class.getName()).getNames()).isEmpty(); + public void testRejectedPlaceholderNotReturned() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ROOT_PACKAGE) + .rejectPackages(RejectedAnnotation.class.getPackage().getName()).enableAnnotationInfo().scan()) { + assertThat(scanResult.getSuperclasses(Accepted.class.getName()).getNames()).isEmpty(); + assertThat(scanResult.getSubclasses(Accepted.class).getNames()).isEmpty(); + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()).isEmpty(); + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()).isEmpty(); + assertThat(scanResult.getAnnotationsOnClass(AcceptedInterface.class.getName()).getNames()).isEmpty(); } } /** - * Test blacklisted package overrides whitelisted class with whitelisted override returned. + * Test rejected package overrides accepted class with accepted override returned. */ @Test - public void testBlacklistedPackageOverridesWhitelistedClassWithWhitelistedOverrideReturned() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(ROOT_PACKAGE) - .blacklistPackages(BlacklistedAnnotation.class.getPackage().getName()) - .whitelistClasses(BlacklistedAnnotation.class.getName()).enableAnnotationInfo().scan()) { - assertThat(scanResult.getAnnotationsOnClass(Whitelisted.class.getName()).getNames()).isEmpty(); + public void testRejectedPackageOverridesAcceptedClassWithAcceptedOverrideReturned() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ROOT_PACKAGE) + .rejectPackages(RejectedAnnotation.class.getPackage().getName()) + .acceptClasses(RejectedAnnotation.class.getName()).enableAnnotationInfo().scan()) { + assertThat(scanResult.getAnnotationsOnClass(Accepted.class.getName()).getNames()).isEmpty(); } } /** - * Test non whitelisted annotation returned without strict whitelist. + * Test non accepted annotation returned without strict accept. */ @Test - public void testNonWhitelistedAnnotationReturnedWithoutStrictWhitelist() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).enableAnnotationInfo() + public void testNonAcceptedAnnotationReturnedWithoutStrictAccept() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).enableAnnotationInfo() .enableExternalClasses().scan()) { - assertThat(scanResult.getAnnotationsOnClass(Whitelisted.class.getName()).getNames()) - .containsExactlyInAnyOrder(BlacklistedAnnotation.class.getName()); + assertThat(scanResult.getAnnotationsOnClass(Accepted.class.getName()).getNames()) + .containsOnly(RejectedAnnotation.class.getName()); } } @@ -290,69 +283,65 @@ public void testNonWhitelistedAnnotationReturnedWithoutStrictWhitelist() { */ @Test public void testExternalAnnotationReturned() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE).enableAnnotationInfo() + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).enableAnnotationInfo() .scan()) { - assertThat(scanResult.getAnnotationsOnClass(Whitelisted.class.getName()).getNames()) - .containsExactly(BlacklistedAnnotation.class.getName()); + assertThat(scanResult.getAnnotationsOnClass(Accepted.class.getName()).getNames()) + .containsExactly(RejectedAnnotation.class.getName()); } } /** - * Test blacklisted package. + * Test rejected package. */ - public void testBlacklistedPackage() { + public void testRejectedPackage() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(ROOT_PACKAGE, "-" + BlacklistedSuperclass.class.getPackage().getName()).scan()) { - assertThat(scanResult.getSuperclasses(Whitelisted.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getSubclasses(Whitelisted.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesImplementing(WhitelistedInterface.class.getName()).getNames()) - .isEmpty(); - assertThat(scanResult.getClassesImplementing(WhitelistedInterface.class.getName()).getNames()) - .isEmpty(); - assertThat(scanResult.getClassesWithAnnotation(BlacklistedAnnotation.class.getName()).getNames()) - .isEmpty(); + .acceptPackages(ROOT_PACKAGE, "-" + RejectedSuperclass.class.getPackage().getName()).scan()) { + assertThat(scanResult.getSuperclasses(Accepted.class.getName()).getNames()).isEmpty(); + assertThat(scanResult.getSubclasses(Accepted.class).getNames()).isEmpty(); + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()).isEmpty(); + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()).isEmpty(); + assertThat(scanResult.getClassesWithAnnotation(RejectedAnnotation.class).getNames()).isEmpty(); } } /** - * Test no exception if querying blacklisted. + * Test no exception if querying rejected. */ - public void testNoExceptionIfQueryingBlacklisted() { + public void testNoExceptionIfQueryingRejected() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(WHITELIST_PACKAGE, "-" + BlacklistedSuperclass.class.getPackage().getName()) - .scan()) { - assertThat(scanResult.getSuperclasses(BlacklistedSuperclass.class.getName()).getNames()).isEmpty(); + .acceptPackages(ACCEPT_PACKAGE, "-" + RejectedSuperclass.class.getPackage().getName()).scan()) { + assertThat(scanResult.getSuperclasses(RejectedSuperclass.class.getName()).getNames()).isEmpty(); } } /** - * Test no exception if explicitly whitelisted class in blacklisted package. + * Test no exception if explicitly accepted class in rejected package. */ - public void testNoExceptionIfExplicitlyWhitelistedClassInBlacklistedPackage() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE, - "-" + BlacklistedSuperclass.class.getPackage().getName() + BlacklistedSuperclass.class.getName()) + public void testNoExceptionIfExplicitlyAcceptedClassInRejectedPackage() { + try (ScanResult scanResult = new ClassGraph() + .acceptPackages(ACCEPT_PACKAGE, + "-" + RejectedSuperclass.class.getPackage().getName() + RejectedSuperclass.class.getName()) .scan()) { - assertThat(scanResult.getSuperclasses(BlacklistedSuperclass.class.getName()).getNames()).isEmpty(); + assertThat(scanResult.getSuperclasses(RejectedSuperclass.class.getName()).getNames()).isEmpty(); } } /** - * Test visible if not blacklisted. + * Test visible if not rejected. */ @Test - public void testVisibleIfNotBlacklisted() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(ROOT_PACKAGE).enableAnnotationInfo() - .scan()) { - assertThat(scanResult.getSuperclasses(Whitelisted.class.getName()).getNames()) - .containsExactly(BlacklistedSuperclass.class.getName()); - assertThat(scanResult.getSubclasses(Whitelisted.class.getName()).getNames()) - .containsExactly(BlacklistedSubclass.class.getName()); - assertThat(scanResult.getClassesImplementing(WhitelistedInterface.class.getName()).getNames()) - .containsExactly(BlacklistedSubinterface.class.getName()); - assertThat(scanResult.getClassesImplementing(WhitelistedInterface.class.getName()).getNames()) - .containsExactly(BlacklistedSubinterface.class.getName()); - assertThat(scanResult.getClassesWithAnnotation(BlacklistedAnnotation.class.getName()).getNames()) - .containsExactly(Whitelisted.class.getName()); + public void testVisibleIfNotRejected() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ROOT_PACKAGE).enableAnnotationInfo().scan()) { + assertThat(scanResult.getSuperclasses(Accepted.class.getName()).getNames()) + .containsExactly(RejectedSuperclass.class.getName()); + assertThat(scanResult.getSubclasses(Accepted.class).getNames()) + .containsExactly(RejectedSubclass.class.getName()); + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()) + .containsExactly(RejectedSubinterface.class.getName()); + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()) + .containsExactly(RejectedSubinterface.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(RejectedAnnotation.class).getNames()) + .containsExactly(Accepted.class.getName()); } } @@ -362,13 +351,18 @@ public void testVisibleIfNotBlacklisted() { @Test public void scanFilePattern() { final AtomicBoolean readFileContents = new AtomicBoolean(false); - try (ScanResult scanResult = new ClassGraph().whitelistPathsNonRecursive("").scan()) { - scanResult.getResourcesWithLeafName("file-content-test.txt").forEachByteArray(new ByteArrayConsumer() { - @Override - public void accept(final Resource res, final byte[] arr) { - readFileContents.set(new String(arr).equals("File contents")); - } - }); + try (ScanResult scanResult = new ClassGraph().acceptPathsNonRecursive("").scan()) { + try { + scanResult.getResourcesWithLeafName("file-content-test.txt") + .forEachByteArrayThrowingIOException(new ByteArrayConsumerThrowsIOException() { + @Override + public void accept(final Resource resource, final byte[] byteArray) throws IOException { + readFileContents.set(new String(byteArray).equals("File contents")); + } + }); + } catch (final IOException e) { + throw new RuntimeException(e); + } assertThat(readFileContents.get()).isTrue(); } } @@ -378,7 +372,7 @@ public void accept(final Resource res, final byte[] arr) { */ @Test public void scanStaticFinalFieldName() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE) + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE) .enableStaticFinalFieldConstantInitializerValues().scan()) { int numInitializers = 0; for (final FieldInfo fieldInfo : scanResult.getClassInfo(StaticField.class.getName()).getFieldInfo()) { @@ -392,6 +386,9 @@ public void scanStaticFinalFieldName() { /** * Scan static final field name ignore visibility. + * + * @throws Exception + * the exception */ @Test public void scanStaticFinalFieldNameIgnoreVisibility() throws Exception { @@ -400,7 +397,7 @@ public void scanStaticFinalFieldNameIgnoreVisibility() throws Exception { "integerField", "booleanField" }) { fieldNames.add(StaticField.class.getName() + "." + fieldName); } - try (ScanResult scanResult = new ClassGraph().whitelistPackages(WHITELIST_PACKAGE) + try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE) .enableStaticFinalFieldConstantInitializerValues().ignoreFieldVisibility().scan()) { int numInitializers = 0; for (final FieldInfo fieldInfo : scanResult.getClassInfo(StaticField.class.getName()).getFieldInfo()) { @@ -437,7 +434,7 @@ public void scanStaticFinalFieldNameIgnoreVisibility() throws Exception { */ @Test public void generateGraphVizFile() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(ROOT_PACKAGE).enableAllInfo().scan()) { + try (ScanResult scanResult = new ClassGraph().acceptPackages(ROOT_PACKAGE).enableAllInfo().scan()) { final String dotFile = scanResult.getAllClasses().generateGraphVizDotFile(20, 20); assertThat(dotFile).contains("\"" + ClsSub.class.getName() + "\" -> \"" + Cls.class.getName() + "\""); } @@ -448,7 +445,7 @@ public void generateGraphVizFile() { */ @Test public void testGetClasspathElements() { - assertThat(new ClassGraph().whitelistPackages(ROOT_PACKAGE).enableAllInfo().getClasspathFiles().size()) + assertThat(new ClassGraph().acceptPackages(ROOT_PACKAGE).enableAllInfo().getClasspathFiles().size()) .isGreaterThan(0); } @@ -458,7 +455,7 @@ public void testGetClasspathElements() { @Test public void testGetManifest() { final AtomicBoolean foundManifest = new AtomicBoolean(); - try (ScanResult scanResult = new ClassGraph().whitelistPaths("META-INF").enableAllInfo().scan()) { + try (ScanResult scanResult = new ClassGraph().acceptPaths("META-INF").enableAllInfo().scan()) { for (@SuppressWarnings("unused") final Resource res : scanResult.getResourcesWithLeafName("MANIFEST.MF")) { foundManifest.set(true); diff --git a/src/test/java/io/github/classgraph/test/ClassInfoTest.java b/src/test/java/io/github/classgraph/test/ClassInfoTest.java index fe1721a3c..546db809f 100644 --- a/src/test/java/io/github/classgraph/test/ClassInfoTest.java +++ b/src/test/java/io/github/classgraph/test/ClassInfoTest.java @@ -4,46 +4,45 @@ import java.util.List; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; import io.github.classgraph.ClassInfoList.ClassInfoFilter; import io.github.classgraph.ScanResult; -import io.github.classgraph.test.whitelisted.ClsSub; -import io.github.classgraph.test.whitelisted.ClsSubSub; -import io.github.classgraph.test.whitelisted.Iface; -import io.github.classgraph.test.whitelisted.IfaceSub; -import io.github.classgraph.test.whitelisted.IfaceSubSub; -import io.github.classgraph.test.whitelisted.Impl1; -import io.github.classgraph.test.whitelisted.Impl1Sub; -import io.github.classgraph.test.whitelisted.Impl1SubSub; -import io.github.classgraph.test.whitelisted.Impl2; -import io.github.classgraph.test.whitelisted.Impl2Sub; -import io.github.classgraph.test.whitelisted.Impl2SubSub; +import io.github.classgraph.test.accepted.ClsSub; +import io.github.classgraph.test.accepted.ClsSubSub; +import io.github.classgraph.test.accepted.Iface; +import io.github.classgraph.test.accepted.IfaceSub; +import io.github.classgraph.test.accepted.IfaceSubSub; +import io.github.classgraph.test.accepted.Impl1; +import io.github.classgraph.test.accepted.Impl1Sub; +import io.github.classgraph.test.accepted.Impl1SubSub; +import io.github.classgraph.test.accepted.Impl2; +import io.github.classgraph.test.accepted.Impl2Sub; +import io.github.classgraph.test.accepted.Impl2SubSub; /** - * The Class ClassInfoTest. + * ClassInfoTest. */ public class ClassInfoTest { - /** The scan result. */ private static ScanResult scanResult; /** * Setup. */ - @BeforeClass + @BeforeAll public static void setup() { - scanResult = new ClassGraph().whitelistPackages(Impl1.class.getPackage().getName()).scan(); + scanResult = new ClassGraph().acceptPackages(Impl1.class.getPackage().getName()).scan(); } /** * Teardown. */ - @AfterClass + @AfterAll public static void teardown() { scanResult.close(); scanResult = null; @@ -69,7 +68,7 @@ public void filter() { public boolean accept(final ClassInfo ci) { return ci.getName().contains("ClsSub"); } - }).getNames()).containsExactlyInAnyOrder(ClsSub.class.getName(), ClsSubSub.class.getName()); + }).getNames()).containsOnly(ClsSub.class.getName(), ClsSubSub.class.getName()); } /** @@ -82,7 +81,7 @@ public void streamHasSuperInterfaceDirect() { public boolean accept(final ClassInfo ci) { return ci.getInterfaces().directOnly().getNames().contains(Iface.class.getName()); } - }).getNames()).containsExactlyInAnyOrder(IfaceSub.class.getName(), Impl2.class.getName()); + }).getNames()).containsOnly(IfaceSub.class.getName(), Impl2.class.getName()); } /** @@ -95,8 +94,8 @@ public void streamHasSuperInterface() { public boolean accept(final ClassInfo ci) { return ci.getInterfaces().getNames().contains(Iface.class.getName()); } - }).getNames()).containsExactlyInAnyOrder(IfaceSub.class.getName(), IfaceSubSub.class.getName(), - Impl2.class.getName(), Impl2Sub.class.getName(), Impl2SubSub.class.getName(), Impl1.class.getName(), + }).getNames()).containsOnly(IfaceSub.class.getName(), IfaceSubSub.class.getName(), Impl2.class.getName(), + Impl2Sub.class.getName(), Impl2SubSub.class.getName(), Impl1.class.getName(), Impl1Sub.class.getName(), Impl1SubSub.class.getName()); } @@ -105,8 +104,8 @@ public boolean accept(final ClassInfo ci) { */ @Test public void implementsInterfaceDirect() { - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).directOnly().getNames()) - .containsExactlyInAnyOrder(IfaceSub.class.getName(), Impl2.class.getName()); + assertThat(scanResult.getClassesImplementing(Iface.class).directOnly().getNames()) + .containsOnly(IfaceSub.class.getName(), Impl2.class.getName()); } /** @@ -114,8 +113,8 @@ public void implementsInterfaceDirect() { */ @Test public void implementsInterface() { - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()).containsExactlyInAnyOrder( - Impl1.class.getName(), Impl1Sub.class.getName(), Impl1SubSub.class.getName(), Impl2.class.getName(), + assertThat(scanResult.getClassesImplementing(Iface.class).getNames()).containsOnly(Impl1.class.getName(), + Impl1Sub.class.getName(), Impl1SubSub.class.getName(), Impl2.class.getName(), Impl2Sub.class.getName(), Impl2SubSub.class.getName(), IfaceSub.class.getName(), IfaceSubSub.class.getName()); } @@ -131,6 +130,6 @@ public boolean accept(final ClassInfo ci) { return ci.getInterfaces().getNames().contains(Iface.class.getName()) && ci.getSuperclasses().getNames().contains(Impl1.class.getName()); } - }).getNames()).containsExactlyInAnyOrder(Impl1Sub.class.getName(), Impl1SubSub.class.getName()); + }).getNames()).containsOnly(Impl1Sub.class.getName(), Impl1SubSub.class.getName()); } } diff --git a/src/test/java/io/github/classgraph/test/accepted/Accepted.java b/src/test/java/io/github/classgraph/test/accepted/Accepted.java new file mode 100644 index 000000000..02acd5869 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/Accepted.java @@ -0,0 +1,12 @@ +package io.github.classgraph.test.accepted; + +import io.github.classgraph.test.rejected.RejectedAnnotation; +import io.github.classgraph.test.rejected.RejectedInterface; +import io.github.classgraph.test.rejected.RejectedSuperclass; + +/** + * Accepted. + */ +@RejectedAnnotation +public class Accepted extends RejectedSuperclass implements RejectedInterface { +} diff --git a/src/test/java/io/github/classgraph/test/accepted/AcceptedInterface.java b/src/test/java/io/github/classgraph/test/accepted/AcceptedInterface.java new file mode 100644 index 000000000..585c92416 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/AcceptedInterface.java @@ -0,0 +1,9 @@ +package io.github.classgraph.test.accepted; + +import io.github.classgraph.test.rejected.RejectedInterface; + +/** + * The Interface AcceptedInterface. + */ +public interface AcceptedInterface extends RejectedInterface { +} diff --git a/src/test/java/io/github/classgraph/test/accepted/Cls.java b/src/test/java/io/github/classgraph/test/accepted/Cls.java new file mode 100644 index 000000000..0c07551e2 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/Cls.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.accepted; + +/** + * Cls. + */ +public class Cls { +} diff --git a/src/test/java/io/github/classgraph/test/accepted/ClsSub.java b/src/test/java/io/github/classgraph/test/accepted/ClsSub.java new file mode 100644 index 000000000..a0d2c144a --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/ClsSub.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.accepted; + +/** + * ClsSub. + */ +public class ClsSub extends Cls { +} diff --git a/src/test/java/io/github/classgraph/test/accepted/ClsSubSub.java b/src/test/java/io/github/classgraph/test/accepted/ClsSubSub.java new file mode 100644 index 000000000..048486c3e --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/ClsSubSub.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.accepted; + +/** + * ClsSubSub. + */ +public class ClsSubSub extends ClsSub { +} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/HasFieldWithTypeCls.java b/src/test/java/io/github/classgraph/test/accepted/HasFieldWithTypeCls.java similarity index 93% rename from src/test/java/io/github/classgraph/test/whitelisted/HasFieldWithTypeCls.java rename to src/test/java/io/github/classgraph/test/accepted/HasFieldWithTypeCls.java index fd17c8588..bd22e0962 100644 --- a/src/test/java/io/github/classgraph/test/whitelisted/HasFieldWithTypeCls.java +++ b/src/test/java/io/github/classgraph/test/accepted/HasFieldWithTypeCls.java @@ -1,13 +1,12 @@ -package io.github.classgraph.test.whitelisted; +package io.github.classgraph.test.accepted; import java.util.ArrayList; import java.util.HashMap; /** - * The Class HasFieldWithTypeCls. + * HasFieldWithTypeCls. */ public class HasFieldWithTypeCls { - /** * The Class HasFieldWithTypeCls1. */ diff --git a/src/test/java/io/github/classgraph/test/whitelisted/Iface.java b/src/test/java/io/github/classgraph/test/accepted/Iface.java similarity index 56% rename from src/test/java/io/github/classgraph/test/whitelisted/Iface.java rename to src/test/java/io/github/classgraph/test/accepted/Iface.java index cb3c868ad..1de272751 100644 --- a/src/test/java/io/github/classgraph/test/whitelisted/Iface.java +++ b/src/test/java/io/github/classgraph/test/accepted/Iface.java @@ -1,4 +1,4 @@ -package io.github.classgraph.test.whitelisted; +package io.github.classgraph.test.accepted; /** * The Interface Iface. diff --git a/src/test/java/io/github/classgraph/test/whitelisted/IfaceSub.java b/src/test/java/io/github/classgraph/test/accepted/IfaceSub.java similarity index 62% rename from src/test/java/io/github/classgraph/test/whitelisted/IfaceSub.java rename to src/test/java/io/github/classgraph/test/accepted/IfaceSub.java index 215de7749..d151e353a 100644 --- a/src/test/java/io/github/classgraph/test/whitelisted/IfaceSub.java +++ b/src/test/java/io/github/classgraph/test/accepted/IfaceSub.java @@ -1,4 +1,4 @@ -package io.github.classgraph.test.whitelisted; +package io.github.classgraph.test.accepted; /** * The Interface IfaceSub. diff --git a/src/test/java/io/github/classgraph/test/whitelisted/IfaceSubSub.java b/src/test/java/io/github/classgraph/test/accepted/IfaceSubSub.java similarity index 65% rename from src/test/java/io/github/classgraph/test/whitelisted/IfaceSubSub.java rename to src/test/java/io/github/classgraph/test/accepted/IfaceSubSub.java index a9be20e36..7a73a1767 100644 --- a/src/test/java/io/github/classgraph/test/whitelisted/IfaceSubSub.java +++ b/src/test/java/io/github/classgraph/test/accepted/IfaceSubSub.java @@ -1,4 +1,4 @@ -package io.github.classgraph.test.whitelisted; +package io.github.classgraph.test.accepted; /** * The Interface IfaceSubSub. diff --git a/src/test/java/io/github/classgraph/test/accepted/Impl1.java b/src/test/java/io/github/classgraph/test/accepted/Impl1.java new file mode 100644 index 000000000..75f6eb53e --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/Impl1.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.accepted; + +/** + * Impl1. + */ +public class Impl1 implements IfaceSubSub { +} diff --git a/src/test/java/io/github/classgraph/test/accepted/Impl1Sub.java b/src/test/java/io/github/classgraph/test/accepted/Impl1Sub.java new file mode 100644 index 000000000..f9aaff8d4 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/Impl1Sub.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.accepted; + +/** + * Impl1Sub. + */ +public class Impl1Sub extends Impl1 { +} diff --git a/src/test/java/io/github/classgraph/test/accepted/Impl1SubSub.java b/src/test/java/io/github/classgraph/test/accepted/Impl1SubSub.java new file mode 100644 index 000000000..633aba1b1 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/Impl1SubSub.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.accepted; + +/** + * Impl1SubSub. + */ +public class Impl1SubSub extends Impl1Sub { +} diff --git a/src/test/java/io/github/classgraph/test/accepted/Impl2.java b/src/test/java/io/github/classgraph/test/accepted/Impl2.java new file mode 100644 index 000000000..17b6deacc --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/Impl2.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.accepted; + +/** + * Impl2. + */ +public class Impl2 implements Iface { +} diff --git a/src/test/java/io/github/classgraph/test/accepted/Impl2Sub.java b/src/test/java/io/github/classgraph/test/accepted/Impl2Sub.java new file mode 100644 index 000000000..7c3e0a462 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/Impl2Sub.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.accepted; + +/** + * Impl2Sub. + */ +public class Impl2Sub extends Impl2 { +} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/Impl2SubSub.java b/src/test/java/io/github/classgraph/test/accepted/Impl2SubSub.java similarity index 51% rename from src/test/java/io/github/classgraph/test/whitelisted/Impl2SubSub.java rename to src/test/java/io/github/classgraph/test/accepted/Impl2SubSub.java index c417684e2..0727b2ea0 100644 --- a/src/test/java/io/github/classgraph/test/whitelisted/Impl2SubSub.java +++ b/src/test/java/io/github/classgraph/test/accepted/Impl2SubSub.java @@ -1,7 +1,7 @@ -package io.github.classgraph.test.whitelisted; +package io.github.classgraph.test.accepted; /** - * The Class Impl2SubSub. + * Impl2SubSub. */ public class Impl2SubSub extends Impl2Sub implements IfaceSubSub { } diff --git a/src/test/java/io/github/classgraph/test/whitelisted/StaticField.java b/src/test/java/io/github/classgraph/test/accepted/StaticField.java similarity index 90% rename from src/test/java/io/github/classgraph/test/whitelisted/StaticField.java rename to src/test/java/io/github/classgraph/test/accepted/StaticField.java index 5fce99442..5dc1d169b 100644 --- a/src/test/java/io/github/classgraph/test/whitelisted/StaticField.java +++ b/src/test/java/io/github/classgraph/test/accepted/StaticField.java @@ -1,10 +1,9 @@ -package io.github.classgraph.test.whitelisted; +package io.github.classgraph.test.accepted; /** - * The Class StaticField. + * StaticField. */ public class StaticField { - /** The Constant stringField. */ // Non-public -- need ignoreFieldVisibility() to match these static final String stringField = "Static field contents"; diff --git a/src/test/java/io/github/classgraph/test/accepted/rejectedsub/RejectedSub.java b/src/test/java/io/github/classgraph/test/accepted/rejectedsub/RejectedSub.java new file mode 100644 index 000000000..884739ce5 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/accepted/rejectedsub/RejectedSub.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.accepted.rejectedsub; + +/** + * RejectedSub. + */ +public class RejectedSub { +} diff --git a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedInterface.java b/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedInterface.java deleted file mode 100644 index 9c4e851e7..000000000 --- a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedInterface.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.blacklisted; - -/** - * The Interface BlacklistedInterface. - */ -public interface BlacklistedInterface { -} diff --git a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedSubclass.java b/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedSubclass.java deleted file mode 100644 index ed7d2175a..000000000 --- a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedSubclass.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.classgraph.test.blacklisted; - -import io.github.classgraph.test.whitelisted.Whitelisted; - -/** - * The Class BlacklistedSubclass. - */ -public class BlacklistedSubclass extends Whitelisted { -} diff --git a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedSubinterface.java b/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedSubinterface.java deleted file mode 100644 index 3bc634d86..000000000 --- a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedSubinterface.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.classgraph.test.blacklisted; - -import io.github.classgraph.test.whitelisted.WhitelistedInterface; - -/** - * The Interface BlacklistedSubinterface. - */ -public interface BlacklistedSubinterface extends WhitelistedInterface { -} diff --git a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedSuperclass.java b/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedSuperclass.java deleted file mode 100644 index a5b2a2b0d..000000000 --- a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedSuperclass.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.blacklisted; - -/** - * The Class BlacklistedSuperclass. - */ -public class BlacklistedSuperclass { -} diff --git a/src/test/java/io/github/classgraph/test/classrefannotation/AnnotationClassRefTest.java b/src/test/java/io/github/classgraph/test/classrefannotation/AnnotationClassRefTest.java index 6ce3ca66b..076118a3b 100644 --- a/src/test/java/io/github/classgraph/test/classrefannotation/AnnotationClassRefTest.java +++ b/src/test/java/io/github/classgraph/test/classrefannotation/AnnotationClassRefTest.java @@ -34,7 +34,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.AnnotationClassRef; import io.github.classgraph.AnnotationInfo; @@ -47,10 +47,9 @@ import io.github.classgraph.ScanResult; /** - * The Class AnnotationClassRefTest. + * AnnotationClassRefTest. */ public class AnnotationClassRefTest { - /** * The Interface ClassRefAnnotation. */ @@ -84,10 +83,9 @@ public void methodWithAnnotation() { @Test public void testClassRefAnnotation() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(AnnotationClassRefTest.class.getPackage().getName()).enableMethodInfo() + .acceptPackages(AnnotationClassRefTest.class.getPackage().getName()).enableMethodInfo() .enableAnnotationInfo().scan()) { - final ClassInfoList testClasses = scanResult - .getClassesWithMethodAnnotation(ClassRefAnnotation.class.getName()); + final ClassInfoList testClasses = scanResult.getClassesWithMethodAnnotation(ClassRefAnnotation.class); assertThat(testClasses.size()).isEqualTo(1); final ClassInfo testClass = testClasses.get(0); final MethodInfo method = testClass.getMethodInfo().getSingleMethod("methodWithAnnotation"); diff --git a/src/test/java/io/github/classgraph/test/external/ExternalSuperclass.java b/src/test/java/io/github/classgraph/test/external/ExternalSuperclass.java index 26a298469..ba5fccc49 100644 --- a/src/test/java/io/github/classgraph/test/external/ExternalSuperclass.java +++ b/src/test/java/io/github/classgraph/test/external/ExternalSuperclass.java @@ -1,7 +1,7 @@ package io.github.classgraph.test.external; /** - * The Class ExternalSuperclass. + * ExternalSuperclass. */ public class ExternalSuperclass { } diff --git a/src/test/java/io/github/classgraph/test/fieldannotation/ClassWithoutFieldOrMethodAnnotations.java b/src/test/java/io/github/classgraph/test/fieldannotation/ClassWithoutFieldOrMethodAnnotations.java index 102ab5d25..8553621a3 100644 --- a/src/test/java/io/github/classgraph/test/fieldannotation/ClassWithoutFieldOrMethodAnnotations.java +++ b/src/test/java/io/github/classgraph/test/fieldannotation/ClassWithoutFieldOrMethodAnnotations.java @@ -1,10 +1,9 @@ package io.github.classgraph.test.fieldannotation; /** - * The Class ClassWithoutFieldOrMethodAnnotations. + * ClassWithoutFieldOrMethodAnnotations. */ public class ClassWithoutFieldOrMethodAnnotations { - /** The field without annotations. */ public int fieldWithoutAnnotations; diff --git a/src/test/java/io/github/classgraph/test/fieldannotation/FieldAndMethodAnnotationTest.java b/src/test/java/io/github/classgraph/test/fieldannotation/FieldAndMethodAnnotationTest.java index 7df42e8d7..2c535d1cb 100644 --- a/src/test/java/io/github/classgraph/test/fieldannotation/FieldAndMethodAnnotationTest.java +++ b/src/test/java/io/github/classgraph/test/fieldannotation/FieldAndMethodAnnotationTest.java @@ -32,17 +32,16 @@ import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import io.github.classgraph.test.external.ExternalAnnotation; /** - * The Class FieldAndMethodAnnotationTest. + * FieldAndMethodAnnotationTest. */ public class FieldAndMethodAnnotationTest { - /** The public field with annotation. */ public int publicFieldWithAnnotation; @@ -59,10 +58,10 @@ public class FieldAndMethodAnnotationTest { @Test public void testGetNamesOfClassesWithFieldAnnotation() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(FieldAndMethodAnnotationTest.class.getPackage().getName()).enableFieldInfo() + .acceptPackages(FieldAndMethodAnnotationTest.class.getPackage().getName()).enableFieldInfo() .enableAnnotationInfo().scan()) { - final List testClasses = scanResult - .getClassesWithFieldAnnotation(ExternalAnnotation.class.getName()).getNames(); + final List testClasses = scanResult.getClassesWithFieldAnnotation(ExternalAnnotation.class) + .getNames(); assertThat(testClasses).isEmpty(); } } @@ -73,11 +72,11 @@ public void testGetNamesOfClassesWithFieldAnnotation() { @Test public void testGetNamesOfClassesWithFieldAnnotationIgnoringVisibility() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(FieldAndMethodAnnotationTest.class.getPackage().getName()).enableFieldInfo() + .acceptPackages(FieldAndMethodAnnotationTest.class.getPackage().getName()).enableFieldInfo() .ignoreFieldVisibility().enableAnnotationInfo().scan()) { - final List testClasses = scanResult - .getClassesWithFieldAnnotation(ExternalAnnotation.class.getName()).getNames(); - assertThat(testClasses).containsExactlyInAnyOrder(FieldAndMethodAnnotationTest.class.getName()); + final List testClasses = scanResult.getClassesWithFieldAnnotation(ExternalAnnotation.class) + .getNames(); + assertThat(testClasses).containsOnly(FieldAndMethodAnnotationTest.class.getName()); } } @@ -88,11 +87,11 @@ public void testGetNamesOfClassesWithFieldAnnotationIgnoringVisibility() { @ExternalAnnotation public void testGetNamesOfClassesWithMethodAnnotation() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(FieldAndMethodAnnotationTest.class.getPackage().getName()).enableMethodInfo() + .acceptPackages(FieldAndMethodAnnotationTest.class.getPackage().getName()).enableMethodInfo() .enableAnnotationInfo().scan()) { - final List testClasses = scanResult - .getClassesWithMethodAnnotation(ExternalAnnotation.class.getName()).getNames(); - assertThat(testClasses).containsExactlyInAnyOrder(FieldAndMethodAnnotationTest.class.getName()); + final List testClasses = scanResult.getClassesWithMethodAnnotation(ExternalAnnotation.class) + .getNames(); + assertThat(testClasses).containsOnly(FieldAndMethodAnnotationTest.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/test/fieldinfo/FieldInfoTest.java b/src/test/java/io/github/classgraph/test/fieldinfo/FieldInfoTest.java index 929913dda..4d4d7e4f2 100644 --- a/src/test/java/io/github/classgraph/test/fieldinfo/FieldInfoTest.java +++ b/src/test/java/io/github/classgraph/test/fieldinfo/FieldInfoTest.java @@ -32,36 +32,49 @@ import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import io.github.classgraph.test.external.ExternalAnnotation; /** - * The Class FieldInfoTest. + * FieldInfoTest. */ public class FieldInfoTest { - - /** The Constant publicFieldWithAnnotation. */ + /** Constant publicFieldWithAnnotation. */ @ExternalAnnotation public static final int publicFieldWithAnnotation = 3; - /** The Constant privateFieldWithAnnotation. */ + /** Constant privateFieldWithAnnotation. */ @ExternalAnnotation private static final String privateFieldWithAnnotation = "test"; - /** The field without annotation. */ + /** Field without annotation. */ public int fieldWithoutAnnotation; + /** + * Field with initializer but without static or final modifier. In Java, the constant pool is not currently used + * by the compiler to assign initializer values for non-static, non-final fields, whereas it supposedly is in + * Kotlin (#379). + */ + public int nonStaticNonFinalFieldWithInitializer = 5; + + /** + * Static non-final field with initializer (#379). + */ + public static int staticNonFinalFieldWithInitializer = 7; + /** * Field info not enabled. */ - @Test(expected = IllegalArgumentException.class) + @Test public void fieldInfoNotEnabled() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(FieldInfoTest.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(FieldInfoTest.class.getPackage().getName()) .scan()) { - scanResult.getClassInfo(FieldInfoTest.class.getName()).getFieldInfo(); + Assertions.assertThrows(IllegalArgumentException.class, + () -> scanResult.getClassInfo(FieldInfoTest.class.getName()).getFieldInfo()); } } @@ -70,15 +83,16 @@ public void fieldInfoNotEnabled() { */ @Test public void testGetFieldInfo() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(FieldInfoTest.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(FieldInfoTest.class.getPackage().getName()) .enableFieldInfo().enableStaticFinalFieldConstantInitializerValues().enableAnnotationInfo() .scan()) { final List fieldInfoStrs = scanResult.getClassInfo(FieldInfoTest.class.getName()).getFieldInfo() .getAsStrings(); - assertThat(fieldInfoStrs).containsExactlyInAnyOrder( + assertThat(fieldInfoStrs).containsOnly( "@" + ExternalAnnotation.class.getName() + " public static final int publicFieldWithAnnotation = 3", - "public int fieldWithoutAnnotation"); + "public int fieldWithoutAnnotation", "public int nonStaticNonFinalFieldWithInitializer", + "public static int staticNonFinalFieldWithInitializer"); } } @@ -87,17 +101,18 @@ public void testGetFieldInfo() { */ @Test public void testGetFieldInfoIgnoringVisibility() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(FieldInfoTest.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(FieldInfoTest.class.getPackage().getName()) .enableFieldInfo().enableStaticFinalFieldConstantInitializerValues().enableAnnotationInfo() .ignoreFieldVisibility().scan()) { final List fieldInfoStrs = scanResult.getClassInfo(FieldInfoTest.class.getName()).getFieldInfo() .getAsStrings(); - assertThat(fieldInfoStrs).containsExactlyInAnyOrder( + assertThat(fieldInfoStrs).containsOnly( "@" + ExternalAnnotation.class.getName() + " public static final int publicFieldWithAnnotation = 3", "@" + ExternalAnnotation.class.getName() + " private static final java.lang.String privateFieldWithAnnotation = \"test\"", - "public int fieldWithoutAnnotation"); + "public int fieldWithoutAnnotation", "public int nonStaticNonFinalFieldWithInitializer", + "public static int staticNonFinalFieldWithInitializer"); } } } diff --git a/src/test/java/io/github/classgraph/test/internal/InternalAnnotatedByExternal.java b/src/test/java/io/github/classgraph/test/internal/InternalAnnotatedByExternal.java index 7414d63fb..867154fc1 100644 --- a/src/test/java/io/github/classgraph/test/internal/InternalAnnotatedByExternal.java +++ b/src/test/java/io/github/classgraph/test/internal/InternalAnnotatedByExternal.java @@ -3,7 +3,7 @@ import io.github.classgraph.test.external.ExternalAnnotation; /** - * The Class InternalAnnotatedByExternal. + * InternalAnnotatedByExternal. */ @ExternalAnnotation public abstract class InternalAnnotatedByExternal { diff --git a/src/test/java/io/github/classgraph/test/internal/InternalExtendsExternal.java b/src/test/java/io/github/classgraph/test/internal/InternalExtendsExternal.java index 63729d72c..492ee4205 100644 --- a/src/test/java/io/github/classgraph/test/internal/InternalExtendsExternal.java +++ b/src/test/java/io/github/classgraph/test/internal/InternalExtendsExternal.java @@ -3,7 +3,7 @@ import io.github.classgraph.test.external.ExternalSuperclass; /** - * The Class InternalExtendsExternal. + * InternalExtendsExternal. */ public abstract class InternalExtendsExternal extends ExternalSuperclass { } diff --git a/src/test/java/io/github/classgraph/test/internal/InternalExternalTest.java b/src/test/java/io/github/classgraph/test/internal/InternalExternalTest.java index 6368dc5a8..b70710262 100644 --- a/src/test/java/io/github/classgraph/test/internal/InternalExternalTest.java +++ b/src/test/java/io/github/classgraph/test/internal/InternalExternalTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; @@ -11,18 +11,17 @@ import io.github.classgraph.test.external.ExternalSuperclass; /** - * The Class InternalExternalTest. + * InternalExternalTest. */ public class InternalExternalTest { - /** - * Test whitelisting external classes. + * Test accepting external classes. */ @Test - public void testWhitelistingExternalClasses() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages( + public void testAcceptingExternalClasses() { + try (ScanResult scanResult = new ClassGraph().acceptPackages( InternalExternalTest.class.getPackage().getName(), ExternalAnnotation.class.getName()).scan()) { - assertThat(scanResult.getAllStandardClasses().getNames()).containsExactlyInAnyOrder( + assertThat(scanResult.getAllStandardClasses().getNames()).containsOnly( InternalExternalTest.class.getName(), InternalExtendsExternal.class.getName(), InternalImplementsExternal.class.getName(), InternalAnnotatedByExternal.class.getName()); } @@ -34,10 +33,10 @@ public void testWhitelistingExternalClasses() { @Test public void testEnableExternalClasses() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(InternalExternalTest.class.getPackage().getName(), + .acceptPackages(InternalExternalTest.class.getPackage().getName(), ExternalAnnotation.class.getName()) .enableExternalClasses().scan()) { - assertThat(scanResult.getAllStandardClasses().getNames()).containsExactlyInAnyOrder( + assertThat(scanResult.getAllStandardClasses().getNames()).containsOnly( ExternalSuperclass.class.getName(), InternalExternalTest.class.getName(), InternalExtendsExternal.class.getName(), InternalImplementsExternal.class.getName(), InternalAnnotatedByExternal.class.getName()); @@ -45,25 +44,25 @@ public void testEnableExternalClasses() { } /** - * Test whitelisting external classes without enabling external classes. + * Test accepting external classes without enabling external classes. */ @Test - public void testWhitelistingExternalClassesWithoutEnablingExternalClasses() { + public void testAcceptingExternalClassesWithoutEnablingExternalClasses() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(InternalExternalTest.class.getPackage().getName(), + .acceptPackages(InternalExternalTest.class.getPackage().getName(), ExternalAnnotation.class.getName()) .enableAllInfo().scan()) { - assertThat(scanResult.getAllStandardClasses().getNames()).containsExactlyInAnyOrder( + assertThat(scanResult.getAllStandardClasses().getNames()).containsOnly( InternalExternalTest.class.getName(), InternalExtendsExternal.class.getName(), InternalImplementsExternal.class.getName(), InternalAnnotatedByExternal.class.getName()); - assertThat(scanResult.getSubclasses(ExternalSuperclass.class.getName()).getNames()) - .containsExactlyInAnyOrder(InternalExtendsExternal.class.getName()); + assertThat(scanResult.getSubclasses(ExternalSuperclass.class).getNames()) + .containsOnly(InternalExtendsExternal.class.getName()); assertThat(scanResult.getAllInterfaces()).isEmpty(); - assertThat(scanResult.getClassesImplementing(ExternalInterface.class.getName()).getNames()) - .containsExactlyInAnyOrder(InternalImplementsExternal.class.getName()); + assertThat(scanResult.getClassesImplementing(ExternalInterface.class).getNames()) + .containsOnly(InternalImplementsExternal.class.getName()); assertThat(scanResult.getAllAnnotations().getNames()).isEmpty(); - assertThat(scanResult.getClassesWithAnnotation(ExternalAnnotation.class.getName()).getNames()) - .containsExactlyInAnyOrder(InternalAnnotatedByExternal.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(ExternalAnnotation.class).getNames()) + .containsOnly(InternalAnnotatedByExternal.class.getName()); } } @@ -73,18 +72,18 @@ public void testWhitelistingExternalClassesWithoutEnablingExternalClasses() { @Test public void testIncludeReferencedClasses() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(InternalExternalTest.class.getPackage().getName()).enableAllInfo().scan()) { + .acceptPackages(InternalExternalTest.class.getPackage().getName()).enableAllInfo().scan()) { assertThat(scanResult.getAllStandardClasses().getNames()) .doesNotContain(ExternalSuperclass.class.getName()); - assertThat(scanResult.getSubclasses(ExternalSuperclass.class.getName()).getNames()) - .containsExactlyInAnyOrder(InternalExtendsExternal.class.getName()); + assertThat(scanResult.getSubclasses(ExternalSuperclass.class).getNames()) + .containsOnly(InternalExtendsExternal.class.getName()); assertThat(scanResult.getAllInterfaces().getNames()).doesNotContain(ExternalInterface.class.getName()); - assertThat(scanResult.getClassesImplementing(ExternalInterface.class.getName()).getNames()) - .containsExactlyInAnyOrder(InternalImplementsExternal.class.getName()); + assertThat(scanResult.getClassesImplementing(ExternalInterface.class).getNames()) + .containsOnly(InternalImplementsExternal.class.getName()); assertThat(scanResult.getAllAnnotations().getNames()) .doesNotContain(ExternalAnnotation.class.getName()); - assertThat(scanResult.getClassesWithAnnotation(ExternalAnnotation.class.getName()).getNames()) - .containsExactlyInAnyOrder(InternalAnnotatedByExternal.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(ExternalAnnotation.class).getNames()) + .containsOnly(InternalAnnotatedByExternal.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/test/internal/InternalImplementsExternal.java b/src/test/java/io/github/classgraph/test/internal/InternalImplementsExternal.java index 207fc8901..9e5b7f000 100644 --- a/src/test/java/io/github/classgraph/test/internal/InternalImplementsExternal.java +++ b/src/test/java/io/github/classgraph/test/internal/InternalImplementsExternal.java @@ -3,7 +3,7 @@ import io.github.classgraph.test.external.ExternalInterface; /** - * The Class InternalImplementsExternal. + * InternalImplementsExternal. */ public abstract class InternalImplementsExternal implements ExternalInterface { } diff --git a/src/test/java/io/github/classgraph/test/methodannotation/ClassWithoutMethodAnnotations.java b/src/test/java/io/github/classgraph/test/methodannotation/ClassWithoutMethodAnnotations.java index 137d7a424..eeabd91cf 100644 --- a/src/test/java/io/github/classgraph/test/methodannotation/ClassWithoutMethodAnnotations.java +++ b/src/test/java/io/github/classgraph/test/methodannotation/ClassWithoutMethodAnnotations.java @@ -1,10 +1,9 @@ package io.github.classgraph.test.methodannotation; /** - * The Class ClassWithoutMethodAnnotations. + * ClassWithoutMethodAnnotations. */ public class ClassWithoutMethodAnnotations { - /** The field without annotations. */ public int fieldWithoutAnnotations; diff --git a/src/test/java/io/github/classgraph/test/methodannotation/MethodAnnotationTest.java b/src/test/java/io/github/classgraph/test/methodannotation/MethodAnnotationTest.java index b0c0848dd..69c88195b 100644 --- a/src/test/java/io/github/classgraph/test/methodannotation/MethodAnnotationTest.java +++ b/src/test/java/io/github/classgraph/test/methodannotation/MethodAnnotationTest.java @@ -32,7 +32,7 @@ import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; @@ -42,20 +42,19 @@ import io.github.classgraph.test.external.ExternalAnnotation; /** - * The Class MethodAnnotationTest. + * MethodAnnotationTest. */ public class MethodAnnotationTest { - /** * Get the names of classes with method annotation. */ @Test public void testGetNamesOfClassesWithMethodAnnotation() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(MethodAnnotationTest.class.getPackage().getName()).enableClassInfo() + .acceptPackages(MethodAnnotationTest.class.getPackage().getName()).enableClassInfo() .enableMethodInfo().enableAnnotationInfo().scan()) { - final List testClasses = scanResult - .getClassesWithMethodAnnotation(ExternalAnnotation.class.getName()).getNames(); + final List testClasses = scanResult.getClassesWithMethodAnnotation(ExternalAnnotation.class) + .getNames(); assertThat(testClasses).isEmpty(); } } @@ -66,12 +65,12 @@ public void testGetNamesOfClassesWithMethodAnnotation() { @Test public void testGetNamesOfClassesWithMethodAnnotationIgnoringVisibility() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(MethodAnnotationTest.class.getPackage().getName()).enableClassInfo() + .acceptPackages(MethodAnnotationTest.class.getPackage().getName()).enableClassInfo() .enableMethodInfo().enableAnnotationInfo().ignoreMethodVisibility().scan()) { final ClassInfoList classesWithMethodAnnotation = scanResult - .getClassesWithMethodAnnotation(ExternalAnnotation.class.getName()); + .getClassesWithMethodAnnotation(ExternalAnnotation.class); final List testClasses = classesWithMethodAnnotation.getNames(); - assertThat(testClasses).containsExactlyInAnyOrder(MethodAnnotationTest.class.getName()); + assertThat(testClasses).containsOnly(MethodAnnotationTest.class.getName()); boolean found = false; for (final ClassInfo ci : classesWithMethodAnnotation) { for (final MethodInfo mi : ci.getMethodInfo()) { diff --git a/src/test/java/io/github/classgraph/test/methodannotation2/TestMethodMetaAnnotation.java b/src/test/java/io/github/classgraph/test/methodannotation2/TestMethodMetaAnnotation.java index d3012835f..228cffd25 100644 --- a/src/test/java/io/github/classgraph/test/methodannotation2/TestMethodMetaAnnotation.java +++ b/src/test/java/io/github/classgraph/test/methodannotation2/TestMethodMetaAnnotation.java @@ -36,17 +36,16 @@ import java.lang.annotation.Target; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; import io.github.classgraph.test.external.ExternalAnnotation; /** - * The Class TestMethodMetaAnnotation. + * TestMethodMetaAnnotation. */ public class TestMethodMetaAnnotation { - /** * The Interface MetaAnnotation. */ @@ -106,12 +105,11 @@ public void annotatedMethod() { @ExternalAnnotation public void testMetaAnnotation() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(TestMethodMetaAnnotation.class.getPackage().getName()).enableAnnotationInfo() + .acceptPackages(TestMethodMetaAnnotation.class.getPackage().getName()).enableAnnotationInfo() .scan()) { - final List testClasses = scanResult.getClassesWithAnnotation(MetaAnnotation.class.getName()) - .getNames(); - assertThat(testClasses).containsExactlyInAnyOrder(MethodAnnotation.class.getName(), - ClassAnnotation.class.getName(), MetaAnnotatedClass.class.getName()); + final List testClasses = scanResult.getClassesWithAnnotation(MetaAnnotation.class).getNames(); + assertThat(testClasses).containsOnly(MethodAnnotation.class.getName(), ClassAnnotation.class.getName(), + MetaAnnotatedClass.class.getName()); } } @@ -122,11 +120,11 @@ public void testMetaAnnotation() { @ExternalAnnotation public void testMetaAnnotationStandardClassesOnly() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(TestMethodMetaAnnotation.class.getPackage().getName()).enableAnnotationInfo() + .acceptPackages(TestMethodMetaAnnotation.class.getPackage().getName()).enableAnnotationInfo() .scan()) { - final List testClasses = scanResult.getClassesWithAnnotation(MetaAnnotation.class.getName()) + final List testClasses = scanResult.getClassesWithAnnotation(MetaAnnotation.class) .getStandardClasses().getNames(); - assertThat(testClasses).containsExactlyInAnyOrder(MetaAnnotatedClass.class.getName()); + assertThat(testClasses).containsOnly(MetaAnnotatedClass.class.getName()); } } @@ -137,11 +135,11 @@ public void testMetaAnnotationStandardClassesOnly() { @ExternalAnnotation public void testMethodMetaAnnotation() { try (ScanResult scanResult = new ClassGraph() - .whitelistPackages(TestMethodMetaAnnotation.class.getPackage().getName()).enableMethodInfo() + .acceptPackages(TestMethodMetaAnnotation.class.getPackage().getName()).enableMethodInfo() .enableAnnotationInfo().scan()) { - final List testClasses = scanResult - .getClassesWithMethodAnnotation(MetaAnnotation.class.getName()).getNames(); - assertThat(testClasses).containsExactlyInAnyOrder(ClassWithMetaAnnotatedMethod.class.getName()); + final List testClasses = scanResult.getClassesWithMethodAnnotation(MetaAnnotation.class) + .getNames(); + assertThat(testClasses).containsOnly(ClassWithMetaAnnotatedMethod.class.getName()); } } } diff --git a/src/test/java/io/github/classgraph/test/methodinfo/MethodInfoTest.java b/src/test/java/io/github/classgraph/test/methodinfo/MethodInfoTest.java index e04c9f031..e7eecc145 100644 --- a/src/test/java/io/github/classgraph/test/methodinfo/MethodInfoTest.java +++ b/src/test/java/io/github/classgraph/test/methodinfo/MethodInfoTest.java @@ -29,21 +29,41 @@ package io.github.classgraph.test.methodinfo; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import java.util.ArrayList; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import io.github.classgraph.ArrayClassInfo; +import io.github.classgraph.ArrayTypeSignature; import io.github.classgraph.ClassGraph; import io.github.classgraph.MethodInfo; import io.github.classgraph.MethodInfoList.MethodInfoFilter; +import io.github.classgraph.MethodParameterInfo; import io.github.classgraph.ScanResult; +import io.github.classgraph.TypeSignature; import io.github.classgraph.test.external.ExternalAnnotation; /** - * The Class MethodInfoTest. + * MethodInfoTest. */ public class MethodInfoTest { + /** + * The Class X. + */ + public static class X extends Exception { + /***/ + private static final long serialVersionUID = 1L; + + /** + * Method. + */ + public void xMethod() { + } + } /** * Public method with args. @@ -60,13 +80,15 @@ public class MethodInfoTest { * the b * @param l * the l + * @param xArray + * the x array * @param varargs * the varargs * @return the int */ @ExternalAnnotation public final int publicMethodWithArgs(final String str, final char c, final long j, final float[] f, - final byte[][] b, final List l, final int[]... varargs) { + final byte[][] b, final List l, final X[][][] xArray, final String[]... varargs) { return 0; } @@ -80,15 +102,22 @@ private static String[] privateMethod() { return null; } + public void throwsException() throws X { + } + + public void throwsGenericException() throws X, X2 { + } + /** * Method info not enabled. */ - @Test(expected = IllegalArgumentException.class) + @Test public void methodInfoNotEnabled() { // .enableSaveMethodInfo() not called - try (ScanResult scanResult = new ClassGraph().whitelistPackages(MethodInfoTest.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(MethodInfoTest.class.getPackage().getName()) .scan()) { - scanResult.getClassInfo(MethodInfoTest.class.getName()).getMethodInfo(); + Assertions.assertThrows(IllegalArgumentException.class, + () -> scanResult.getClassInfo(MethodInfoTest.class.getName()).getMethodInfo()); } } @@ -97,29 +126,31 @@ public void methodInfoNotEnabled() { */ @Test public void testGetMethodInfo() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(MethodInfoTest.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(MethodInfoTest.class.getPackage().getName()) .enableClassInfo().enableMethodInfo().enableAnnotationInfo().scan()) { assertThat(scanResult.getClassInfo(MethodInfoTest.class.getName()).getMethodInfo() .filter(new MethodInfoFilter() { @Override public boolean accept(final MethodInfo methodInfo) { // JDK 10 fix - return !methodInfo.getName().equals("$closeResource"); + return !methodInfo.getName().equals("$closeResource") + && !methodInfo.getName().equals("lambda$0") && !methodInfo.isSynthetic(); } - }).getAsStrings()).containsExactlyInAnyOrder( // + }).getAsStrings()).containsOnly( // "@" + ExternalAnnotation.class.getName() // - + " public final int publicMethodWithArgs" - + "(java.lang.String, char, long, float[], byte[][], " - + "java.util.List, int[]...)", - "@" + Test.class.getName() - + "(expected=class java.lang.IllegalArgumentException, timeout=0) " - + "public void methodInfoNotEnabled()", - "@" + Test.class.getName() + "(expected=class org.junit.Test$None, timeout=0) " - + "public void testGetMethodInfo()", - "@" + Test.class.getName() + "(expected=class org.junit.Test$None, timeout=0) " - + "public void testGetConstructorInfo()", - "@" + Test.class.getName() + "(expected=class org.junit.Test$None, timeout=0) " - + "public void testGetMethodInfoIgnoringVisibility()"); + + " public final int publicMethodWithArgs(final java.lang.String str, " + + "final char c, final long j, final float[] f, final byte[][] b, " + + "final java.util.List l, " + "final " + X.class.getName() + + "[][][] xArray, " + "final java.lang.String[]... varargs)", + "public void throwsException() throws " + X.class.getName(), + "public void throwsGenericException() throws " + + X.class.getName() + ", X2", + "@" + Test.class.getName() + " public void methodInfoNotEnabled()", + "@" + Test.class.getName() + " public void testGetMethodInfo()", + "@" + Test.class.getName() + " public void testGetConstructorInfo()", + "@" + Test.class.getName() + " public void testGetMethodInfoIgnoringVisibility()", + "@" + Test.class.getName() + " public void testGetThrownExceptions()", + "@" + Test.class.getName() + " public void testMethodInfoLoadMethodForArrayArg()"); } } @@ -128,10 +159,10 @@ public boolean accept(final MethodInfo methodInfo) { */ @Test public void testGetConstructorInfo() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(MethodInfoTest.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(MethodInfoTest.class.getPackage().getName()) .enableMethodInfo().scan()) { assertThat(scanResult.getClassInfo(MethodInfoTest.class.getName()).getConstructorInfo().getAsStrings()) - .containsExactlyInAnyOrder("public ()"); + .containsOnly("public ()"); } } @@ -140,30 +171,93 @@ public void testGetConstructorInfo() { */ @Test public void testGetMethodInfoIgnoringVisibility() { - try (ScanResult scanResult = new ClassGraph().whitelistPackages(MethodInfoTest.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(MethodInfoTest.class.getPackage().getName()) .enableClassInfo().enableMethodInfo().enableAnnotationInfo().ignoreMethodVisibility().scan()) { assertThat(scanResult.getClassInfo(MethodInfoTest.class.getName()).getMethodInfo() .filter(new MethodInfoFilter() { @Override public boolean accept(final MethodInfo methodInfo) { // JDK 10 fix - return !methodInfo.getName().equals("$closeResource"); + return !methodInfo.getName().equals("$closeResource") + && !methodInfo.getName().equals("lambda$0") && !methodInfo.isSynthetic(); } - }).getAsStrings()).containsExactlyInAnyOrder( // + }).getAsStrings()).containsOnly( // "@" + ExternalAnnotation.class.getName() // - + " public final int publicMethodWithArgs" - + "(java.lang.String, char, long, float[], byte[][], " - + "java.util.List, int[]...)", + + " public final int publicMethodWithArgs(final java.lang.String str, " + + "final char c, final long j, final float[] f, final byte[][] b, " + + "final java.util.List l, " + "final " + X.class.getName() + + "[][][] xArray, " + "final java.lang.String[]... varargs)", "private static java.lang.String[] privateMethod()", - "@" + Test.class.getName() - + "(expected=class java.lang.IllegalArgumentException, timeout=0) " - + "public void methodInfoNotEnabled()", - "@" + Test.class.getName() + "(expected=class org.junit.Test$None, timeout=0) " - + "public void testGetMethodInfo()", - "@" + Test.class.getName() + "(expected=class org.junit.Test$None, timeout=0) " - + "public void testGetConstructorInfo()", - "@" + Test.class.getName() + "(expected=class org.junit.Test$None, timeout=0) " - + "public void testGetMethodInfoIgnoringVisibility()"); + "public void throwsException() throws " + X.class.getName(), + "public void throwsGenericException() throws " + + X.class.getName() + ", X2", + "@" + Test.class.getName() + " public void methodInfoNotEnabled()", + "@" + Test.class.getName() + " public void testGetMethodInfo()", + "@" + Test.class.getName() + " public void testGetConstructorInfo()", + "@" + Test.class.getName() + " public void testGetMethodInfoIgnoringVisibility()", + "@" + Test.class.getName() + " public void testGetThrownExceptions()", + "@" + Test.class.getName() + " public void testMethodInfoLoadMethodForArrayArg()"); + } + } + + /** + * MethodInfo.loadClassAndGetMethod for arrays argument (#344) + */ + @Test + public void testMethodInfoLoadMethodForArrayArg() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(MethodInfoTest.class.getPackage().getName()) + .enableClassInfo().enableMethodInfo().enableAnnotationInfo().scan()) { + final MethodInfo mi = scanResult.getClassInfo(MethodInfoTest.class.getName()).getMethodInfo() + .getSingleMethod("publicMethodWithArgs"); + assertThat(mi).isNotNull(); + assertThatCode(() -> { + mi.loadClassAndGetMethod(); + }).doesNotThrowAnyException(); + assertThat(mi.loadClassAndGetMethod()).isNotNull(); + + // Extract array-typed params from method params + final List arrayClassInfoList = new ArrayList<>(); + for (final MethodParameterInfo mpi : mi.getParameterInfo()) { + final TypeSignature paramTypeSig = mpi.getTypeSignatureOrTypeDescriptor(); + if (paramTypeSig instanceof ArrayTypeSignature) { + arrayClassInfoList.add(((ArrayTypeSignature) paramTypeSig).getArrayClassInfo()); + } + } + assertThat(arrayClassInfoList.toString()).isEqualTo("[class float[], class byte[][], " + "class " + + X.class.getName() + "[][][], " + "class java.lang.String[][]]"); + final ArrayClassInfo p1 = arrayClassInfoList.get(1); + assertThat(p1.loadElementClass()).isEqualTo(byte.class); + assertThat(p1.loadClass()).isEqualTo(byte[][].class); + assertThat(p1.getElementClassInfo()).isNull(); + assertThat(p1.getNumDimensions()).isEqualTo(2); + final ArrayClassInfo p2 = arrayClassInfoList.get(2); + assertThat(p2.loadElementClass()).isEqualTo(X.class); + assertThat(p2.getElementClassInfo().getName()).isEqualTo(X.class.getName()); + assertThat(p2.loadClass()).isEqualTo(X[][][].class); + assertThat(p2.getElementClassInfo().getMethodInfo().get(0).getName()).isEqualTo("xMethod"); + assertThat(p2.getNumDimensions()).isEqualTo(3); + final ArrayClassInfo p3 = arrayClassInfoList.get(3); + assertThat(p3.loadElementClass()).isEqualTo(String.class); + assertThat(p3.loadClass()).isEqualTo(String[][].class); + assertThat(p3.getElementClassInfo()).isNull(); + assertThat(p3.getNumDimensions()).isEqualTo(2); + } + } + + @Test + public void testGetThrownExceptions() { + try (ScanResult scanResult = new ClassGraph().acceptPackages(MethodInfoTest.class.getPackage().getName()) + .enableClassInfo().enableMethodInfo().scan()) { + MethodInfo mi = scanResult.getClassInfo(MethodInfoTest.class.getName()).getMethodInfo() + .getSingleMethod("throwsException"); + assertThat(mi.getThrownExceptions()).hasSize(1); + assertThat(mi.getThrownExceptions().get(0).getSimpleName()).isEqualTo("X"); + + mi = scanResult.getClassInfo(MethodInfoTest.class.getName()).getMethodInfo() + .getSingleMethod("throwsGenericException"); + assertThat(mi.getThrownExceptions()).hasSize(2); + assertThat(mi.getThrownExceptions().get(0).getSimpleName()).isEqualTo("X"); + assertThat(mi.getThrownExceptions().get(1).getSimpleName()).isEqualTo("X"); } } } diff --git a/src/test/java/io/github/classgraph/test/parameterannotation/RetentionPolicyForFunctionParameterAnnotationsTest.java b/src/test/java/io/github/classgraph/test/parameterannotation/RetentionPolicyForFunctionParameterAnnotationsTest.java new file mode 100644 index 000000000..a2bb6aaa2 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/parameterannotation/RetentionPolicyForFunctionParameterAnnotationsTest.java @@ -0,0 +1,231 @@ +package io.github.classgraph.test.parameterannotation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.ScanResult; + +/** + * This class tests for function parameter annotations with different retention policies. + * + * @author Tony Nguyen + * @version 4.8.22 + * @see + * RetentionPolicy + */ +public class RetentionPolicyForFunctionParameterAnnotationsTest { + /** The scan result. */ + private static ScanResult scanResult; + + /** The class info. */ + private static ClassInfo classInfo; + + /** The Constant RETENTION_CLASS. */ + private final static String RETENTION_CLASS = "retention_class"; + + /** The Constant RETENTION_RUNTIME. */ + private final static String RETENTION_RUNTIME = "retention_runtime"; + + /** The Constant RETENTION_SOURCE. */ + private final static String RETENTION_SOURCE = "retention_source"; + + /** + * The Interface ParamAnnoRuntime. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface ParamAnnoRuntime { + /** + * Value. + * + * @return the string + */ + String value() default RETENTION_RUNTIME; + } + + /** + * The Interface ParamAnnoClass. + */ + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.PARAMETER) + public @interface ParamAnnoClass { + /** + * Value. + * + * @return the string + */ + String value() default RETENTION_CLASS; + } + + /** + * The Interface ParamAnnoSource. + */ + @Retention(RetentionPolicy.SOURCE) + @Target(ElementType.PARAMETER) + public @interface ParamAnnoSource { + /** + * Value. + * + * @return the string + */ + String value() default RETENTION_SOURCE; + } + + /** + * Generate ScanResult before running tests. + */ + @BeforeAll + public static void beforeClass() { + scanResult = new ClassGraph() + .acceptPackages(RetentionPolicyForFunctionParameterAnnotationsTest.class.getPackage().getName()) + .enableAllInfo().scan(); + classInfo = scanResult.getClassInfo(RetentionPolicyForFunctionParameterAnnotationsTest.class.getName()); + } + + /** + * Close ScanResult after running tests. + */ + @AfterAll + public static void afterClass() { + scanResult.close(); + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Must be able to detect parameter annotation with RUNTIME retention. + */ + @Test + public void canDetect_ParameterAnnotation_WithRuntimeRetention() { + final MethodInfo methodInfo = classInfo.getMethodInfo() + .getSingleMethod("parameterAnnotation_WithRuntimeRetention"); + + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); + } + + /** + * Parameter annotation with runtime retention. + * + * @param input + * the input + */ + public void parameterAnnotation_WithRuntimeRetention(@ParamAnnoRuntime final int input) { + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * The Interface SecondParamAnnoRuntime. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface SecondParamAnnoRuntime { + } + + /** + * Should be able to detect multiple annotations with RUNTIME retention for a single function parameter. + */ + @Test + public void canDetect_TwoAnnotations_WithRuntimeRetention_ForSingleParam() { + final MethodInfo methodInfo = classInfo.getMethodInfo() + .getSingleMethod("twoAnnotations_WithRuntimeRetention_ForSingleParam"); + + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); + + assertThat(methodInfo.hasParameterAnnotation(SecondParamAnnoRuntime.class)).isTrue(); + } + + /** + * Two annotations with runtime retention for single param. + * + * @param input + * the input + */ + public void twoAnnotations_WithRuntimeRetention_ForSingleParam( + @ParamAnnoRuntime @SecondParamAnnoRuntime final int input) { + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Annotations with CLASS retention does not need to be retained by vm at run time, but annotations with RUNTIME + * retention should still be detectable. + */ + @Test + public void canDetect_ParameterAnnotation_OneRuntimeRetention_OneClassRetention() { + final MethodInfo methodInfo = classInfo.getMethodInfo() + .getSingleMethod("oneRuntimeRetention_OneClassRetention"); + + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); + } + + /** + * One runtime retention annotation, one class retention annotation. + * + * @param input + * the input + */ + public void oneRuntimeRetention_OneClassRetention(@ParamAnnoRuntime @ParamAnnoClass final int input) { + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Annotations with CLASS retention does not need to be retained by vm at run time, but annotations with RUNTIME + * retention should still be detectable. + * + * This tests a changed ordering of the annotations with different retention policies. + */ + @Test + public void canDetect_ParameterAnnotation_OneRuntimeRetention_OneClassRetention_ChangedAnnotationOrder() { + final MethodInfo methodInfo = classInfo.getMethodInfo() + .getSingleMethod("oneRuntimeRetention_OneClassRetention_ChangedAnnotationOrder"); + + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); + } + + /** + * One runtime retention annotation, one class retention annotation, in reverse order. + * + * @param input + * the input + */ + public void oneRuntimeRetention_OneClassRetention_ChangedAnnotationOrder( + @ParamAnnoClass @ParamAnnoRuntime final int input) { + } + + // ------------------------------------------------------------------------------------------------------------- + + /** + * Annotations with SOURCE retention are discarded on compilation, but annotations with RUNTIME retention should + * still be detectable. + */ + @Test + public void canDetect_ParameterAnnotation_OneRuntimeRetention_OneSourceRetention() { + final MethodInfo methodInfo = classInfo.getMethodInfo() + .getSingleMethod("oneRuntimeRetention_OneSourceRetention"); + + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); + } + + /** + * One runtime retention annotation, one source retention annotation. + * + * @param input + * the input + */ + public void oneRuntimeRetention_OneSourceRetention(@ParamAnnoRuntime @ParamAnnoSource final int input) { + } +} \ No newline at end of file diff --git a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedAnnotation.java b/src/test/java/io/github/classgraph/test/rejected/RejectedAnnotation.java similarity index 64% rename from src/test/java/io/github/classgraph/test/blacklisted/BlacklistedAnnotation.java rename to src/test/java/io/github/classgraph/test/rejected/RejectedAnnotation.java index a1ca11231..8df320ada 100644 --- a/src/test/java/io/github/classgraph/test/blacklisted/BlacklistedAnnotation.java +++ b/src/test/java/io/github/classgraph/test/rejected/RejectedAnnotation.java @@ -1,4 +1,4 @@ -package io.github.classgraph.test.blacklisted; +package io.github.classgraph.test.rejected; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -6,9 +6,9 @@ import java.lang.annotation.Target; /** - * The Interface BlacklistedAnnotation. + * The Interface RejectedAnnotation. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -public @interface BlacklistedAnnotation { +public @interface RejectedAnnotation { } diff --git a/src/test/java/io/github/classgraph/test/rejected/RejectedInterface.java b/src/test/java/io/github/classgraph/test/rejected/RejectedInterface.java new file mode 100644 index 000000000..8125f209a --- /dev/null +++ b/src/test/java/io/github/classgraph/test/rejected/RejectedInterface.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.rejected; + +/** + * The Interface RejectedInterface. + */ +public interface RejectedInterface { +} diff --git a/src/test/java/io/github/classgraph/test/rejected/RejectedSubclass.java b/src/test/java/io/github/classgraph/test/rejected/RejectedSubclass.java new file mode 100644 index 000000000..5cb399109 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/rejected/RejectedSubclass.java @@ -0,0 +1,9 @@ +package io.github.classgraph.test.rejected; + +import io.github.classgraph.test.accepted.Accepted; + +/** + * RejectedSubclass. + */ +public class RejectedSubclass extends Accepted { +} diff --git a/src/test/java/io/github/classgraph/test/rejected/RejectedSubinterface.java b/src/test/java/io/github/classgraph/test/rejected/RejectedSubinterface.java new file mode 100644 index 000000000..d51c5a23e --- /dev/null +++ b/src/test/java/io/github/classgraph/test/rejected/RejectedSubinterface.java @@ -0,0 +1,9 @@ +package io.github.classgraph.test.rejected; + +import io.github.classgraph.test.accepted.AcceptedInterface; + +/** + * The Interface RejectedSubinterface. + */ +public interface RejectedSubinterface extends AcceptedInterface { +} diff --git a/src/test/java/io/github/classgraph/test/rejected/RejectedSuperclass.java b/src/test/java/io/github/classgraph/test/rejected/RejectedSuperclass.java new file mode 100644 index 000000000..e9e1878f1 --- /dev/null +++ b/src/test/java/io/github/classgraph/test/rejected/RejectedSuperclass.java @@ -0,0 +1,7 @@ +package io.github.classgraph.test.rejected; + +/** + * RejectedSuperclass. + */ +public class RejectedSuperclass { +} diff --git a/src/test/java/io/github/classgraph/test/utils/LogNodeTest.java b/src/test/java/io/github/classgraph/test/utils/LogNodeTest.java index d9e6d8d0b..18beaa73e 100644 --- a/src/test/java/io/github/classgraph/test/utils/LogNodeTest.java +++ b/src/test/java/io/github/classgraph/test/utils/LogNodeTest.java @@ -1,6 +1,6 @@ package io.github.classgraph.test.utils; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.PrintStream; @@ -8,52 +8,53 @@ import java.util.logging.Level; import java.util.logging.Logger; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import io.github.classgraph.ClassGraph; import nonapi.io.github.classgraph.utils.LogNode; /** - * The Class LogNodeTest. + * LogNodeTest. */ public class LogNodeTest { + private ConsoleHandler errPrintStreamHandler = null; + private final Logger rootLogger = Logger.getLogger(""); + + /** Reset encapsulation circumvention method after each test. */ + @AfterEach + void resetAfterTest() { + rootLogger.removeHandler(errPrintStreamHandler); + // Set to System.err + System.setErr(System.err); + } /** * Test log node logging to system err. */ @Test public void testLogNodeLoggingToSystemErr() { - ConsoleHandler errPrintStreamHandler = null; - final Logger rootLogger = Logger.getLogger(""); - try { - - // Set the System.err - final ByteArrayOutputStream err = new ByteArrayOutputStream(); - System.setErr(new PrintStream(err)); - - errPrintStreamHandler = new ConsoleHandler(); - errPrintStreamHandler.setLevel(Level.INFO); - rootLogger.addHandler(errPrintStreamHandler); - - final LogNode node = new LogNode(); - node.log("any logging message").log("child message").log("sub child message"); - node.log("another root"); - node.flush(); - - final Logger log = Logger.getLogger(ClassGraph.class.getName()); - if (log.isLoggable(Level.INFO)) { - final String systemErrMessages = new String(err.toByteArray()); - assertTrue(systemErrMessages.contains("any logging message")); - assertTrue(systemErrMessages.contains("-- child message")); - assertTrue(systemErrMessages.contains("---- sub child message")); - assertTrue(systemErrMessages.contains("another root")); - // System.out.println(systemErrMessages); - } // else logging will not take place - - } finally { - rootLogger.removeHandler(errPrintStreamHandler); - // Set to System.err - System.setErr(System.err); - } + // Set the System.err + final ByteArrayOutputStream err = new ByteArrayOutputStream(); + System.setErr(new PrintStream(err)); + + errPrintStreamHandler = new ConsoleHandler(); + errPrintStreamHandler.setLevel(Level.INFO); + rootLogger.addHandler(errPrintStreamHandler); + + final LogNode node = new LogNode(); + node.log("any logging message").log("child message").log("sub child message"); + node.log("another root"); + node.flush(); + + final Logger log = Logger.getLogger(ClassGraph.class.getName()); + if (log.isLoggable(Level.INFO)) { + final String systemErrMessages = new String(err.toByteArray()); + assertTrue(systemErrMessages.contains("any logging message")); + assertTrue(systemErrMessages.contains("-- child message")); + assertTrue(systemErrMessages.contains("---- sub child message")); + assertTrue(systemErrMessages.contains("another root")); + // System.out.println(systemErrMessages); + } // else logging will not take place } } diff --git a/src/test/java/io/github/classgraph/test/whitelisted/Cls.java b/src/test/java/io/github/classgraph/test/whitelisted/Cls.java deleted file mode 100644 index f2a424f78..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/Cls.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -/** - * The Class Cls. - */ -public class Cls { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/ClsSub.java b/src/test/java/io/github/classgraph/test/whitelisted/ClsSub.java deleted file mode 100644 index a38227b8e..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/ClsSub.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -/** - * The Class ClsSub. - */ -public class ClsSub extends Cls { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/ClsSubSub.java b/src/test/java/io/github/classgraph/test/whitelisted/ClsSubSub.java deleted file mode 100644 index d2e45652c..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/ClsSubSub.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -/** - * The Class ClsSubSub. - */ -public class ClsSubSub extends ClsSub { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/Impl1.java b/src/test/java/io/github/classgraph/test/whitelisted/Impl1.java deleted file mode 100644 index 2fb4c4a20..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/Impl1.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -/** - * The Class Impl1. - */ -public class Impl1 implements IfaceSubSub { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/Impl1Sub.java b/src/test/java/io/github/classgraph/test/whitelisted/Impl1Sub.java deleted file mode 100644 index 399c20e6f..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/Impl1Sub.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -/** - * The Class Impl1Sub. - */ -public class Impl1Sub extends Impl1 { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/Impl1SubSub.java b/src/test/java/io/github/classgraph/test/whitelisted/Impl1SubSub.java deleted file mode 100644 index 0ad6a16b8..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/Impl1SubSub.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -/** - * The Class Impl1SubSub. - */ -public class Impl1SubSub extends Impl1Sub { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/Impl2.java b/src/test/java/io/github/classgraph/test/whitelisted/Impl2.java deleted file mode 100644 index 353196d4b..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/Impl2.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -/** - * The Class Impl2. - */ -public class Impl2 implements Iface { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/Impl2Sub.java b/src/test/java/io/github/classgraph/test/whitelisted/Impl2Sub.java deleted file mode 100644 index ead825ce7..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/Impl2Sub.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -/** - * The Class Impl2Sub. - */ -public class Impl2Sub extends Impl2 { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/Whitelisted.java b/src/test/java/io/github/classgraph/test/whitelisted/Whitelisted.java deleted file mode 100644 index 9acd09836..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/Whitelisted.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -import io.github.classgraph.test.blacklisted.BlacklistedAnnotation; -import io.github.classgraph.test.blacklisted.BlacklistedInterface; -import io.github.classgraph.test.blacklisted.BlacklistedSuperclass; - -/** - * The Class Whitelisted. - */ -@BlacklistedAnnotation -public class Whitelisted extends BlacklistedSuperclass implements BlacklistedInterface { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/WhitelistedInterface.java b/src/test/java/io/github/classgraph/test/whitelisted/WhitelistedInterface.java deleted file mode 100644 index b786bfb76..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/WhitelistedInterface.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.classgraph.test.whitelisted; - -import io.github.classgraph.test.blacklisted.BlacklistedInterface; - -/** - * The Interface WhitelistedInterface. - */ -public interface WhitelistedInterface extends BlacklistedInterface { -} diff --git a/src/test/java/io/github/classgraph/test/whitelisted/blacklistedsub/BlacklistedSub.java b/src/test/java/io/github/classgraph/test/whitelisted/blacklistedsub/BlacklistedSub.java deleted file mode 100644 index 2f7a0cdb2..000000000 --- a/src/test/java/io/github/classgraph/test/whitelisted/blacklistedsub/BlacklistedSub.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.classgraph.test.whitelisted.blacklistedsub; - -/** - * The Class BlacklistedSub. - */ -public class BlacklistedSub { -} diff --git a/src/test/java/nonapi/io/github/classgraph/classpath/ClasspathFinderTest.java b/src/test/java/nonapi/io/github/classgraph/classpath/ClasspathFinderTest.java new file mode 100644 index 000000000..f1abfe7d8 --- /dev/null +++ b/src/test/java/nonapi/io/github/classgraph/classpath/ClasspathFinderTest.java @@ -0,0 +1,150 @@ +package nonapi.io.github.classgraph.classpath; + +import static nonapi.io.github.classgraph.classpath.SystemJarFinder.getJreRtJarPath; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.io.TempDir; + +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.scanspec.ScanSpec; +import nonapi.io.github.classgraph.utils.LogNode; + +public class ClasspathFinderTest { + + /** + * Test that {@link ScanSpec#enableSystemJarsAndModules}, {@link ScanSpec#ignoreParentClassLoaders}, and + * {@link ScanSpec#overrideClasspath} work in combination: + *

+ * Only the system jars and the override classpath should be found. + */ + @Test + @EnabledForJreRange(max = JRE.JAVA_8) + public void testOverrideClasspathAndEnableSystemJars(@TempDir final Path tmpDir) throws Exception { + // Arrange + final Path classesDir = tmpDir.toAbsolutePath().normalize().toRealPath(); + final ScanSpec scanSpec = new ScanSpec(); + scanSpec.enableSystemJarsAndModules = true; + scanSpec.ignoreParentClassLoaders = true; + scanSpec.overrideClasspath = Collections.singletonList(classesDir); + + // Act + final ClasspathFinder classpathFinder = new ClasspathFinder(scanSpec, new ReflectionUtils(), new LogNode()); + + // Assert + final Set paths = new TreeSet<>(); + for (final String path : classpathFinder.getClasspathOrder().getClasspathEntryUniqueResolvedPaths()) { + paths.add(Paths.get(path)); + } + assertTrue(paths.remove(classesDir), "Classpath should have contained " + classesDir + ": " + paths); + assertTrue(paths.remove(Paths.get(getJreRtJarPath())), + "Classpath should have contained system jars: " + paths); + assertEquals(0, paths.size(), "Classpath should have no other entries: " + paths); + } + + /** + * Test that {@link ScanSpec#enableSystemJarsAndModules}, {@link ScanSpec#ignoreParentClassLoaders}, and + * {@link ScanSpec#overrideClassLoaders} work in combination: + *

+ * Only the system jars and the override classloaders should be found. + */ + @Test + @EnabledForJreRange(max = JRE.JAVA_8) + public void testOverrideClassLoaderAndEnableSystemJars(@TempDir final Path tmpDir) throws Exception { + // Arrange + final Path classesDir = tmpDir.toAbsolutePath().normalize().toRealPath(); + final ScanSpec scanSpec = new ScanSpec(); + scanSpec.enableSystemJarsAndModules = true; + scanSpec.ignoreParentClassLoaders = true; + scanSpec.overrideClassLoaders(new URLClassLoader(new URL[] { classesDir.toUri().toURL() })); + + // Act + final ClasspathFinder classpathFinder = new ClasspathFinder(scanSpec, new ReflectionUtils(), new LogNode()); + + // Assert + final Set paths = new TreeSet<>(); + for (final String path : classpathFinder.getClasspathOrder().getClasspathEntryUniqueResolvedPaths()) { + paths.add(Paths.get(path)); + } + assertTrue(paths.remove(classesDir), "Classpath should have contained " + classesDir + ": " + paths); + assertTrue(paths.remove(Paths.get(getJreRtJarPath())), + "Classpath should have contained system jars: " + paths); + assertEquals(0, paths.size(), "Classpath should have no other entries: " + paths); + } + + /** + * Test that {@link ScanSpec#enableSystemJarsAndModules}, {@link ScanSpec#ignoreParentClassLoaders}, and + * {@link ScanSpec#overrideClasspath} work in combination: + *

+ * Only the system modules and the override classpath should be found. + */ + @Test + @EnabledForJreRange(min = JRE.JAVA_9) + public void testOverrideClasspathAndEnableSystemModules(@TempDir final Path tmpDir) throws Exception { + // Arrange + final Path classesDir = tmpDir.toAbsolutePath().normalize().toRealPath(); + final ScanSpec scanSpec = new ScanSpec(); + scanSpec.enableSystemJarsAndModules = true; + scanSpec.ignoreParentClassLoaders = true; + scanSpec.overrideClasspath = Collections. singletonList(classesDir); + + // Act + final ClasspathFinder classpathFinder = new ClasspathFinder(scanSpec, new ReflectionUtils(), new LogNode()); + final ModuleFinder moduleFinder = classpathFinder.getModuleFinder(); + + // Assert + assertNotNull(moduleFinder, "ModuleFinder should be non-null"); + assertTrue(moduleFinder.getSystemModuleRefs().size() > 0, "ModuleFinder should have found system modules"); + + final Set paths = new TreeSet<>(); + for (final String path : classpathFinder.getClasspathOrder().getClasspathEntryUniqueResolvedPaths()) { + paths.add(Paths.get(path)); + } + assertTrue(paths.remove(classesDir), "Classpath should have contained " + classesDir + ": " + paths); + assertEquals(0, paths.size(), "Classpath should have no other entries: " + paths); + } + + /** + * Test that {@link ScanSpec#enableSystemJarsAndModules}, {@link ScanSpec#ignoreParentClassLoaders}, and + * {@link ScanSpec#overrideClassLoaders} work in combination: + *

+ * Only the system modules and the override classloaders should be found. + */ + @Test + @EnabledForJreRange(min = JRE.JAVA_9) + public void testOverrideClassLoaderAndEnableSystemModules(@TempDir final Path tmpDir) throws Exception { + // Arrange + final Path classesDir = tmpDir.toAbsolutePath().normalize().toRealPath(); + final ScanSpec scanSpec = new ScanSpec(); + scanSpec.enableSystemJarsAndModules = true; + scanSpec.ignoreParentClassLoaders = true; + scanSpec.overrideClassLoaders(new URLClassLoader(new URL[] { classesDir.toUri().toURL() })); + + // Act + final ClasspathFinder classpathFinder = new ClasspathFinder(scanSpec, new ReflectionUtils(), new LogNode()); + final ModuleFinder moduleFinder = classpathFinder.getModuleFinder(); + + // Assert + assertNotNull(moduleFinder, "ModuleFinder should be non-null"); + assertTrue(moduleFinder.getSystemModuleRefs().size() > 0, "ModuleFinder should have found system modules"); + + final Set paths = new TreeSet<>(); + for (final String path : classpathFinder.getClasspathOrder().getClasspathEntryUniqueResolvedPaths()) { + paths.add(Paths.get(path)); + } + assertTrue(paths.remove(classesDir), "Classpath should have contained " + classesDir + ": " + paths); + assertEquals(0, paths.size(), "Classpath should have no other entries: " + paths); + } +} diff --git a/src/test/java/nonapi/io/github/classgraph/json/JSONParserTest.java b/src/test/java/nonapi/io/github/classgraph/json/JSONParserTest.java new file mode 100644 index 000000000..78d7d75d1 --- /dev/null +++ b/src/test/java/nonapi/io/github/classgraph/json/JSONParserTest.java @@ -0,0 +1,32 @@ +package nonapi.io.github.classgraph.json; + +import org.junit.jupiter.api.Test; + +import nonapi.io.github.classgraph.types.ParseException; + +/** + * Unit test. + */ +public class JSONParserTest { + /** + * Test double value. + * + * @throws ParseException + * the parse exception + */ + @Test + public void test1() throws ParseException { + JSONParser.parseJSON("{\"doubleValue\":-2.147483648}"); + } + + /** + * Test double value with exponent. + * + * @throws ParseException + * the parse exception + */ + @Test + public void test2() throws ParseException { + JSONParser.parseJSON("{\"doubleValue\":-2.147483648E9}"); + } +} \ No newline at end of file diff --git a/src/test/perf/io/github/classgraph/InputStreamBenchmark.java b/src/test/perf/io/github/classgraph/InputStreamBenchmark.java index 7767b747b..d2b971736 100644 --- a/src/test/perf/io/github/classgraph/InputStreamBenchmark.java +++ b/src/test/perf/io/github/classgraph/InputStreamBenchmark.java @@ -42,11 +42,10 @@ import org.openjdk.jmh.infra.Blackhole; /** - * The Class InputStreamBenchmark. + * InputStreamBenchmark. */ @State(Scope.Benchmark) public class InputStreamBenchmark { - /** The nb bytes. */ @Param({ "16", "4096", "32178", "500000", "5000000" }) public int nbBytes; diff --git a/src/test/perf/io/github/classgraph/issues/issue400/Issue400.java b/src/test/perf/io/github/classgraph/issues/issue400/Issue400.java new file mode 100644 index 000000000..ece4090ff --- /dev/null +++ b/src/test/perf/io/github/classgraph/issues/issue400/Issue400.java @@ -0,0 +1,92 @@ +package io.github.classgraph.issues.issue400; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Offset.offset; + +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.Test; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Verify that a large number of stored/deflated nested JAR entries don't cause memory problems. + * + * @author RĂ³bert Papp ( https://github.com/TWiStErRob ) + */ +public class Issue400 { + + private static final long MB = 1024 * 1024; + private static final long MEMORY_TOLERANCE = 5 * MB; + + /** + * @return used JVM heap size allocated in RAM + * @see What are Runtime.getRuntime().totalMemory() and + * freeMemory()? + */ + private long usedRam() { + final Runtime runtime = Runtime.getRuntime(); + runtime.gc(); + System.runFinalization(); + runtime.gc(); + System.runFinalization(); + runtime.gc(); + System.runFinalization(); + return (runtime.totalMemory() - runtime.freeMemory()); + } + + /** + * Test whether RAM leaks, or whether nested deflated jars cause large RAM overhead. + * + * @param jars + * the jar URLs. + */ + @SuppressWarnings("null") + private void loadsJarWithManyNestedEntriesAndDoesNotUseMuchMemory(final URL... jars) { + final long ramAtStart = usedRam(); + long ramAfterScan; + try (ScanResult scanResult = new ClassGraph().overrideClassLoaders(new URLClassLoader(jars)) + .ignoreParentClassLoaders().enableAllInfo().scan()) { + ramAfterScan = usedRam(); + // There are no classes in any of the JARs. + assertThat(scanResult.getAllClassesAsMap()).isEmpty(); + // Check if it contains the JAR and all nested entries. + assertThat(scanResult.getClasspathURLs()).hasSize(1 + 128); + } + final long ramAtEnd = usedRam(); + + if (ramAtStart < ramAfterScan) { + assertThat(ramAtStart) + .withFailMessage("Memory usage while using ScanResult should stay within reasonable range: " + + "went from %s to %s MB.", ramAtStart / MB, ramAfterScan / MB) + .isCloseTo(ramAfterScan, offset(MEMORY_TOLERANCE)); + } + + if (ramAtStart < ramAtEnd) { + assertThat(ramAtStart) + .withFailMessage("Memory usage after cleaning up should stay within reasonable range: " + + "went from %s to %s MB.", ramAtStart / MB, ramAtEnd / MB) + .isCloseTo(ramAtEnd, offset(MEMORY_TOLERANCE)); + } + } + + /** + * Test jar with stored entries. + */ + @Test + public void loadsStoredJarWithManyNestedEntriesAndDoesNotUseMuchMemory() { + loadsJarWithManyNestedEntriesAndDoesNotUseMuchMemory( + Issue400.class.getClassLoader().getResource("issue400-nested-stored.jar")); + } + + /** + * Test jar with deflated entries. + */ + @Test + public void loadsDeflatedJarWithManyNestedEntriesAndDoesNotUseMuchMemory() { + loadsJarWithManyNestedEntriesAndDoesNotUseMuchMemory( + Issue400.class.getClassLoader().getResource("issue400-nested-deflated.jar")); + } +} diff --git a/src/test/resources/class-path-manifest-entry.jar b/src/test/resources/class-path-manifest-entry.jar index b18aa7a47..0fc1d548d 100644 Binary files a/src/test/resources/class-path-manifest-entry.jar and b/src/test/resources/class-path-manifest-entry.jar differ diff --git a/src/test/resources/io/github/classgraph/issues/issue146/CompiledWithJDK8.java b/src/test/resources/io/github/classgraph/issues/issue146/CompiledWithJDK8.java index 3d8c484ab..9d30e3f69 100644 --- a/src/test/resources/io/github/classgraph/issues/issue146/CompiledWithJDK8.java +++ b/src/test/resources/io/github/classgraph/issues/issue146/CompiledWithJDK8.java @@ -2,6 +2,5 @@ // Compile this with JDK8, using the commandline switch: -parameters public class CompiledWithJDK8 { - public void method(int param0, String param1, double[] param2) {} } diff --git a/src/test/resources/issue340.jar b/src/test/resources/issue340.jar new file mode 100644 index 000000000..dbabb00bc Binary files /dev/null and b/src/test/resources/issue340.jar differ diff --git a/src/test/resources/issue364-no-permissions.jar b/src/test/resources/issue364-no-permissions.jar new file mode 100644 index 000000000..eceee1939 Binary files /dev/null and b/src/test/resources/issue364-no-permissions.jar differ diff --git a/src/test/resources/issue364-permissions.jar b/src/test/resources/issue364-permissions.jar new file mode 100644 index 000000000..482721515 Binary files /dev/null and b/src/test/resources/issue364-permissions.jar differ diff --git a/src/test/resources/issue400-nested-deflated.jar b/src/test/resources/issue400-nested-deflated.jar new file mode 100644 index 000000000..2c5a99731 Binary files /dev/null and b/src/test/resources/issue400-nested-deflated.jar differ diff --git a/src/test/resources/issue400-nested-stored.jar b/src/test/resources/issue400-nested-stored.jar new file mode 100644 index 000000000..a43d698cd Binary files /dev/null and b/src/test/resources/issue400-nested-stored.jar differ diff --git a/src/test/resources/issue468/x+y/z+w.jar b/src/test/resources/issue468/x+y/z+w.jar new file mode 100644 index 000000000..4e10f9c4d Binary files /dev/null and b/src/test/resources/issue468/x+y/z+w.jar differ diff --git a/src/test/resources/issue673/a.zip b/src/test/resources/issue673/a.zip new file mode 100644 index 000000000..8d5beffc9 Binary files /dev/null and b/src/test/resources/issue673/a.zip differ diff --git a/src/test/resources/issue673/b.zip b/src/test/resources/issue673/b.zip new file mode 100644 index 000000000..f74ac9b7b Binary files /dev/null and b/src/test/resources/issue673/b.zip differ diff --git a/src/test/resources/issue673/c.zip b/src/test/resources/issue673/c.zip new file mode 100644 index 000000000..8754898c4 Binary files /dev/null and b/src/test/resources/issue673/c.zip differ diff --git a/src/test/resources/issue766/ProjectWithAnnotations.iar b/src/test/resources/issue766/ProjectWithAnnotations.iar new file mode 100644 index 000000000..2f9c28832 Binary files /dev/null and b/src/test/resources/issue766/ProjectWithAnnotations.iar differ diff --git a/src/test/resources/issue797.jar b/src/test/resources/issue797.jar new file mode 100644 index 000000000..df7f1672c Binary files /dev/null and b/src/test/resources/issue797.jar differ diff --git a/src/test/resources/record.jar b/src/test/resources/record.jar new file mode 100644 index 000000000..5968f163e Binary files /dev/null and b/src/test/resources/record.jar differ diff --git a/src/test/resources/scalapackage.zip b/src/test/resources/scalapackage.zip new file mode 100644 index 000000000..83ce31671 Binary files /dev/null and b/src/test/resources/scalapackage.zip differ