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/workflows/ci.yml b/.github/workflows/ci.yml index 3ef408cde..d87baada3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,11 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - java: [ '8', '11', '12', '13', '14', '15', '16' ] + java: [ '8', '11', '13', '15', '16', '17', '18', '19' ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: ${{ matrix.java }} 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/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index c32394f14..000000000 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed 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. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.5"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 0d5e64988..000000000 Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 642d572ce..44f3cf2c1 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,2 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar +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 0e6b5e6db..8b3ccf423 100644 --- a/.project +++ b/.project @@ -1,23 +1,34 @@ - ClassGraph - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.m2e.core.maven2Nature - org.eclipse.jdt.core.javanature - + ClassGraph + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + 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/.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/README.md b/README.md index f2293fffb..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 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,7 +15,7 @@ ClassGraph is an uber-fast parallelized classpath scanner and module scanner for [![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) @@ -28,7 +28,7 @@ ClassGraph is an uber-fast parallelized classpath scanner and module scanner for
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/classgraph/classgraph/blob/master/LICENSE) -| ClassGraph is now fully stable. This project adheres to the **[Zero Bugs Commitment](https://github.com/classgraph/classgraph/blob/master/Zero-Bugs-Commitment.md)**. | +| ClassGraph is stable and mature, and has a low bug report rate, despite being used by hundreds of projects. | |-----------------------------| ### ClassGraph vs. Java Introspection @@ -104,6 +104,35 @@ Replace `X.Y.Z` below with the latest [release number](https://github.com/classg 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.** + +To use one of these libraries: + +* 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). + +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.** + ### 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). @@ -171,12 +200,13 @@ Some other classpath scanning mechanisms include: * [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) 2020 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: diff --git a/Zero-Bugs-Commitment.md b/Zero-Bugs-Commitment.md deleted file mode 100644 index 399949ea3..000000000 --- a/Zero-Bugs-Commitment.md +++ /dev/null @@ -1,77 +0,0 @@ -# The Zero Bugs Commitment - -This project adheres to the **Zero Bugs Commitment** (`#ZeroBugs`). -This is a commitment to practice *responsible software engineering*, -*proactive community participation*, and *positive community engagement*, -with a goal of keeping the count of known or open bugs at zero, while -respecting and cultivating contributions. - -**As developers of this project, we pledge that:** - -## (1) We will prioritize fixing bugs over implementing new features. - -*(Motivation: It is human nature to be much more interested in building new -things than doing the hard work to fix old, broken things.)* - -💡 We pledge, wherever reasonable, to prioritize fixing known bugs above -implementing new features, with the goal of **keeping the count of known or -open bugs at zero**. - -## (2) We will take responsibility for code we have written or contributed to. - -*(Motivation: As attention shifts between projects, it is difficult to return -to work on old code. This can lead to bit rot.)* - -💡 We pledge to take long-term responsibility for any significant code we -create or contribute to, fixing problems and updating code as necessary to -prevent bit rot. If we can no longer fulfill this responsibility, we will -find someone else who can assume the responsibility for our code. - -Note that this is about taking *personal responsibility* for our own work, not -about who has *official maintainership* for a project or piece of code. - -## (3) We will be responsive during the bugfixing process. - -*(Motivation: It is easy to delay responding to a bug report, a bug comment or -a request until the issue becomes forgotten or obsolete. This is the unfortunate -end state of a significant proportion of bug reports filed across the open -source ecosystem.)* - -💡 We pledge to be responsive to bug reports, comments and requests currently -open in our project's bug tracker, and to be proactive in resolving problems -as quickly as practical. - -## (4) We will be respectful and inclusive. - -*(Motivation: Open source communities have been known to reject halting but -earnest efforts of new contributors. Bug trackers are also full of pet -complaints and bugs closed as `#WONTFIX`, without a significant attempt to -understand core issues, or to find a solution or compromise.)* - -💡 We pledge to cultivate contributions and growth in our community by -welcoming, encouraging, and helping users who offer contributions; -by striving to listen to the needs and requests of community members; -and by trying to find a reasonable solution or middle ground when there is -a disagreement. - ---- - -**THIS DOCUMENT IS IN THE PUBLIC DOMAIN** - -You are strongly encouraged to share this commitment, to make this commitment -yourself for software that you develop or maintain, and to encourage others to -do the same. - -**To sign this pledge**, you can add the following wording to your project -homepage, linking to this document, or to your own copy or your own version of -this document: - -| **This project adheres to the [Zero Bugs Commitment](https://github.com/classgraph/classgraph/blob/master/Zero-Bugs-Commitment.md).** | -|-----------------------------| - -You may modify or redistribute this document at will without restriction. -However, please leave a record of your changes below. - -#### Version history: - -0.1: Original version (author: Luke Hutchison) diff --git a/mvnw b/mvnw index 41c0f0c23..e9cf8d330 100755 --- a/mvnw +++ b/mvnw @@ -19,292 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.3 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# 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 # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# 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 -fi +# 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" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + 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 - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + 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 -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - 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 +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +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:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +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 -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +# 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" +} -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -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 -if [ -z "$JAVACMD" ] ; then - 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" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +# 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 -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +mkdir -p -- "${MAVEN_HOME%/*}" -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# 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 -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +# 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 -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# 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 "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +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 - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break +# 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 - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + 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 - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + 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 -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - 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 - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +# 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 -########################################################################################## -# End of extension -########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# 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 -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +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 -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +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" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 86115719e..3fd2be860 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,182 +1,189 @@ -@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 Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +<# : 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 5fa4b55c9..f19d6afaf 100644 --- a/pom.xml +++ b/pom.xml @@ -1,576 +1,566 @@ - 4.0.0 + 4.0.0 - io.github.classgraph - classgraph - 4.8.106-SNAPSHOT - ClassGraph + io.github.classgraph + classgraph + 4.8.184 + ClassGraph - The uber-fast, ultra-lightweight classpath and module 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 - ClassGraph - https://github.com/classgraph - - + + + 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 - classgraph-4.8.105 - + + 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 + + - - + + + + + - - - - - + + + + + io.github.toolfactory + narcissus + 1.0.11 + true + - - - - - org.junit.jupiter - junit-jupiter - 5.7.1 - test - - - org.openjdk.jmh - jmh-core - 1.29 - test - - - org.openjdk.jmh - jmh-generator-annprocess - 1.29 - test - - - org.assertj - assertj-core - 3.19.0 - test - - - javax.enterprise - cdi-api - 2.0 - test - - - org.ops4j.pax.url - pax-url-aether - 2.6.1 - test - - - org.slf4j - slf4j-api - 2.0.0-alpha1 - test - - - org.slf4j - slf4j-jdk14 - 2.0.0-alpha1 - test - - - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.2.Final - test - - - com.google.jimfs - jimfs - 1.2 - test - + + + 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.eclipse.jdt + org.eclipse.jdt.annotation + 2.3.0 + provided + + - - - - - - org.eclipse.jdt - org.eclipse.jdt.annotation - 2.2.600 - provided - - + + + + + + + + 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.maven.plugins - maven-enforcer-plugin - 3.0.0-M3 - - - org.apache.maven.plugins - maven-resources-plugin - 3.2.0 - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M5 - - - org.codehaus.mojo - build-helper-maven-plugin - 3.2.0 - - - org.apache.maven.plugins - maven-jar-plugin - 3.2.0 - - - org.apache.maven.plugins - maven-antrun-plugin - 3.0.0 - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.2.0 - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - - - org.apache.maven.plugins - maven-release-plugin - 3.0.0-M4 - + + + org.apache.maven.plugins + maven-clean-plugin + 3.3.2 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.1 + - - - org.apache.maven.plugins - maven-clean-plugin - 3.1.0 - - - org.apache.maven.plugins - maven-deploy-plugin - 3.0.0-M1 - - - org.apache.maven.plugins - maven-install-plugin - 3.0.0-M1 - - - org.apache.maven.plugins - maven-site-plugin - 3.9.1 - - + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + true + + + - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - org.codehaus.mojo - animal-sniffer-enforcer-rule - 1.20 - - - - - - enforce-versions - validate - - enforce - - - - - [3.6.3,) - - - - - - - check-signatures - compile - - enforce - - - - - - org.codehaus.mojo.signature - java17 - 1.0 - - - - - - - + + + + + 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-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-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-antrun-plugin - - - - - - - - - - - add-module-info-to-jar - package - - run - - - - - - - - - - - + + org.apache.maven.plugins + maven-antrun-plugin + + + + + + + + + + + add-module-info-to-jar + package + + run + + + + + + + + + + + + + + + add-modular-javadoc + verify + + run + + + + + + + + + + + - - - org.apache.maven.plugins - maven-compiler-plugin - - UTF-8 - - - - - - - - - - - - 7 - 7 - - 8 - 8 - false - - - - default-testCompile - test-compile - - testCompile - - - UTF-8 - - 8 - 8 - - - - + + + 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.apache.maven.plugins - maven-surefire-plugin - + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${surefireArgLine} + + - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-test-source - generate-test-sources - - add-test-source - - - - src/test/perf - - - - - + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test/perf + + + + + - - - 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,jdk.internal.misc;resolution:="optional",sun.misc;resolution:="optional",sun.nio.ch;resolution:="optional" - - - true - - - - + + + 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 + + + + - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - package - - jar-no-fork - - - - + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + package + + jar-no-fork + + + + - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-javadocs - package - - jar - - - 8 - ${javadoc.html.version} - all - nonapi.* - public - - - - + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + package + + jar + + + 8 + ${javadoc.html.version} + all + nonapi.* + public + + + + + + + - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - true - - ossrh - https://oss.sonatype.org/ - true - 10 - - + + + + jdk9plus + + [9,) + + + -html5 + + + true + + + + jdk17plus + + [17,) + + + --enable-native-access=ALL-UNNAMED + + - - - org.apache.maven.plugins - maven-release-plugin - - deploy - deploy - true - - release - - -Prelease - - - - - - - - - - jdk9plus - - -html5 - - - true - - - [9,) - - - - - - release - - - - org.apache.maven.plugins - maven-gpg-plugin - - - --pinentry-mode - loopback - - ${gpg.keyname} - ${gpg.keyname} - - - - sign-artifacts - verify - - sign - - - - - - - - + + + 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 d3b3392e4..3ac4343ac 100644 --- a/src/main/java/io/github/classgraph/AnnotationClassRef.java +++ b/src/main/java/io/github/classgraph/AnnotationClassRef.java @@ -107,9 +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 ((ArrayTypeSignature) typeSignature).loadClass(ignoreExceptions); + return typeSignature.loadClass(ignoreExceptions); } else { throw new IllegalArgumentException("Got unexpected type " + typeSignature.getClass().getName() + " for ref type signature: " + typeDescriptorStr); @@ -142,7 +142,7 @@ protected String getClassName() { } else if (typeSignature instanceof ClassRefTypeSignature) { className = ((ClassRefTypeSignature) typeSignature).getFullyQualifiedClassName(); } else if (typeSignature instanceof ArrayTypeSignature) { - className = ((ArrayTypeSignature) typeSignature).getClassName(); + className = typeSignature.getClassName(); } else { throw new IllegalArgumentException("Got unexpected type " + typeSignature.getClass().getName() + " for ref type signature: " + typeDescriptorStr); @@ -214,6 +214,7 @@ protected void toString(final boolean useSimpleNames, final StringBuilder buf) { // } // } - buf.append(/* prefix + */ getTypeSignature().toString(useSimpleNames) + ".class"); + /* 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 104526630..4b855a260 100644 --- a/src/main/java/io/github/classgraph/AnnotationEnumValue.java +++ b/src/main/java/io/github/classgraph/AnnotationEnumValue.java @@ -118,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); } } diff --git a/src/main/java/io/github/classgraph/AnnotationInfo.java b/src/main/java/io/github/classgraph/AnnotationInfo.java index a619edaa2..356f1a41a 100644 --- a/src/main/java/io/github/classgraph/AnnotationInfo.java +++ b/src/main/java/io/github/classgraph/AnnotationInfo.java @@ -40,8 +40,8 @@ import java.util.Map.Entry; import java.util.Set; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; import nonapi.io.github.classgraph.utils.LogNode; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** Holds metadata about a specific annotation instance on a class, method, method parameter or field. */ public class AnnotationInfo extends ScanResultObject implements Comparable, HasName { @@ -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); + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -274,7 +288,7 @@ public ClassInfo getClassInfo() { 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. */ @@ -337,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; diff --git a/src/main/java/io/github/classgraph/AnnotationInfoList.java b/src/main/java/io/github/classgraph/AnnotationInfoList.java index 03b60fb5b..79a0151d7 100644 --- a/src/main/java/io/github/classgraph/AnnotationInfoList.java +++ b/src/main/java/io/github/classgraph/AnnotationInfoList.java @@ -28,6 +28,7 @@ */ package io.github.classgraph; +import java.lang.annotation.Annotation; import java.lang.annotation.Repeatable; import java.util.ArrayList; import java.util.HashSet; @@ -36,6 +37,7 @@ 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; @@ -344,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. * diff --git a/src/main/java/io/github/classgraph/AnnotationParameterValue.java b/src/main/java/io/github/classgraph/AnnotationParameterValue.java index ddbbec91d..caa3db213 100644 --- a/src/main/java/io/github/classgraph/AnnotationParameterValue.java +++ b/src/main/java/io/github/classgraph/AnnotationParameterValue.java @@ -252,7 +252,7 @@ private static void toString(final Object val, final boolean useSimpleNames, fin } else if (val instanceof ScanResultObject) { ((ScanResultObject) val).toString(useSimpleNames, buf); } else { - buf.append(val.toString()); + buf.append(val); } } diff --git a/src/main/java/io/github/classgraph/ArrayTypeSignature.java b/src/main/java/io/github/classgraph/ArrayTypeSignature.java index 1774904f1..6565c9e14 100644 --- a/src/main/java/io/github/classgraph/ArrayTypeSignature.java +++ b/src/main/java/io/github/classgraph/ArrayTypeSignature.java @@ -244,7 +244,7 @@ public Class loadElementClass(final boolean ignoreExceptions) { elementClassRef = elementTypeSignature.loadClass(ignoreExceptions); } else { // Fallback, if scanResult is not set - final String elementTypeName = ((ClassRefTypeSignature) elementTypeSignature).getClassName(); + final String elementTypeName = elementTypeSignature.getClassName(); try { elementClassRef = Class.forName(elementTypeName); } catch (final Throwable t) { diff --git a/src/main/java/io/github/classgraph/ClassGraph.java b/src/main/java/io/github/classgraph/ClassGraph.java index b712ab5eb..d77c06a8b 100644 --- a/src/main/java/io/github/classgraph/ClassGraph.java +++ b/src/main/java/io/github/classgraph/ClassGraph.java @@ -30,6 +30,7 @@ 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; @@ -48,6 +49,7 @@ 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; @@ -80,15 +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() { + reflectionUtils = new ReflectionUtils(); // Initialize ScanResult, if this is the first call to ClassGraph constructor - ScanResult.init(); + ScanResult.init(reflectionUtils); } /** @@ -155,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; } @@ -640,10 +683,6 @@ public ClassGraph acceptPackages(final String... packageNames) { enableClassInfo(); for (final String packageName : packageNames) { final String packageNameNormalized = AcceptReject.normalizePackageOrClassName(packageName); - if (packageNameNormalized.startsWith("!") || packageNameNormalized.startsWith("-")) { - throw new IllegalArgumentException( - "This style of accepting/rejecting is no longer supported: " + packageNameNormalized); - } // Accept package scanSpec.packageAcceptReject.addToAccept(packageNameNormalized); final String path = AcceptReject.packageNameToPath(packageNameNormalized); @@ -913,16 +952,13 @@ public ClassGraph blacklistPaths(final String... paths) { * * * @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 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 = AcceptReject.normalizePackageOrClassName(className); // Accept the class itself scanSpec.classAcceptReject.addToAccept(classNameNormalized); @@ -941,8 +977,7 @@ public ClassGraph acceptClasses(final String... classNames) { * Use {@link #acceptClasses(String...)} instead. * * @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). * @return this (for method chaining). * @deprecated Use {@link #acceptClasses(String...)} instead. */ @@ -959,16 +994,13 @@ public ClassGraph whitelistClasses(final String... classNames) { * N.B. Automatically calls {@link #enableClassInfo()}. * * @param classNames - * The fully-qualified names of classes to reject (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 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 = AcceptReject.normalizePackageOrClassName(className); scanSpec.classAcceptReject.addToReject(classNameNormalized); scanSpec.classfilePathAcceptReject @@ -981,8 +1013,7 @@ public ClassGraph rejectClasses(final String... classNames) { * Use {@link #rejectClasses(String...)} instead. * * @param classNames - * The fully-qualified names of classes to reject (using '.' as a separator). May not include a glob - * wildcard ({@code '*'}). + * The fully-qualified names of classes to reject (using '.' as a separator). * @return this (for method chaining). * @deprecated Use {@link #rejectClasses(String...)} instead. */ @@ -1080,7 +1111,7 @@ private void acceptOrRejectLibOrExtJars(final boolean accept, final String... ja } if (jarLeafName.contains("*")) { // Compare wildcarded pattern against all jars in lib and ext dirs - final Pattern pattern = AcceptReject.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); @@ -1381,14 +1412,44 @@ public ClassGraph setMaxBufferedJarRAMSize(final int maxBufferedJarRAMSize) { /** * 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; + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -1465,7 +1526,7 @@ public void run() { try { // Call scanner, but ignore the returned ScanResult new Scanner(/* performScan = */ true, scanSpec, executorService, numParallelTasks, - scanResultProcessor, failureHandler, topLevelLog).call(); + scanResultProcessor, failureHandler, reflectionUtils, topLevelLog).call(); } catch (final InterruptedException | CancellationException | ExecutionException e) { // Call failure handler failureHandler.onFailure(e); @@ -1493,7 +1554,7 @@ private Future scanAsync(final boolean performScan, final ExecutorSe final int numParallelTasks) { try { return executorService.submit(new Scanner(performScan, scanSpec, executorService, numParallelTasks, - /* scanResultProcessor = */ null, /* failureHandler = */ null, topLevelLog)); + /* 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). @@ -1728,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 67fa18e2a..7067119e5 100644 --- a/src/main/java/io/github/classgraph/ClassGraphClassLoader.java +++ b/src/main/java/io/github/classgraph/ClassGraphClassLoader.java @@ -32,8 +32,8 @@ import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; -import java.nio.ByteBuffer; import java.security.ProtectionDomain; +import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.LinkedHashSet; @@ -89,8 +89,7 @@ public class ClassGraphClassLoader extends ClassLoader { // 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 context classloader of the class - // that called ClassGraph) + // Try the null classloader first (this will default to the bootstrap class loader) environmentClassLoaderDelegationOrder = new LinkedHashSet<>(); environmentClassLoaderDelegationOrder.add(null); @@ -98,9 +97,7 @@ public class ClassGraphClassLoader extends ClassLoader { final ClassLoader[] envClassLoaderOrder = scanResult.getClassLoaderOrderRespectingParentDelegation(); if (envClassLoaderOrder != null) { // Try environment classloaders - for (final ClassLoader envClassLoader : envClassLoaderOrder) { - environmentClassLoaderDelegationOrder.add(envClassLoader); - } + environmentClassLoaderDelegationOrder.addAll(Arrays.asList(envClassLoaderOrder)); } } @@ -264,25 +261,18 @@ protected Class findClass(final String 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 - try { - final ByteBuffer resourceByteBuffer = resource.read(); - // 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, resourceByteBuffer, (ProtectionDomain) null); - } finally { - resource.close(); - } + // 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); } catch (final LinkageError e) { if (linkageError == null) { linkageError = e; } - } finally { - resource.close(); } } } @@ -350,7 +340,7 @@ public URL getResource(final String path) { } } - // Finally if the above attempts fail, try retrieving resource from ScanResult. + // 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()) { @@ -387,11 +377,11 @@ public Enumeration getResources(final String path) throws IOException { } } - // Finally if the above attempts fail, try retrieving resource from ScanResult. + // 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 Collections. emptyEnumeration(); + return Collections.emptyEnumeration(); } else { return new Enumeration() { /** The idx. */ @@ -437,7 +427,7 @@ public InputStream getResourceAsStream(final String path) { } } - // Finally if the above attempts fail, try opening resource from ScanResult. + // 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()) { diff --git a/src/main/java/io/github/classgraph/ClassInfo.java b/src/main/java/io/github/classgraph/ClassInfo.java index 271c5aa7c..dfc691a2d 100644 --- a/src/main/java/io/github/classgraph/ClassInfo.java +++ b/src/main/java/io/github/classgraph/ClassInfo.java @@ -29,6 +29,7 @@ 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; @@ -50,12 +51,15 @@ 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. */ @@ -88,6 +92,12 @@ public class ClassInfo extends ScanResultObject implements Comparable /** 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; @@ -135,7 +145,7 @@ public class ClassInfo extends ScanResultObject implements Comparable AnnotationParameterValueList annotationDefaultParamValues; /** The type annotation decorators for the {@link ClassTypeSignature} instance. */ - List typeAnnotationDecorators; + transient List typeAnnotationDecorators; /** * Names of classes referenced by this class in class refs and type signatures in the constant pool of the @@ -164,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. */ @@ -450,6 +471,16 @@ void setIsRecord(final boolean isRecord) { } } + /** + * Set source file. + * + * @param sourceFile + * the source file + */ + void setSourceFile(final String sourceFile) { + this.sourceFile = sourceFile; + } + /** * Add {@link ClassTypeAnnotationDecorator} instances. * @@ -628,8 +659,7 @@ void addMethodInfo(final MethodInfoList methodInfoList, final Map filterClassInfo(final Collection classes, final ScanSpec scanSpec, final boolean strictAccept, final ClassType... classTypes) { if (classes == null) { - return Collections. emptySet(); + return Collections.emptySet(); } boolean includeAllTypes = classTypes.length == 0; boolean includeStandardClasses = false; @@ -1186,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(); } /** @@ -1195,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); } /** @@ -1213,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); } /** @@ -1291,6 +1348,17 @@ 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()); + } + /** * Checks if this class extends the named superclass. * @@ -1348,6 +1416,18 @@ public boolean isImplementedInterface() { 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()); + } + /** * Checks whether this class implements the named interface. * @@ -1359,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. * @@ -1389,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; } @@ -1397,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. * @@ -1413,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. * @@ -1421,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; } @@ -1430,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); @@ -1448,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; } @@ -1456,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. * @@ -1472,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. @@ -1482,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; } @@ -1490,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. * @@ -1506,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. * @@ -1514,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; } @@ -1525,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 @@ -1533,32 +1698,106 @@ 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 @@ -1674,8 +1913,9 @@ public ClassInfoList getInterfaces() { .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); } /** @@ -1685,9 +1925,6 @@ 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, /* strictAccept = */ !isExternalClass); @@ -1718,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; + } - // 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<>(); + 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, /* 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; } } @@ -1833,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 @@ -1862,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 @@ -1896,14 +2194,16 @@ public AnnotationParameterValueList getAnnotationDefaultParameterValues() { 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; } /** @@ -1917,9 +2217,6 @@ 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 @@ -2034,12 +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 method has not been overridden by method of same name and type descriptor if (nameAndTypeDescriptorSet.add(new SimpleEntry<>(mi.getName(), mi.getTypeDescriptorStr()))) { - // Add method/constructor to output order + // Add method to output order methodInfoList.add(mi); } } @@ -2487,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())) { @@ -2499,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: * @@ -2567,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; @@ -2632,21 +2973,23 @@ ClassInfoList getClassesWithFieldAnnotationDirectOnly() { * classfile). */ public ClassTypeSignature getTypeSignature() { - 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); + 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); } - } catch (final ParseException e) { - throw new IllegalArgumentException("Invalid type signature for class " + getName() - + " in classpath element " + getClasspathElementURI() + " : " + typeSignatureStr, e); } } return typeSignature; @@ -2662,6 +3005,62 @@ 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; + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -2691,7 +3090,7 @@ public URI getClasspathElementURI() { public URL getClasspathElementURL() { try { return getClasspathElementURI().toURL(); - } catch (final MalformedURLException e) { + } catch (final IllegalArgumentException | MalformedURLException e) { throw new IllegalArgumentException("Could not get classpath element URL", e); } } @@ -3037,9 +3436,11 @@ public int hashCode() { */ @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) { + if (buf.length() > 0 && buf.charAt(buf.length() - 1) != ' ' + && buf.charAt(buf.length() - 1) != '(') { buf.append(' '); } annotation.toString(useSimpleNames, buf); @@ -3059,15 +3460,32 @@ protected void toString(final boolean useSimpleNames, final StringBuilder buf) { } else { // Non-generic classes TypeUtils.modifiersToString(modifiers, ModifierType.CLASS, /* 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(isRecord() ? "record " // - : isEnum() ? "enum " // - : isAnnotation() ? "@interface " // - : isInterface() ? "interface " // - : "class "); + // 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); + } + buf.append(')'); + } final ClassInfo superclass = getSuperclass(); if (superclass != null && !superclass.getName().equals("java.lang.Object")) { buf.append(" extends "); diff --git a/src/main/java/io/github/classgraph/ClassInfoList.java b/src/main/java/io/github/classgraph/ClassInfoList.java index 5045baa5a..794af1659 100644 --- a/src/main/java/io/github/classgraph/ClassInfoList.java +++ b/src/main/java/io/github/classgraph/ClassInfoList.java @@ -170,7 +170,7 @@ public ClassInfoList(final int sizeHint) { public ClassInfoList(final Collection classInfoCollection) { this(classInfoCollection instanceof Set // ? (Set) classInfoCollection - : new HashSet(classInfoCollection), // + : new HashSet<>(classInfoCollection), // /* directlyRelatedClasses = */ null, /* sortByName = */ true); } 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 881f6c347..081d36f45 100644 --- a/src/main/java/io/github/classgraph/ClassRefTypeSignature.java +++ b/src/main/java/io/github/classgraph/ClassRefTypeSignature.java @@ -171,14 +171,20 @@ protected void addTypeAnnotation(final List typePath, final Annota // Find how many deeper nested levels to descend to int numDeeperNestedLevels = 0; int nextTypeArgIdx = -1; - for (int i = 0; i < typePath.size(); i++) { - final TypePathNode typePathNode = typePath.get(i); + 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); } } @@ -231,9 +237,13 @@ protected void addTypeAnnotation(final List typePath, final Annota // 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( - typePath.subList(numDeeperNestedLevels + 1, typePath.size()), annotationInfo); + typeArgumentList.get(nextTypeArgIdx).addTypeAnnotation(remainingTypePath, annotationInfo); } } } @@ -409,19 +419,13 @@ protected void toStringInternal(final boolean useSimpleNames, final AnnotationIn // Append suffixes if (!suffixes.isEmpty()) { for (int i = useSimpleNames ? suffixes.size() - 1 : 0; i < suffixes.size(); i++) { - final AnnotationInfoList typeAnnotations = suffixTypeAnnotations == null ? null - : suffixTypeAnnotations.get(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; - // however, use '$' if the class name is numerical, i.e. "...$1" for anonymous - // inner classes. - if (Character.isDigit(suffixes.get(i).charAt(0))) { - buf.append('$'); - } else { - buf.append('.'); - } + // 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); @@ -463,7 +467,7 @@ static ClassRefTypeSignature parse(final Parser parser, final String definingCla if (parser.peek() == 'L') { parser.next(); final int startParserPosition = parser.getPosition(); - if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ true)) { + if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ true, /* stopAtDot = */ true)) { throw new ParseException(parser, "Could not parse identifier token"); } String className = parser.currToken(); @@ -476,7 +480,8 @@ static ClassRefTypeSignature parse(final Parser parser, final String definingCla suffixTypeArguments = new ArrayList<>(); while (parser.peek() == '.' || parser.peek() == '$') { parser.advance(1); - if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ true)) { + 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()); diff --git a/src/main/java/io/github/classgraph/ClassTypeSignature.java b/src/main/java/io/github/classgraph/ClassTypeSignature.java index 89a150bc5..1bfdd8f0c 100644 --- a/src/main/java/io/github/classgraph/ClassTypeSignature.java +++ b/src/main/java/io/github/classgraph/ClassTypeSignature.java @@ -33,6 +33,7 @@ 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; @@ -91,6 +92,46 @@ private ClassTypeSignature(final ClassInfo classInfo, final List 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; + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -190,8 +231,10 @@ protected void findReferencedClassNames(final Set refdClassNames) { if (superclassSignature != null) { superclassSignature.findReferencedClassNames(refdClassNames); } - for (final ClassRefTypeSignature typeSignature : superinterfaceSignatures) { - typeSignature.findReferencedClassNames(refdClassNames); + if (superinterfaceSignatures != null) { + for (final ClassRefTypeSignature typeSignature : superinterfaceSignatures) { + typeSignature.findReferencedClassNames(refdClassNames); + } } if (throwsSignatures != null) { for (final ClassRefOrTypeVariableSignature typeSignature : throwsSignatures) { @@ -227,8 +270,8 @@ protected void findReferencedClassInfo(final Map classNameToC */ @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) @@ -242,9 +285,9 @@ public boolean equals(final Object obj) { 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); } // ------------------------------------------------------------------------------------------------------------- @@ -275,7 +318,7 @@ void toStringInternal(final String className, final boolean useSimpleNames, fina if (buf.length() > 0) { buf.append(' '); } - buf.append("@throws(" + throwsSignature + ")"); + buf.append("@throws(").append(throwsSignature).append(")"); } } if (modifiers != 0) { @@ -312,7 +355,7 @@ void toStringInternal(final String className, final boolean useSimpleNames, fina buf.append(superSig); } } - if (!superinterfaceSignatures.isEmpty()) { + if (superinterfaceSignatures != null && !superinterfaceSignatures.isEmpty()) { buf.append(isInterface ? " extends " : " implements "); for (int i = 0; i < superinterfaceSignatures.size(); i++) { if (i > 0) { @@ -415,8 +458,7 @@ static ClassTypeSignature parse(final String typeDescriptor, final ClassInfo cla if (parser.hasMore()) { throw new ParseException(parser, "Extra characters at end of type descriptor"); } - final ClassTypeSignature classTypeSignature = new ClassTypeSignature(classInfo, typeParameters, - superclassSignature, superinterfaceSignatures, throwsSignatures); - return classTypeSignature; + 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 e24b8f5f5..ef16f3029 100644 --- a/src/main/java/io/github/classgraph/Classfile.java +++ b/src/main/java/io/github/classgraph/Classfile.java @@ -54,6 +54,9 @@ * 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 {@link ClassfileReader} for the current classfile. */ @@ -128,6 +131,9 @@ class Classfile { /** The type signature. */ 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; @@ -423,6 +429,11 @@ private void extendScanningUpwards(final LogNode log) { } } } + if (methodInfo.getThrownExceptionNames() != null) { + for (final String thrownExceptionName : methodInfo.getThrownExceptionNames()) { + scheduleScanningIfExternalClass(thrownExceptionName, "method throws", log); + } + } } } // Check field annotations @@ -479,6 +490,7 @@ void link(final Map classNameToClassInfo, classInfo.setIsInterface(isInterface); classInfo.setIsAnnotation(isAnnotation); classInfo.setIsRecord(isRecord); + classInfo.setSourceFile(sourceFile); if (superclassName != null) { classInfo.addSuperclass(superclassName, classNameToClassInfo); } @@ -523,7 +535,7 @@ void link(final Map classNameToClassInfo, if (!isModuleDescriptor) { // Get package for this class or package descriptor final String packageName = PackageInfo.getParentPackageName(className); - packageInfo = PackageInfo.getOrCreatePackage(packageName, packageNameToPackageInfo); + packageInfo = PackageInfo.getOrCreatePackage(packageName, packageNameToPackageInfo, scanSpec); if (isPackageDescriptor) { // Add any class annotations on the package-info.class file to the ModuleInfo packageInfo.addAnnotations(classAnnotations); @@ -821,24 +833,6 @@ private boolean constantPoolStringEquals(final int cpIdx, final String asciiStr) // ------------------------------------------------------------------------------------------------------------- - /** - * 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 reader.readUnsignedShort(entryOffset[cpIdx]); - } - /** * Read an int from the constant pool. * @@ -928,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"); } @@ -982,7 +976,7 @@ private Object readAnnotationElementValue() throws IOException { case 'J': return cpReadLong(reader.readUnsignedShort()); case 'S': - return (short) cpReadUnsignedShort(reader.readUnsignedShort()); + return (short) cpReadInt(reader.readUnsignedShort()); case 'Z': return cpReadInt(reader.readUnsignedShort()) != 0; case 's': @@ -1018,15 +1012,15 @@ private Object readAnnotationElementValue() throws IOException { // ------------------------------------------------------------------------------------------------------------- - static interface ClassTypeAnnotationDecorator { + interface ClassTypeAnnotationDecorator { void decorate(ClassTypeSignature classTypeSignature); } - static interface MethodTypeAnnotationDecorator { + interface MethodTypeAnnotationDecorator { void decorate(MethodTypeSignature methodTypeSignature); } - static interface TypeAnnotationDecorator { + interface TypeAnnotationDecorator { void decorate(TypeSignature typeSignature); } @@ -1065,16 +1059,18 @@ private List readTypePath() throws IOException { /** * 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 @@ -1097,13 +1093,14 @@ private void readConstantPoolEntries() throws IOException { 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 = 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 reader.skip(4); @@ -1145,12 +1142,16 @@ private void readConstantPoolEntries() throws IOException { } indirectStringRefs[i] = (nameRef << 16) | typeRef; break; + // There is no constant pool tag type 13 or 14 case 15: // method handle reader.skip(3); break; case 16: // method type reader.skip(2); break; + case 17: // dynamic + reader.skip(4); + break; case 18: // invoke dynamic reader.skip(4); break; @@ -1203,21 +1204,29 @@ private void readConstantPoolEntries() throws IOException { 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); + } } } } @@ -1458,10 +1467,13 @@ private void readMethods() throws IOException, ClassfileFormatException { } 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++) { @@ -1537,35 +1549,66 @@ private void readMethods() throws IOException, ClassfileFormatException { 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) { - formalParameterIndex = reader.readUnsignedByte(); + // Type in formal parameter declaration of method, constructor, + // or lambda expression typeParameterIndex = -1; boundIndex = -1; + formalParameterIndex = reader.readUnsignedByte(); throwsTypeIndex = -1; } else if (targetType == 0x17) { - throwsTypeIndex = reader.readUnsignedShort(); + // 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" @@ -1627,9 +1670,18 @@ public void decorate(final MethodTypeSignature methodTypeSignature) { methodTypeSignature.addRecieverTypeAnnotation(annotationInfo); } else if (targetType == 0x16) { // Type in formal parameter declaration of method, constructor, - // or lambda expression + // or lambda expression. // N.B. formal parameter indices are dodgy, because not all compilers - // index parameters the same way -- so be robust here + // 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()) { @@ -1672,9 +1724,36 @@ public void decorate(final MethodTypeSignature methodTypeSignature) { 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; - reader.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 { reader.skip(attributeLength); } @@ -1686,8 +1765,8 @@ public void decorate(final MethodTypeSignature methodTypeSignature) { } methodInfoList.add(new MethodInfo(className, methodName, methodAnnotationInfo, methodModifierFlags, methodTypeDescriptor, methodTypeSignatureStr, methodParameterNames, - methodParameterModifiers, methodParameterAnnotations, methodHasBody, - methodTypeAnnotationDecorators)); + methodParameterModifiers, methodParameterAnnotations, methodHasBody, minLineNum, + maxLineNum, methodTypeAnnotationDecorators, thrownExceptionNames)); } } } @@ -1735,14 +1814,19 @@ private void readClassAttributes() throws IOException, ClassfileFormatException 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; @@ -1767,6 +1851,9 @@ public void decorate(final ClassTypeSignature classTypeSignature) { 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, @@ -1842,6 +1929,8 @@ public void decorate(final ClassTypeSignature classTypeSignature) { } else if (constantPoolStringEquals(attributeNameCpIdx, "Signature")) { // Get class type signature, including type variables typeSignatureStr = getConstantPoolString(reader.readUnsignedShort()); + } else if (constantPoolStringEquals(attributeNameCpIdx, "SourceFile")) { + sourceFile = getConstantPoolString(reader.readUnsignedShort()); } else if (constantPoolStringEquals(attributeNameCpIdx, "EnclosingMethod")) { final String innermostEnclosingClassName = getConstantPoolClassName(reader.readUnsignedShort()); final int enclosingMethodCpIdx = reader.readUnsignedShort(); @@ -1930,9 +2019,9 @@ public void decorate(final ClassTypeSignature classTypeSignature) { this.stringInternMap = stringInternMap; this.scanSpec = scanSpec; - try { - // Open a BufferedSequentialReader for the classfile - reader = classfileResource.openClassfile(); + // Open a BufferedSequentialReader for the classfile + try (ClassfileReader classfileReader = classfileResource.openClassfile()) { + reader = classfileReader; // Check magic number if (reader.readInt() != 0xCAFEBABE) { @@ -1944,7 +2033,7 @@ public void decorate(final ClassTypeSignature classTypeSignature) { majorVersion = reader.readUnsignedShort(); // Read the constant pool - readConstantPoolEntries(); + readConstantPoolEntries(log); // Read basic class info ( readBasicClassInfo(); @@ -1961,9 +2050,6 @@ public void decorate(final ClassTypeSignature classTypeSignature) { // Read class attributes readClassAttributes(); - } finally { - // Close BufferedSequentialReader - classfileResource.close(); reader = null; } diff --git a/src/main/java/io/github/classgraph/ClasspathElement.java b/src/main/java/io/github/classgraph/ClasspathElement.java index 69cdd00d8..84fe51126 100644 --- a/src/main/java/io/github/classgraph/ClasspathElement.java +++ b/src/main/java/io/github/classgraph/ClasspathElement.java @@ -31,10 +31,9 @@ import java.io.File; import java.net.URI; import java.util.ArrayList; +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; @@ -49,7 +48,7 @@ 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; @@ -72,16 +71,18 @@ abstract class ClasspathElement { boolean containsSpecificallyAcceptedClasspathElementResourcePath; /** - * 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). + * 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 Queue> childClasspathElementsIndexed = new ConcurrentLinkedQueue<>(); + final int classpathElementIdxWithinParent; /** - * The child classpath elements, ordered by order within the parent classpath element. + * 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). */ - List childClasspathElementsOrdered; + Collection childClasspathElements = new ConcurrentLinkedQueue<>(); /** * Resources found within this classpath element that were accepted and not rejected. (Only written by one @@ -104,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. @@ -113,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. * @@ -157,16 +181,16 @@ int getNumClassfileMatches() { * the relative path * @param log * the log + * @return true if path should be scanned */ - protected void checkResourcePathAcceptReject(final String relativePath, final LogNode log) { + 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 rejected classpath element resource path, stopping scanning: " + relativePath); } - skipClasspathElement = true; - return; + return false; } if (scanSpec.classpathElementResourcePathAcceptReject.isSpecificallyAccepted(relativePath)) { if (log != null) { @@ -175,6 +199,7 @@ protected void checkResourcePathAcceptReject(final String relativePath, final Lo containsSpecificallyAcceptedClasspathElementResourcePath = true; } } + return true; } // ------------------------------------------------------------------------------------------------------------- @@ -335,6 +360,23 @@ protected LogNode log(final int classpathElementIdx, final String msg, final 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); + } + // ------------------------------------------------------------------------------------------------------------- /** diff --git a/src/main/java/io/github/classgraph/ClasspathElementPathDir.java b/src/main/java/io/github/classgraph/ClasspathElementDir.java similarity index 74% rename from src/main/java/io/github/classgraph/ClasspathElementPathDir.java rename to src/main/java/io/github/classgraph/ClasspathElementDir.java index c90f4ca87..1468542e6 100644 --- a/src/main/java/io/github/classgraph/ClasspathElementPathDir.java +++ b/src/main/java/io/github/classgraph/ClasspathElementDir.java @@ -37,11 +37,13 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; 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; @@ -49,7 +51,6 @@ import io.github.classgraph.Scanner.ClasspathEntryWorkUnit; import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandlerRegistry; -import nonapi.io.github.classgraph.classpath.ClasspathOrder.ClasspathElementAndClassLoader; import nonapi.io.github.classgraph.concurrency.WorkQueue; import nonapi.io.github.classgraph.fastzipfilereader.LogicalZipFile; import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; @@ -63,13 +64,10 @@ import nonapi.io.github.classgraph.utils.VersionFinder; /** A directory classpath element, using the {@link Path} API. */ -class ClasspathElementPathDir extends ClasspathElement { +class ClasspathElementDir extends ClasspathElement { /** The directory at the root of the classpath element. */ private final Path classpathEltPath; - /** The package root. */ - private final Path packageRootPath; - /** Used to ensure that recursive scanning doesn't get into an infinite loop due to a link cycle. */ private final Set scannedCanonicalPaths = new HashSet<>(); @@ -79,20 +77,17 @@ class ClasspathElementPathDir extends ClasspathElement { /** * A directory classpath element. * - * @param classpathEltPath - * the classpath element {@link Path} - * @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 */ - ClasspathElementPathDir(final Path classpathEltPath, final String packageRoot, final ClassLoader classLoader, - final NestedJarHandler nestedJarHandler, final ScanSpec scanSpec) { - super(classLoader, scanSpec); - this.classpathEltPath = classpathEltPath; - this.packageRootPath = classpathEltPath.resolve(packageRoot); + ClasspathElementDir(final ClasspathEntryWorkUnit workUnit, final NestedJarHandler nestedJarHandler, + final ScanSpec scanSpec) { + super(workUnit, scanSpec); + this.classpathEltPath = (Path) workUnit.classpathEntryObj; this.nestedJarHandler = nestedJarHandler; } @@ -117,36 +112,40 @@ void open(final WorkQueue workQueue, final LogNode log) 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)) { - for (final Path filePath : stream) { - if (Files.isRegularFile(filePath) && filePath.getFileName().endsWith(".jar")) { - if (log != null) { - log(classpathElementIdx, "Found lib jar: " + filePath, log); + 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( - new ClasspathElementAndClassLoader(filePath, 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 } } } - // Only look for package roots if the package root is the root of the classpath element - if (packageRootPath.equals(classpathEltPath)) { + // 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( - new ClasspathElementAndClassLoader(classpathEltPath, packageRootPrefix, - classLoader), + workQueue.addWorkUnit(new ClasspathEntryWorkUnit(packageRoot, getClassLoader(), /* parentClasspathElement = */ this, - /* orderWithinParentClasspathElement = */ childClasspathEntryIdx++)); + /* orderWithinParentClasspathElement = */ childClasspathEntryIdx++, + packageRootPrefix)); } } } @@ -156,7 +155,6 @@ void open(final WorkQueue workQueue, final LogNode log) "Skipping classpath element, since dir cannot be accessed: " + classpathEltPath, log); } skipClasspathElement = true; - return; } } @@ -165,35 +163,31 @@ void open(final WorkQueue workQueue, final LogNode log) * * @param resourcePath * the {@link Path} for the resource - * @param nestedJarHandler - * the nested jar handler * @return the resource */ - private Resource newResource(final Path resourcePath, final NestedJarHandler nestedJarHandler) { - long length; - try { - length = Files.size(resourcePath); - } catch (IOException | SecurityException e) { - length = -1L; - } - return new Resource(this, length) { + 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; /** True if the resource is open. */ - protected AtomicBoolean isOpen = new AtomicBoolean(); + private final AtomicBoolean isOpen = new AtomicBoolean(); @Override - public String getPath() { - String path = FastPathResolver.resolve(packageRootPath.relativize(resourcePath).toString()); - while (path.startsWith("/")) { - path = path.substring(1); + public long getLength() { + if (length == notYetLoadedLength) { + try { + length = Files.size(resourcePath); + } catch (IOException | SecurityException e) { + length = -1; + } } - return path; + return length; } @Override - public String getPathRelativeToClasspathElement() { + public String getPath() { String path = FastPathResolver.resolve(classpathEltPath.relativize(resourcePath).toString()); while (path.startsWith("/")) { path = path.substring(1); @@ -201,10 +195,16 @@ public String getPathRelativeToClasspathElement() { return path; } + @Override + public String getPathRelativeToClasspathElement() { + return packageRootPrefix.isEmpty() ? getPath() : packageRootPrefix + getPath(); + } + @Override public long getLastModified() { try { - return resourcePath.toFile().lastModified(); + return attributes == null ? resourcePath.toFile().lastModified() + : attributes.lastModifiedTime().toMillis(); } catch (final UnsupportedOperationException e) { return 0L; } @@ -215,83 +215,65 @@ public long getLastModified() { public Set getPosixFilePermissions() { Set posixFilePermissions = null; try { - posixFilePermissions = Files.readAttributes(resourcePath, PosixFileAttributes.class) - .permissions(); + if (attributes instanceof PosixFileAttributes) { + posixFilePermissions = ((PosixFileAttributes) attributes).permissions(); + } else { + posixFilePermissions = Files.readAttributes(resourcePath, PosixFileAttributes.class) + .permissions(); + } } catch (UnsupportedOperationException | IOException | SecurityException e) { // POSIX attributes not supported } return posixFilePermissions; } - @Override - public ByteBuffer read() throws IOException { + protected void checkCanOpen() { if (skipClasspathElement) { // Shouldn't happen - throw new IOException("Parent directory could not be opened"); + throw new IllegalStateException("Classpath element could not be opened"); } if (isOpen.getAndSet(true)) { - throw new IOException( + throw new IllegalStateException( "Resource is already open -- cannot open it again without first calling close()"); } - pathSlice = new PathSlice(resourcePath, nestedJarHandler); - length = pathSlice.sliceLength; + if (scanResult != null && scanResult.isClosed()) { + throw new IllegalStateException("Cannot open a resource after the ScanResult is closed"); + } + } + + @Override + public ByteBuffer read() throws IOException { + openAndCreateSlice(); byteBuffer = pathSlice.read(); return byteBuffer; } @Override ClassfileReader openClassfile() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Parent directory could not be opened"); - } - if (isOpen.getAndSet(true)) { - throw new IOException( - "Resource is already open -- cannot open it again without first calling close()"); - } // Classfile won't be compressed, so wrap it in a new PathSlice and then open it - pathSlice = new PathSlice(resourcePath, nestedJarHandler); - length = pathSlice.sliceLength; - return new ClassfileReader(pathSlice); + openAndCreateSlice(); + return new ClassfileReader(pathSlice, this); } @Override public InputStream open() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Parent directory could not be opened"); - } - if (isOpen.getAndSet(true)) { - throw new IOException( - "Resource is already open -- cannot open it again without first calling close()"); - } - pathSlice = new PathSlice(resourcePath, nestedJarHandler); - inputStream = pathSlice.open(new Runnable() { - @Override - public void run() { - if (isOpen.getAndSet(false)) { - close(); - } - } - }); - length = pathSlice.sliceLength; + openAndCreateSlice(); + inputStream = pathSlice.open(this); return inputStream; } @Override public byte[] load() throws IOException { - read(); - try (Resource res = this) { // Close this after use - pathSlice = new PathSlice(resourcePath, nestedJarHandler); - final byte[] bytes = pathSlice.load(); - length = bytes.length; - return bytes; + try { + openAndCreateSlice(); + return pathSlice.load(); + } finally { + close(); } } @Override public void close() { - super.close(); // Close inputStream if (isOpen.getAndSet(false)) { if (byteBuffer != null) { // Any ByteBuffer ref should be a duplicate, so it doesn't need to be cleaned @@ -302,8 +284,17 @@ public void close() { nestedJarHandler.markSliceAsClosed(pathSlice); pathSlice = null; } + + // Close inputStream + super.close(); } } + + private void openAndCreateSlice() throws IOException { + checkCanOpen(); + pathSlice = new PathSlice(resourcePath, false, 0L, nestedJarHandler, false); + length = pathSlice.sliceLength; + } }; } @@ -317,8 +308,8 @@ public void close() { */ @Override Resource getResource(final String relativePath) { - final Path resourcePath = packageRootPath.resolve(relativePath); - return FileUtils.canReadAndIsFile(resourcePath) ? newResource(resourcePath, nestedJarHandler) : null; + final Path resourcePath = classpathEltPath.resolve(relativePath); + return FileUtils.canReadAndIsFile(resourcePath) ? newResource(resourcePath, null) : null; } /** @@ -330,9 +321,6 @@ Resource getResource(final String relativePath) { * the log */ private void scanPathRecursively(final Path path, final LogNode log) { - if (skipClasspathElement) { - return; - } // See if this canonical path has been scanned before, so that recursive scanning doesn't get stuck in an // infinite loop due to symlinks Path canonicalPath; @@ -351,7 +339,7 @@ private void scanPathRecursively(final Path path, final LogNode log) { return; } - String dirRelativePathStr = FastPathResolver.resolve(packageRootPath.relativize(path).toString()); + String dirRelativePathStr = FastPathResolver.resolve(classpathEltPath.relativize(path).toString()); while (dirRelativePathStr.startsWith("/")) { dirRelativePathStr = dirRelativePathStr.substring(1); } @@ -371,7 +359,8 @@ private void scanPathRecursively(final Path path, final LogNode log) { // 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 (dirRelativePathStr.startsWith(LogicalZipFile.MULTI_RELEASE_PATH_PREFIX)) { + 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); @@ -380,8 +369,7 @@ private void scanPathRecursively(final Path path, final LogNode log) { } // Accept/reject classpath elements based on dir resource paths - checkResourcePathAcceptReject(dirRelativePathStr, log); - if (skipClasspathElement) { + if (!checkResourcePathAcceptReject(dirRelativePathStr, log)) { return; } @@ -414,10 +402,10 @@ private void scanPathRecursively(final Path path, final LogNode log) { if (log != null) { log.log("Could not read directory " + path + " : " + e.getMessage()); } - skipClasspathElement = true; return; } 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; @@ -425,9 +413,13 @@ private void scanPathRecursively(final Path path, final LogNode log) { // 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 Path subPath : pathsInDir) { + final Iterator pathsIterator = pathsInDir.iterator(); + while (pathsIterator.hasNext()) { + final Path subPath = pathsIterator.next(); // Process files in dir before recursing - if (Files.isRegularFile(subPath)) { + 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 @@ -438,8 +430,7 @@ private void scanPathRecursively(final Path path, final LogNode log) { } // Accept/reject classpath elements based on file resource paths - checkResourcePathAcceptReject(subPathRelativeStr, subLog); - if (skipClasspathElement) { + if (!checkResourcePathAcceptReject(subPathRelativeStr, subLog)) { return; } @@ -449,12 +440,12 @@ private void scanPathRecursively(final Path path, final LogNode log) { || (parentMatchStatus == ScanSpecPathMatch.AT_ACCEPTED_CLASS_PACKAGE && scanSpec.classfileIsSpecificallyAccepted(subPathRelativeStr))) { // Resource is accepted - final Resource resource = newResource(subPath, nestedJarHandler); + final Resource resource = newResource(subPath, fileAttributes); addAcceptedResource(resource, parentMatchStatus, /* isClassfileOnly = */ false, subLog); // Save last modified time try { - fileToLastModified.put(subPath.toFile(), subPath.toFile().lastModified()); + fileToLastModified.put(subPath.toFile(), fileAttributes.lastModifiedTime().toMillis()); } catch (final UnsupportedOperationException e) { // Ignore } @@ -467,31 +458,30 @@ private void scanPathRecursively(final Path path, final LogNode log) { } } else if (scanSpec.enableClassInfo && dirRelativePathStr.equals("/")) { // Always check for module descriptor in package root, even if package root isn't in accept - for (final Path subPath : pathsInDir) { - if (subPath.getFileName().toString().equals("module-info.class") && Files.isRegularFile(subPath)) { - final Resource resource = newResource(subPath, nestedJarHandler); - addAcceptedResource(resource, parentMatchStatus, /* isClassfileOnly = */ true, subLog); - try { - fileToLastModified.put(subPath.toFile(), subPath.toFile().lastModified()); - } catch (final UnsupportedOperationException e) { - // Ignore + 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; } - break; } } } // Recurse into subdirectories for (final Path subPath : pathsInDir) { try { - if (Files.isDirectory(subPath)) { + if (getFileAttributes.get(subPath).isDirectory()) { scanPathRecursively(subPath, subLog); - // If a rejected classpath element resource path was found, it will set skipClasspathElement - if (skipClasspathElement) { - if (subLog != null) { - subLog.addElapsedTime(); - } - return; - } } } catch (final SecurityException e) { if (subLog != null) { @@ -521,18 +511,21 @@ private void scanPathRecursively(final Path path, 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(classpathElementIdx, "Scanning Path classpath element " + getURI(), log); - scanPathRecursively(packageRootPath, subLog); + scanPathRecursively(classpathEltPath, subLog); finishScanPaths(subLog); } @@ -568,7 +561,11 @@ public File getFile() { */ @Override URI getURI() { - return packageRootPath.toUri(); + try { + return classpathEltPath.toUri(); + } catch (IOError | SecurityException e) { + throw new IllegalArgumentException("Could not convert to URI: " + classpathEltPath); + } } @Override @@ -584,9 +581,10 @@ List getAllURIs() { @Override public String toString() { try { - return packageRootPath.toUri().toString(); + // Path.toString() does not include the URI scheme for some reason + return classpathEltPath.toUri().toString(); } catch (IOError | SecurityException e) { - return packageRootPath.toString(); + return classpathEltPath.toString(); } } @@ -595,7 +593,7 @@ public String toString() { */ @Override public int hashCode() { - return Objects.hash(classpathEltPath, packageRootPath); + return Objects.hash(classpathEltPath); } /* (non-Javadoc) @@ -605,11 +603,10 @@ public int hashCode() { public boolean equals(final Object obj) { if (obj == this) { return true; - } else if (!(obj instanceof ClasspathElementPathDir)) { + } else if (!(obj instanceof ClasspathElementDir)) { return false; } - final ClasspathElementPathDir other = (ClasspathElementPathDir) obj; - return Objects.equals(this.classpathEltPath, other.classpathEltPath) - && Objects.equals(this.packageRootPath, other.packageRootPath); + final ClasspathElementDir other = (ClasspathElementDir) obj; + return Objects.equals(this.classpathEltPath, other.classpathEltPath); } } diff --git a/src/main/java/io/github/classgraph/ClasspathElementFileDir.java b/src/main/java/io/github/classgraph/ClasspathElementFileDir.java deleted file mode 100644 index 9e4de125d..000000000 --- a/src/main/java/io/github/classgraph/ClasspathElementFileDir.java +++ /dev/null @@ -1,575 +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 io.github.classgraph; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.attribute.PosixFileAttributes; -import java.nio.file.attribute.PosixFilePermission; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -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.classloaderhandler.ClassLoaderHandlerRegistry; -import nonapi.io.github.classgraph.classpath.ClasspathOrder.ClasspathElementAndClassLoader; -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.FileSlice; -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.LogNode; -import nonapi.io.github.classgraph.utils.VersionFinder; - -/** A directory classpath element, using the {@link File} API. */ -class ClasspathElementFileDir extends ClasspathElement { - /** The directory at the root of the classpath element. */ - private final File classpathEltDir; - - /** The directory at the root of the package hierarchy. */ - private final File packageRootDir; - - /** Used to ensure that recursive scanning doesn't get into an infinite loop due to a link cycle. */ - 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 nestedJarHandler - * the nested jar handler - * @param scanSpec - * the scan spec - */ - ClasspathElementFileDir(final File classpathEltDir, final String packageRootPrefix, - final ClassLoader classLoader, final NestedJarHandler nestedJarHandler, final ScanSpec scanSpec) { - super(classLoader, scanSpec); - this.classpathEltDir = classpathEltDir; - this.packageRootDir = new File(classpathEltDir, packageRootPrefix); - this.nestedJarHandler = nestedJarHandler; - } - - /* (non-Javadoc) - * @see io.github.classgraph.ClasspathElement#open( - * nonapi.io.github.classgraph.concurrency.WorkQueue, nonapi.io.github.classgraph.utils.LogNode) - */ - @Override - void open(final WorkQueue workQueue, final LogNode log) { - if (!scanSpec.scanDirs) { - if (log != null) { - log(classpathElementIdx, - "Skipping classpath element, since dir scanning is disabled: " + classpathEltDir, log); - } - skipClasspathElement = true; - return; - } - try { - // 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 (FileUtils.canReadAndIsDir(libDir)) { - // 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(classpathElementIdx, "Found lib jar: " + file, log); - } - workQueue.addWorkUnit(new ClasspathEntryWorkUnit( - new ClasspathElementAndClassLoader(file.getPath(), classLoader), - /* parentClasspathElement = */ this, - /* orderWithinParentClasspathElement = */ childClasspathEntryIdx++)); - } - } - } - } - } - // Only look for package roots if the package root is the root of the classpath element - if (packageRootDir.equals(classpathEltDir)) { - for (final String packageRootPrefix : ClassLoaderHandlerRegistry.AUTOMATIC_PACKAGE_ROOT_PREFIXES) { - final File packageRoot = new File(classpathEltDir, packageRootPrefix); - if (FileUtils.canReadAndIsDir(packageRoot)) { - if (log != null) { - log(classpathElementIdx, "Found package root: " + packageRoot, log); - } - workQueue - .addWorkUnit(new ClasspathEntryWorkUnit( - new ClasspathElementAndClassLoader(classpathEltDir, packageRootPrefix, - classLoader), - /* parentClasspathElement = */ this, - /* orderWithinParentClasspathElement = */ childClasspathEntryIdx++)); - } - } - } - } catch (final SecurityException e) { - if (log != null) { - log(classpathElementIdx, - "Skipping classpath element, since dir cannot be accessed: " + classpathEltDir, log); - } - skipClasspathElement = true; - return; - } - } - - /** - * Create a new {@link Resource} object for a resource or classfile discovered while scanning paths. - * - * @param pathRelativeToPackageRoot - * the path of the resource relative to the package root - * @param resourceFile - * the {@link File} for the resource - * @param nestedJarHandler - * the nested jar handler - * @return the resource - */ - private Resource newResource(final String pathRelativeToPackageRoot, final File resourceFile, - final NestedJarHandler nestedJarHandler) { - return new Resource(this, resourceFile.length()) { - /** The {@link FileSlice} opened on the file. */ - private FileSlice fileSlice; - - /** True if the resource is open. */ - protected AtomicBoolean isOpen = new AtomicBoolean(); - - @Override - public String getPath() { - String path = FastPathResolver.resolve(pathRelativeToPackageRoot); - while (path.startsWith("/")) { - path = path.substring(1); - } - return path; - } - - @Override - public String getPathRelativeToClasspathElement() { - // Relativize resource file to classpath element dir - final File resourceFile = new File(packageRootDir, pathRelativeToPackageRoot); - String pathRelativeToClasspathElt = FastPathResolver - .resolve(resourceFile.getPath().substring(classpathEltDir.getPath().length())); - while (pathRelativeToClasspathElt.startsWith("/")) { - pathRelativeToClasspathElt = pathRelativeToClasspathElt.substring(1); - } - return pathRelativeToClasspathElt; - } - - @Override - public long getLastModified() { - return resourceFile.lastModified(); - } - - @SuppressWarnings("null") - @Override - public Set getPosixFilePermissions() { - Set posixFilePermissions = null; - try { - posixFilePermissions = Files.readAttributes(resourceFile.toPath(), PosixFileAttributes.class) - .permissions(); - } catch (UnsupportedOperationException | IOException | SecurityException e) { - // POSIX attributes not supported - } - return posixFilePermissions; - } - - @Override - public ByteBuffer read() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Parent directory could not be opened"); - } - if (isOpen.getAndSet(true)) { - throw new IOException( - "Resource is already open -- cannot open it again without first calling close()"); - } - fileSlice = new FileSlice(resourceFile, nestedJarHandler, /* log = */ null); - length = fileSlice.sliceLength; - byteBuffer = fileSlice.read(); - return byteBuffer; - } - - @Override - ClassfileReader openClassfile() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Parent directory could not be opened"); - } - if (isOpen.getAndSet(true)) { - throw new IOException( - "Resource is already open -- cannot open it again without first calling close()"); - } - // Classfile won't be compressed, so wrap it in a new FileSlice and then open it - fileSlice = new FileSlice(resourceFile, nestedJarHandler, /* log = */ null); - length = fileSlice.sliceLength; - return new ClassfileReader(fileSlice); - } - - @Override - public InputStream open() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Parent directory could not be opened"); - } - if (isOpen.getAndSet(true)) { - throw new IOException( - "Resource is already open -- cannot open it again without first calling close()"); - } - fileSlice = new FileSlice(resourceFile, nestedJarHandler, /* log = */ null); - inputStream = fileSlice.open(new Runnable() { - @Override - public void run() { - if (isOpen.getAndSet(false)) { - close(); - } - } - }); - length = fileSlice.sliceLength; - return inputStream; - } - - @Override - public byte[] load() throws IOException { - read(); - try (Resource res = this) { // Close this after use - fileSlice = new FileSlice(resourceFile, nestedJarHandler, /* log = */ null); - final byte[] bytes = fileSlice.load(); - length = bytes.length; - return bytes; - } - } - - @Override - public void close() { - super.close(); // Close inputStream - if (isOpen.getAndSet(false)) { - if (byteBuffer != null) { - // Any ByteBuffer ref should be a duplicate, so it doesn't need to be cleaned - byteBuffer = null; - } - if (fileSlice != null) { - fileSlice.close(); - nestedJarHandler.markSliceAsClosed(fileSlice); - fileSlice = null; - } - } - } - }; - } - - /** - * Get the {@link Resource} for a given relative path. - * - * @param pathRelativeToPackageRoot - * The relative path of the {@link Resource} to return. - * @return The {@link Resource} for the given relative path, or null if relativePath does not exist in this - * classpath element. - */ - @Override - Resource getResource(final String pathRelativeToPackageRoot) { - final File resourceFile = new File(packageRootDir, pathRelativeToPackageRoot); - return FileUtils.canReadAndIsFile(resourceFile) - ? newResource(pathRelativeToPackageRoot, resourceFile, nestedJarHandler) - : null; - } - - /** - * Recursively scan a directory for file path patterns matching the scan spec. - * - * @param dir - * the directory - * @param log - * the log - */ - private void scanDirRecursively(final File dir, final LogNode log) { - if (skipClasspathElement) { - return; - } - // 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; - try { - canonicalPath = dir.getCanonicalPath(); - if (!scannedCanonicalPaths.add(canonicalPath)) { - if (log != null) { - log.log("Reached symlink cycle, stopping recursion: " + dir); - } - return; - } - } catch (final IOException | SecurityException e) { - if (log != null) { - log.log("Could not canonicalize path: " + dir, e); - } - return; - } - - final String dirPath = dir.getPath(); - final int ignorePrefixLen = packageRootDir.getPath().length() + 1; - final String dirRelativePath = ignorePrefixLen > dirPath.length() ? "/" // - : dirPath.substring(ignorePrefixLen).replace(File.separatorChar, '/') + "/"; - final boolean isDefaultPackage = "/".equals(dirRelativePath); - - if (nestedClasspathRootPrefixes != null && nestedClasspathRootPrefixes.contains(dirRelativePath)) { - if (log != null) { - log.log("Reached nested classpath root, stopping recursion to avoid duplicate scanning: " - + dirRelativePath); - } - return; - } - - // 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 (dirRelativePath.startsWith(LogicalZipFile.MULTI_RELEASE_PATH_PREFIX)) { - if (log != null) { - log.log("Found unexpected nested versioned entry in directory classpath element -- skipping: " - + dirRelativePath); - } - return; - } - - // Accept/reject classpath elements based on dir resource paths - checkResourcePathAcceptReject(dirRelativePath, log); - if (skipClasspathElement) { - return; - } - - final ScanSpecPathMatch parentMatchStatus = scanSpec.dirAcceptMatchStatus(dirRelativePath); - if (parentMatchStatus == ScanSpecPathMatch.HAS_REJECTED_PATH_PREFIX) { - // Reached a non-accepted or rejected path -- stop the recursive scan - if (log != null) { - log.log("Reached rejected directory, stopping recursive scan: " + dirRelativePath); - } - return; - } - if (parentMatchStatus == ScanSpecPathMatch.NOT_WITHIN_ACCEPTED_PATH) { - // Reached a non-accepted and non-rejected path -- stop the recursive scan - return; - } - - final LogNode subLog = log == null ? null - // Log dirs after files (addAcceptedResources() precedes log entry with "0:") - : log.log("1:" + canonicalPath, "Scanning directory: " + dir - + (dir.getPath().equals(canonicalPath) ? "" : " ; canonical path: " + canonicalPath)); - - final File[] filesInDir = dir.listFiles(); - if (filesInDir == null) { - if (log != null) { - log.log("Invalid directory " + dir); - } - return; - } - Arrays.sort(filesInDir); - - // 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 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) { - // Process files in dir before recursing - if (fileInDir.isFile()) { - final String fileInDirRelativePath = dirRelativePath.isEmpty() || isDefaultPackage - ? fileInDir.getName() - : dirRelativePath + fileInDir.getName(); - // 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 && fileInDirRelativePath.endsWith(".class") - && !fileInDirRelativePath.equals("module-info.class")) { - continue; - } - - // Accept/reject classpath elements based on file resource paths - checkResourcePathAcceptReject(fileInDirRelativePath, subLog); - if (skipClasspathElement) { - return; - } - - // 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(fileInDirRelativePath))) { - // Resource is accepted - final Resource resource = newResource(fileInDirRelativePath, fileInDir, nestedJarHandler); - addAcceptedResource(resource, parentMatchStatus, /* isClassfileOnly = */ false, subLog); - - // Save last modified time - fileToLastModified.put(fileInDir, fileInDir.lastModified()); - } else { - if (subLog != null) { - subLog.log("Skipping non-accepted file: " + fileInDirRelativePath); - } - } - } - } - } else if (scanSpec.enableClassInfo && dirRelativePath.equals("/")) { - // Always check for module descriptor in package root, even if package root isn't in accept - for (final File fileInDir : filesInDir) { - if (fileInDir.getName().equals("module-info.class") && fileInDir.isFile()) { - final Resource resource = newResource("module-info.class", fileInDir, nestedJarHandler); - addAcceptedResource(resource, parentMatchStatus, /* isClassfileOnly = */ true, subLog); - fileToLastModified.put(fileInDir, fileInDir.lastModified()); - break; - } - } - } - // Recurse into subdirectories - for (final File fileInDir : filesInDir) { - if (fileInDir.isDirectory()) { - scanDirRecursively(fileInDir, subLog); - // If a rejected classpath element resource path was found, it will set skipClasspathElement - if (skipClasspathElement) { - if (subLog != null) { - subLog.addElapsedTime(); - } - return; - } - } - } - - if (subLog != null) { - subLog.addElapsedTime(); - } - - // Save the last modified time of the directory - fileToLastModified.put(dir, dir.lastModified()); - } - - /** - * Hierarchically scan directory structure for classfiles and matching files. - * - * @param log - * the log - */ - @Override - void scanPaths(final LogNode log) { - if (skipClasspathElement) { - return; - } - if (scanned.getAndSet(true)) { - // Should not happen - throw new IllegalArgumentException("Already scanned classpath element " + toString()); - } - - final LogNode subLog = log == null ? null - : log(classpathElementIdx, "Scanning directory classpath element " + packageRootDir, log); - - scanDirRecursively(packageRootDir, subLog); - - finishScanPaths(subLog); - } - - /** - * Get the module name from module descriptor. - * - * @return the module name - */ - @Override - public String getModuleName() { - return moduleNameFromModuleDescriptor == null || moduleNameFromModuleDescriptor.isEmpty() ? null - : moduleNameFromModuleDescriptor; - } - - /** - * Get the directory {@link File}. - * - * @return The classpath element directory as a {@link File}. - */ - @Override - public File getFile() { - return classpathEltDir; - } - - /* (non-Javadoc) - * @see io.github.classgraph.ClasspathElement#getURI() - */ - @Override - URI getURI() { - return packageRootDir.toURI(); - } - - @Override - List getAllURIs() { - return Collections.singletonList(getURI()); - } - - /** - * Return the classpath element directory as a String. - * - * @return the string - */ - @Override - public String toString() { - return packageRootDir.toString(); - } - - /* (non-Javadoc) - * @see java.lang.Object#hashCode() - */ - @Override - public int hashCode() { - return Objects.hash(classpathEltDir, packageRootDir); - } - - /* (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 ClasspathElementFileDir)) { - return false; - } - final ClasspathElementFileDir other = (ClasspathElementFileDir) obj; - return Objects.equals(this.classpathEltDir, other.classpathEltDir) - && Objects.equals(this.packageRootDir, other.packageRootDir); - } -} diff --git a/src/main/java/io/github/classgraph/ClasspathElementModule.java b/src/main/java/io/github/classgraph/ClasspathElementModule.java index 15af59210..734638057 100644 --- a/src/main/java/io/github/classgraph/ClasspathElementModule.java +++ b/src/main/java/io/github/classgraph/ClasspathElementModule.java @@ -42,6 +42,7 @@ import io.github.classgraph.Scanner.ClasspathEntryWorkUnit; 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.LogicalZipFile; @@ -52,12 +53,13 @@ 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. */ /** - * @author luke + * A module classpath element. * + * @author luke */ class ClasspathElementModule extends ClasspathElement { @@ -79,17 +81,18 @@ class ClasspathElementModule extends ClasspathElement { * * @param moduleRef * the module ref - * @param classLoader - * the classloader + * @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, + ClasspathElementModule(final ModuleRef moduleRef, final SingletonMap, IOException> // - moduleRefToModuleReaderProxyRecyclerMap, final ScanSpec scanSpec) { - super(classLoader, scanSpec); + moduleRefToModuleReaderProxyRecyclerMap, final ClasspathEntryWorkUnit workUnit, + final ScanSpec scanSpec) { + super(workUnit, scanSpec); this.moduleRefToModuleReaderProxyRecyclerMap = moduleRefToModuleReaderProxyRecyclerMap; this.moduleRef = moduleRef; } @@ -111,9 +114,10 @@ void open(final WorkQueue workQueueIgnored, final LogNod } try { moduleReaderProxyRecycler = moduleRefToModuleReaderProxyRecyclerMap.get(moduleRef, log); - } catch (final IOException | NullSingletonException e) { + } catch (final IOException | NullSingletonException | NewInstanceException e) { if (log != null) { - log(classpathElementIdx, "Skipping invalid module " + getModuleName() + " : " + e, log); + log(classpathElementIdx, "Skipping invalid module " + getModuleName() + " : " + + (e.getCause() == null ? e : e.getCause()), log); } skipClasspathElement = true; return; @@ -133,18 +137,13 @@ private Resource newResource(final String resourcePath) { private ModuleReaderProxy moduleReaderProxy; /** True if the resource is open. */ - protected AtomicBoolean isOpen = new AtomicBoolean(); + private final AtomicBoolean isOpen = new AtomicBoolean(); @Override public String getPath() { return resourcePath; } - @Override - public String getPathRelativeToClasspathElement() { - return resourcePath; - } - @Override public long getLastModified() { return 0L; // Unknown @@ -155,16 +154,23 @@ public Set getPosixFilePermissions() { return null; // N/A } - @Override - public ByteBuffer read() throws IOException { + 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"); } if (isOpen.getAndSet(true)) { - throw new IOException( + 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: @@ -181,22 +187,43 @@ public ByteBuffer read() throws IOException { @Override ClassfileReader openClassfile() throws IOException { - return new ClassfileReader(open()); + return new ClassfileReader(open(), this); } @Override - public InputStream open() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Module could not be opened"); - } - if (isOpen.getAndSet(true)) { - throw new IOException( - "Resource is already open -- cannot open it again without first calling close()"); + 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); } + } + + @Override + public InputStream open() throws IOException { + checkCanOpen(); try { + final Resource thisResource = this; moduleReaderProxy = moduleReaderProxyRecycler.acquire(); - inputStream = 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; @@ -209,35 +236,40 @@ public InputStream open() throws IOException { @Override public byte[] load() throws IOException { - read(); try (Resource res = this) { // Close this after use + read(); // Fill byteBuffer final byte[] byteArray; - if (byteBuffer.hasArray() && byteBuffer.position() == 0 - && byteBuffer.limit() == byteBuffer.capacity()) { - byteArray = byteBuffer.array(); + if (res.byteBuffer.hasArray() && res.byteBuffer.position() == 0 + && res.byteBuffer.limit() == res.byteBuffer.capacity()) { + byteArray = res.byteBuffer.array(); } else { - byteArray = new byte[byteBuffer.remaining()]; - byteBuffer.get(byteArray); + byteArray = new byte[res.byteBuffer.remaining()]; + res.byteBuffer.get(byteArray); } - length = byteArray.length; + res.length = byteArray.length; return byteArray; } } @Override public void close() { - super.close(); // Close inputStream - if (isOpen.getAndSet(false) && moduleReaderProxy != null) { - if (byteBuffer != null) { - // Release any open ByteBuffer - moduleReaderProxy.release(byteBuffer); + if (isOpen.getAndSet(false)) { + if (moduleReaderProxy != null) { + 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; } - // 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(); } } }; @@ -269,7 +301,7 @@ 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 LogNode subLog = log == null ? null @@ -312,7 +344,8 @@ void scanPaths(final LogNode log) { // 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 (relativePath.startsWith(LogicalZipFile.MULTI_RELEASE_PATH_PREFIX)) { + if (!scanSpec.enableMultiReleaseVersions + && relativePath.startsWith(LogicalZipFile.MULTI_RELEASE_PATH_PREFIX)) { if (subLog != null) { subLog.log( "Found unexpected nested versioned entry in module -- skipping: " + relativePath); @@ -328,9 +361,8 @@ void scanPaths(final LogNode log) { } // Accept/reject classpath elements based on file resource paths - checkResourcePathAcceptReject(relativePath, log); - if (skipClasspathElement) { - return; + if (!checkResourcePathAcceptReject(relativePath, log)) { + continue; } // Get match status of the parent directory of this resource's relative path (or reuse the last diff --git a/src/main/java/io/github/classgraph/ClasspathElementZip.java b/src/main/java/io/github/classgraph/ClasspathElementZip.java index 01eb1394a..83649ad54 100644 --- a/src/main/java/io/github/classgraph/ClasspathElementZip.java +++ b/src/main/java/io/github/classgraph/ClasspathElementZip.java @@ -49,7 +49,7 @@ import io.github.classgraph.Scanner.ClasspathEntryWorkUnit; import nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandlerRegistry; -import nonapi.io.github.classgraph.classpath.ClasspathOrder.ClasspathElementAndClassLoader; +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; @@ -75,8 +75,6 @@ class ClasspathElementZip extends ClasspathElement { 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-rejected zip entries. */ @@ -96,27 +94,26 @@ class ClasspathElementZip extends ClasspathElement { /** * A jarfile classpath element. * - * @param rawPathObj - * the raw path to the jarfile as a {@link String}, possibly including "!"-delimited nested paths, or - * a {@link URL}, {@link URI} ol {@link Path} for the jarfile. - * @param classLoader - * the classloader + * @param workUnit + * the work unit * @param nestedJarHandler * the nested jar handler * @param scanSpec * the scan spec */ - ClasspathElementZip(final Object rawPathObj, final ClassLoader classLoader, - final NestedJarHandler nestedJarHandler, final ScanSpec scanSpec) { - super(classLoader, scanSpec); - // Convert the raw path object (String, URL, URI, or Path) to a string. + 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 e) { + } catch (final IOError | SecurityException e) { // Fall through } } @@ -144,7 +141,7 @@ void open(final WorkQueue workQueue, final LogNode log) } 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.jarAcceptReject.isAcceptedAndNotRejected(outermostZipFilePathResolved)) { if (subLog != null) { @@ -160,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) { @@ -171,7 +170,7 @@ 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 final String packageRoot = logicalZipFileAndPackageRoot.getValue(); @@ -217,11 +216,10 @@ void open(final WorkQueue workQueue, final LogNode log) if (subLog != null) { subLog.log("Found nested lib jar: " + entryPath); } - workQueue.addWorkUnit(new ClasspathEntryWorkUnit( - new ClasspathElementAndClassLoader(entryPath, classLoader), + workQueue.addWorkUnit(new ClasspathEntryWorkUnit(entryPath, getClassLoader(), /* parentClasspathElement = */ this, /* orderWithinParentClasspathElement = */ - childClasspathEntryIdx++)); + childClasspathEntryIdx++, /* packageRootPrefix = */ "")); break; } } @@ -258,12 +256,10 @@ void open(final WorkQueue workQueue, final LogNode log) if (scheduledChildClasspathElements.add(childClassPathEltPathWithPrefix)) { // Schedule child classpath element for scanning workQueue.addWorkUnit( // - new ClasspathEntryWorkUnit( - new ClasspathElementAndClassLoader(childClassPathEltPathWithPrefix, - classLoader), + new ClasspathEntryWorkUnit(childClassPathEltPathWithPrefix, getClassLoader(), /* parentClasspathElement = */ this, /* orderWithinParentClasspathElement = */ - childClasspathEntryIdx++)); + childClasspathEntryIdx++, /* packageRootPrefix = */ "")); } } } @@ -289,11 +285,10 @@ void open(final WorkQueue workQueue, final LogNode log) // Only add child classpath elements once if (scheduledChildClasspathElements.add(childClassPathEltPath)) { // Schedule child classpath element for scanning - workQueue.addWorkUnit(new ClasspathEntryWorkUnit( - new ClasspathElementAndClassLoader(childClassPathEltPath, classLoader), + workQueue.addWorkUnit(new ClasspathEntryWorkUnit(childClassPathEltPath, getClassLoader(), /* parentClasspathElement = */ this, /* orderWithinParentClasspathElement = */ - childClasspathEntryIdx++)); + childClasspathEntryIdx++, /* packageRootPrefix = */ "")); } } } @@ -312,7 +307,7 @@ 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. */ - protected AtomicBoolean isOpen = new AtomicBoolean(); + private final AtomicBoolean isOpen = new AtomicBoolean(); /** * Path with package root prefix and/or any Spring Boot prefix ("BOOT-INF/classes/" or @@ -376,25 +371,30 @@ public Set getPosixFilePermissions() { return perms; } - @Override - public InputStream open() throws IOException { + 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 IOException( + 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 + ClassfileReader openClassfile() throws IOException { + return new ClassfileReader(open(), this); + } + + @Override + public InputStream open() throws IOException { + checkCanOpen(); try { - inputStream = zipEntry.getSlice().open(new Runnable() { - @Override - public void run() { - if (isOpen.getAndSet(false)) { - close(); - } - } - }); + inputStream = zipEntry.getSlice().open(this); length = zipEntry.uncompressedSize; return inputStream; @@ -404,21 +404,9 @@ public void run() { } } - @Override - ClassfileReader openClassfile() throws IOException { - return new ClassfileReader(open()); - } - @Override public ByteBuffer read() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Jarfile could not be opened"); - } - if (isOpen.getAndSet(true)) { - throw new IOException( - "Resource is already open -- cannot open it again without first calling close()"); - } + checkCanOpen(); try { byteBuffer = zipEntry.getSlice().read(); length = byteBuffer.remaining(); @@ -431,28 +419,25 @@ public ByteBuffer read() throws IOException { @Override public byte[] load() throws IOException { - if (skipClasspathElement) { - // Shouldn't happen - throw new IOException("Jarfile could not be opened"); - } - if (isOpen.getAndSet(true)) { - throw new IOException( - "Resource is already open -- cannot open it again without first calling close()"); - } + checkCanOpen(); try (Resource res = this) { // Close this after use final byte[] byteArray = zipEntry.getSlice().load(); - length = byteArray.length; + res.length = byteArray.length; return byteArray; } } @Override public void close() { - super.close(); // Close inputStream - if (isOpen.getAndSet(false) && byteBuffer != null) { - // ByteBuffer should be a duplicate or slice, or should wrap an array, so it doesn't - // need to be unmapped - byteBuffer = null; + 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(); } } }; @@ -482,6 +467,9 @@ void scanPaths(final LogNode log) { if (logicalZipFile == null) { skipClasspathElement = true; } + if (!checkResourcePathAcceptReject(getZipFilePath(), log)) { + skipClasspathElement = true; + } if (skipClasspathElement) { return; } @@ -518,7 +506,8 @@ void scanPaths(final LogNode log) { // 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 (relativePath.startsWith(LogicalZipFile.MULTI_RELEASE_PATH_PREFIX)) { + 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 " @@ -590,9 +579,8 @@ void scanPaths(final LogNode log) { } // Accept/reject classpath elements based on file resource paths - checkResourcePathAcceptReject(relativePath, log); - if (skipClasspathElement) { - return; + if (!checkResourcePathAcceptReject(relativePath, log)) { + continue; } // Get match status of the parent directory of this ZipEntry file's relative path (or reuse the last @@ -723,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); } 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 519100b00..6c0f60860 100644 --- a/src/main/java/io/github/classgraph/FieldInfo.java +++ b/src/main/java/io/github/classgraph/FieldInfo.java @@ -46,22 +46,7 @@ * 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; @@ -72,11 +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 List typeAnnotationDecorators; + private transient List typeAnnotationDecorators; // ------------------------------------------------------------------------------------------------------------- @@ -106,48 +88,17 @@ 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, final List typeAnnotationDecorators) { - super(); + 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 class that declares this field. - * - * @return The {@link ClassInfo} object for the declaring class. - * - * @see #getClassName() - */ - @Override - public ClassInfo getClassInfo() { - return super.getClassInfo(); - } - - // ------------------------------------------------------------------------------------------------------------- - /** * Deprecated -- use {@link #getModifiersStr()} instead. * @@ -164,39 +115,13 @@ public String getModifierStr() { * * @return The field modifiers, as a string. */ + @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. * @@ -207,12 +132,12 @@ 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; } /** @@ -221,34 +146,27 @@ public int getModifiers() { * * @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); - if (typeAnnotationDecorators != null) { - for (final TypeAnnotationDecorator decorator : typeAnnotationDecorators) { - decorator.decorate(typeDescriptor); + 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); } - } catch (final ParseException e) { - throw new IllegalArgumentException(e); } + return typeDescriptor; } - return typeDescriptor; - } - - /** - * Returns the type descriptor string for the field, which will not include type parameters. If you need generic - * type parameters, call {@link #getTypeSignatureStr()} instead. - * - * @return The type descriptor string for the field. - */ - public String getTypeDescriptorStr() { - return typeDescriptorStr; } /** @@ -262,41 +180,33 @@ public String getTypeDescriptorStr() { * 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); - if (typeAnnotationDecorators != null) { - for (final TypeAnnotationDecorator decorator : typeAnnotationDecorators) { - decorator.decorate(typeSignature); + 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); } - } 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; - } - - /** - * Returns the type signature string for the field, possibly including type parameters. If this returns null, - * indicating that no type signature information is available for this field, call - * {@link #getTypeDescriptorStr()} instead. - * - * @return The type signature string for the field, or null if not available. - */ - public String getTypeSignatureStr() { - return typeSignatureStr; } /** @@ -307,6 +217,7 @@ public String getTypeSignatureStr() { * @return The parsed type signature for the field, or if not available, the parsed type descriptor for the * field. */ + @Override public TypeSignature getTypeSignatureOrTypeDescriptor() { TypeSignature typeSig = null; try { @@ -320,22 +231,6 @@ public TypeSignature getTypeSignatureOrTypeDescriptor() { return getTypeDescriptor(); } - /** - * Returns the type signature string for the field, possibly including type parameters. If the type signature - * string is null, indicating that no type signature information is available for this field, returns the type - * descriptor string instead. - * - * @return The type signature string for the field, or if not available, the type descriptor string for the - * method. - */ - public String getTypeSignatureOrTypeDescriptorStr() { - if (typeSignatureStr != null) { - return typeSignatureStr; - } else { - return typeDescriptorStr; - } - } - /** * Returns the constant initializer value of a field. Requires * {@link ClassGraph#enableStaticFinalFieldConstantInitializerValues()} to have been called. Will only return @@ -355,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); - } - // ------------------------------------------------------------------------------------------------------------- /** @@ -414,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 { @@ -446,18 +289,6 @@ void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames) // ------------------------------------------------------------------------------------------------------------- - /** - * Get the name of the class that declares this field. - * - * @return The name of the declaring class. - * - * @see #getClassInfo() - */ - @Override - public String getClassName() { - return declaringClassName; - } - /* (non-Javadoc) * @see io.github.classgraph.ScanResultObject#setScanResult(io.github.classgraph.ScanResult) */ @@ -565,25 +396,26 @@ public int compareTo(final FieldInfo other) { // ------------------------------------------------------------------------------------------------------------- - @Override - protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + 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(' '); } 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(' '); } final TypeSignature typeSig = getTypeSignatureOrTypeDescriptor(); @@ -605,4 +437,9 @@ protected void toString(final boolean useSimpleNames, final StringBuilder buf) { } } } + + @Override + protected void toString(final boolean useSimpleNames, final StringBuilder buf) { + toString(true, useSimpleNames, buf); + } } 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 f0e9aea10..d3f6bb8a2 100644 --- a/src/main/java/io/github/classgraph/HierarchicalTypeSignature.java +++ b/src/main/java/io/github/classgraph/HierarchicalTypeSignature.java @@ -37,6 +37,11 @@ */ public abstract class HierarchicalTypeSignature extends ScanResultObject { protected AnnotationInfoList typeAnnotationInfo; + + /** A hierarchical type signature. */ + public HierarchicalTypeSignature() { + super(); + } /** * Add a type annotation. @@ -61,6 +66,15 @@ void setScanResult(final ScanResult 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. * diff --git a/src/main/java/io/github/classgraph/MethodInfo.java b/src/main/java/io/github/classgraph/MethodInfo.java index 577360ebb..b688773c7 100644 --- a/src/main/java/io/github/classgraph/MethodInfo.java +++ b/src/main/java/io/github/classgraph/MethodInfo.java @@ -28,11 +28,13 @@ */ 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; @@ -42,40 +44,17 @@ 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; @@ -100,8 +79,18 @@ public class MethodInfo extends ScanResultObject implements Comparable typeAnnotationDecorators; + private transient List typeAnnotationDecorators; + + private String[] thrownExceptionNames; + + private transient ClassInfoList thrownExceptions; // ------------------------------------------------------------------------------------------------------------- @@ -133,25 +122,30 @@ public class MethodInfo extends ScanResultObject implements Comparable methodTypeAnnotationDecorators) { - super(); - this.declaringClassName = definingClassName; - this.name = methodName; - this.modifiers = modifiers; - this.typeDescriptorStr = typeDescriptorStr; - this.typeSignatureStr = typeSignatureStr; + final AnnotationInfo[][] parameterAnnotationInfo, final boolean hasBody, final int minLineNum, + final int maxLineNum, final List 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; } // ------------------------------------------------------------------------------------------------------------- @@ -167,70 +161,83 @@ 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 class that declares this method. - * - * @return The {@link ClassInfo} object for the declaring class. - * - * @see #getClassName() - */ - @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 {@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); - if (typeAnnotationDecorators != null) { - for (final MethodTypeAnnotationDecorator decorator : typeAnnotationDecorators) { - decorator.decorate(typeDescriptor); + 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); } - } catch (final ParseException e) { - throw new IllegalArgumentException(e); } + return typeDescriptor; } - return typeDescriptor; - } - - /** - * Returns the type descriptor string for the method, which will not include type parameters. If you need - * generic type parameters, call {@link #getTypeSignatureStr()} instead. - * - * @return The type descriptor string for the method. - */ - public String getTypeDescriptorStr() { - return typeDescriptorStr; } /** @@ -244,38 +251,30 @@ public String getTypeDescriptorStr() { * 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); - if (typeAnnotationDecorators != null) { - for (final MethodTypeAnnotationDecorator decorator : typeAnnotationDecorators) { - decorator.decorate(typeSignature); + 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); } - } 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 type signature string for the method, possibly including type parameters. If this returns null, - * indicating that no type signature information is available for this method, call - * {@link #getTypeDescriptorStr()} instead. - * - * @return The type signature string for the method, or null if not available. - */ - public String getTypeSignatureStr() { - return typeSignatureStr; } /** @@ -286,6 +285,7 @@ public String getTypeSignatureStr() { * @return The parsed type signature for the method, or if not available, the parsed type descriptor for the * method. */ + @Override public MethodTypeSignature getTypeSignatureOrTypeDescriptor() { MethodTypeSignature typeSig = null; try { @@ -300,21 +300,35 @@ public MethodTypeSignature getTypeSignatureOrTypeDescriptor() { } /** - * Returns the type signature string 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 type - * descriptor string instead. + * Returns the list of exceptions thrown by the method, as a {@link ClassInfoList}. * - * @return The type signature string for the method, or if not available, the type descriptor string for the - * method. + * @return The list of exceptions thrown by the method, as a {@link ClassInfoList} (the list may be empty). */ - public String getTypeSignatureOrTypeDescriptorStr() { - if (typeSignatureStr != null) { - return typeSignatureStr; - } else { - return typeDescriptorStr; + 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 the exceptions thrown by the method, as an array. + * + * @return The exceptions thrown by the method, as an array (the array may be empty). + */ + public String[] getThrownExceptionNames() { + return thrownExceptionNames == null ? new String[0] : thrownExceptionNames; + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -328,33 +342,6 @@ public boolean isConstructor() { return "".equals(name); } - /** - * Returns true if this method is public. - * - * @return True if this method is public. - */ - public boolean isPublic() { - return Modifier.isPublic(modifiers); - } - - /** - * 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. - * - * @return True if this method is final. - */ - public boolean isFinal() { - return Modifier.isFinal(modifiers); - } - /** * Returns true if this method is synchronized. * @@ -373,15 +360,6 @@ public boolean isBridge() { return (modifiers & 0x0040) != 0; } - /** - * Returns true if this method is synthetic. - * - * @return True if this is synthetic. - */ - public boolean isSynthetic() { - return (modifiers & 0x1000) != 0; - } - /** * Returns true if this method is a varargs method. * @@ -400,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). * @@ -409,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). @@ -428,185 +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 - List paramTypeDescriptors = null; - int numParams = 0; - try { - final MethodTypeSignature typeSig = getTypeDescriptor(); - if (typeSig != null) { - paramTypeDescriptors = typeSig.getParameterTypeSignatures(); - numParams = paramTypeDescriptors.size(); - } - } catch (final Exception e) { - // Ignore - } - List paramTypeSignatures = null; - try { + // 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(); } - } catch (final Exception e) { - // Ignore - } - // Figure out the number of params in the alignment (should be num params in type descriptor) - if (paramTypeSignatures != null && paramTypeSignatures.size() > numParams) { - // Should not happen - throw new ClassGraphException( - "typeSignatureParamTypes.size() > typeDescriptorParamTypes.size() for method " - + declaringClassName + "." + name); - } + // 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. + } - // 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 new ClassGraphException("Type descriptor for method " + declaringClassName + "." + name - + " has insufficient parameters"); - } + // 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; + } - // 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." - - 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]; + // "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]; + } } } - } - 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]; + 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]; + } } } - } - 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]; + 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 && numParams > 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, n = numParams - paramTypeSignatures.size(); i < n; i++) { - // Left-pad with nulls - paramTypeSignaturesAligned.add(null); + 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 == null ? null : 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()); } /** @@ -637,7 +634,27 @@ private Class[] loadParameterClasses() { 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()); } return parameterClasses.toArray(new Class[0]); } @@ -649,7 +666,13 @@ private Class[] loadParameterClasses() { * * @return The {@link Method} reference for this method. * @throws IllegalArgumentException - * if the method does not exist, or if the method is a constructor. + *

    + *
  • 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()) { @@ -662,9 +685,12 @@ public Method loadClassAndGetMethod() throws IllegalArgumentException { } catch (final NoSuchMethodException e1) { try { return loadClass().getDeclaredMethod(getName(), parameterClassesArr); - } catch (final NoSuchMethodException es2) { + } 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); } } @@ -676,7 +702,12 @@ public Method loadClassAndGetMethod() throws IllegalArgumentException { * * @return The {@link Constructor} reference for this constructor. * @throws IllegalArgumentException - * if the constructor does not exist, or if the method is not a constructor. + *
    + *
  • 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()) { @@ -690,7 +721,7 @@ public Constructor loadClassAndGetConstructor() throws IllegalArgumentExcepti } catch (final NoSuchMethodException e1) { try { return loadClass().getDeclaredConstructor(parameterClassesArr); - } catch (final NoSuchMethodException es2) { + } catch (final NoSuchMethodException e2) { throw new IllegalArgumentException("Constructor not found for class " + getClassName()); } } @@ -723,9 +754,7 @@ void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames) } if (hasRepeatableAnnotation) { final AnnotationInfoList aiList = new AnnotationInfoList(pai.length); - for (final AnnotationInfo ai : pai) { - aiList.add(ai); - } + aiList.addAll(Arrays.asList(pai)); aiList.handleRepeatableAnnotations(allRepeatableAnnotationNames, getClassInfo(), RelType.METHOD_PARAMETER_ANNOTATIONS, RelType.CLASSES_WITH_METHOD_PARAMETER_ANNOTATION, @@ -739,18 +768,6 @@ void handleRepeatableAnnotations(final Set allRepeatableAnnotationNames) // ------------------------------------------------------------------------------------------------------------- - /** - * Get the name of the class that declares this method. - * - * @return The name of the declaring class. - * - * @see #getClassInfo() - */ - @Override - public String getClassName() { - return declaringClassName; - } - /* (non-Javadoc) * @see io.github.classgraph.ScanResultObject#setScanResult(io.github.classgraph.ScanResult) */ @@ -782,6 +799,13 @@ 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); + } + } + } } /** @@ -830,6 +854,14 @@ protected void findReferencedClassInfo(final Map classNameToC } } } + 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)); + } + } + } } // ------------------------------------------------------------------------------------------------------------- @@ -937,7 +969,9 @@ protected void toString(final boolean useSimpleNames, final StringBuilder buf) { buf); } - buf.append(' '); + if (buf.length() > 0) { + buf.append(' '); + } if (name != null) { buf.append(useSimpleNames ? ClassInfo.getSimpleName(name) : name); } @@ -987,47 +1021,52 @@ protected void toString(final boolean useSimpleNames, final StringBuilder buf) { MethodParameterInfo.modifiersToString(paramInfo.getModifiers(), buf); final TypeSignature paramTypeSignature = paramInfo.getTypeSignatureOrTypeDescriptor(); - 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; + // 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 { - annotationsToExclude = new AnnotationInfoList(paramInfo.annotationInfo.length); - for (int j = 0; j < paramInfo.annotationInfo.length; j++) { - annotationsToExclude.add(paramInfo.annotationInfo[j]); + // 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); } - paramTypeSignature.toStringInternal(useSimpleNames, annotationsToExclude, buf); } 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++) { @@ -1036,6 +1075,17 @@ protected void toString(final boolean useSimpleNames, final StringBuilder buf) { } 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]); + } + } } } } diff --git a/src/main/java/io/github/classgraph/MethodParameterInfo.java b/src/main/java/io/github/classgraph/MethodParameterInfo.java index abb48f037..090e99208 100644 --- a/src/main/java/io/github/classgraph/MethodParameterInfo.java +++ b/src/main/java/io/github/classgraph/MethodParameterInfo.java @@ -28,12 +28,15 @@ */ 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. * @@ -178,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 @@ -192,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 @@ -205,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. * diff --git a/src/main/java/io/github/classgraph/ModuleInfo.java b/src/main/java/io/github/classgraph/ModuleInfo.java index 272e7c65b..dfbc5ae22 100644 --- a/src/main/java/io/github/classgraph/ModuleInfo.java +++ b/src/main/java/io/github/classgraph/ModuleInfo.java @@ -28,11 +28,13 @@ */ package io.github.classgraph; +import java.lang.annotation.Annotation; import java.net.URI; 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. */ @@ -215,6 +217,14 @@ public PackageInfoList getPackageInfo() { // ------------------------------------------------------------------------------------------------------------- + void setScanResult(final ScanResult scanResult) { + if (annotationInfoSet != null) { + for (final AnnotationInfo ai : annotationInfoSet) { + ai.setScanResult(scanResult); + } + } + } + /** * Add annotations found in a module descriptor classfile. * @@ -231,9 +241,22 @@ void addAnnotations(final AnnotationInfoList 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 @@ -260,9 +283,21 @@ public AnnotationInfoList getAnnotationInfo() { 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. diff --git a/src/main/java/io/github/classgraph/ModulePathInfo.java b/src/main/java/io/github/classgraph/ModulePathInfo.java index 2cf1c6c2f..9700ae5e9 100644 --- a/src/main/java/io/github/classgraph/ModulePathInfo.java +++ b/src/main/java/io/github/classgraph/ModulePathInfo.java @@ -33,9 +33,10 @@ 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.ReflectionUtils; import nonapi.io.github.classgraph.utils.StringUtils; /** @@ -125,38 +126,47 @@ 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() { - // 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(managementFactory, "getRuntimeMXBean", - /* throwException = */ false); - @SuppressWarnings("unchecked") - final List commandlineArguments = runtimeMXBean == null ? null - : (List) ReflectionUtils.invokeMethod(runtimeMXBean, "getInputArguments", - /* throwException = */ false); - 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 - for (final String argPart : JarUtils.smartPathSplit(argParam, sepChar, - /* scanSpec = */ null)) { - 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))); } } } diff --git a/src/main/java/io/github/classgraph/ModuleReaderProxy.java b/src/main/java/io/github/classgraph/ModuleReaderProxy.java index ba4e2379b..2ec6ec657 100644 --- a/src/main/java/io/github/classgraph/ModuleReaderProxy.java +++ b/src/main/java/io/github/classgraph/ModuleReaderProxy.java @@ -31,10 +31,11 @@ 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 { @@ -47,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. @@ -66,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"); } @@ -104,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())"); } @@ -124,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. @@ -172,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; } /** @@ -182,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 7edee00eb..97c09ab3f 100644 --- a/src/main/java/io/github/classgraph/ModuleRef.java +++ b/src/main/java/io/github/classgraph/ModuleRef.java @@ -35,8 +35,8 @@ import java.util.List; import java.util.Set; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; import nonapi.io.github.classgraph.utils.CollectionUtils; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** 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); CollectionUtils.sortIfNotEmpty(this.packages); - final Object optionalRawVersion = ReflectionUtils.invokeMethod(this.descriptor, "rawVersion", - /* throwException = */ true); + 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); } /** diff --git a/src/main/java/io/github/classgraph/ObjectTypedValueWrapper.java b/src/main/java/io/github/classgraph/ObjectTypedValueWrapper.java index a18f175e0..838f9f888 100644 --- a/src/main/java/io/github/classgraph/ObjectTypedValueWrapper.java +++ b/src/main/java/io/github/classgraph/ObjectTypedValueWrapper.java @@ -40,6 +40,7 @@ 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 annotationEnumValue; @@ -629,21 +630,21 @@ protected void toString(final boolean useSimpleNames, final StringBuilder buf) { } else if (stringValue != null) { buf.append(stringValue); } else if (integerValue != null) { - buf.append(Integer.toString(integerValue)); + buf.append(integerValue); } else if (longValue != null) { - buf.append(Long.toString(longValue)); + buf.append(longValue); } else if (shortValue != null) { - buf.append(Short.toString(shortValue)); + buf.append(shortValue); } else if (booleanValue != null) { - buf.append(Boolean.toString(booleanValue)); + buf.append(booleanValue); } else if (characterValue != null) { - buf.append(Character.toString(characterValue)); + buf.append(characterValue); } else if (floatValue != null) { - buf.append(Float.toString(floatValue)); + buf.append(floatValue); } else if (doubleValue != null) { - buf.append(Double.toString(doubleValue)); + buf.append(doubleValue); } else if (byteValue != null) { - buf.append(Byte.toString(byteValue)); + buf.append(byteValue); } else if (stringArrayValue != null) { buf.append(Arrays.toString(stringArrayValue)); } else if (intArrayValue != null) { diff --git a/src/main/java/io/github/classgraph/PackageInfo.java b/src/main/java/io/github/classgraph/PackageInfo.java index 15d2269ba..f617ed7e6 100644 --- a/src/main/java/io/github/classgraph/PackageInfo.java +++ b/src/main/java/io/github/classgraph/PackageInfo.java @@ -28,6 +28,7 @@ */ package io.github.classgraph; +import java.lang.annotation.Annotation; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -35,6 +36,8 @@ 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. */ @@ -121,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 @@ -150,6 +174,18 @@ public AnnotationInfoList getAnnotationInfo() { 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()); + } + /** * Check if the package has the named annotation. * @@ -270,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) { @@ -287,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; } } diff --git a/src/main/java/io/github/classgraph/Resource.java b/src/main/java/io/github/classgraph/Resource.java index 2400915f9..b7430850a 100644 --- a/src/main/java/io/github/classgraph/Resource.java +++ b/src/main/java/io/github/classgraph/Resource.java @@ -101,7 +101,7 @@ public Resource(final ClasspathElement classpathElement, final long length) { private static URL uriToURL(final URI uri) { try { return uri.toURL(); - } catch (final MalformedURLException 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 " @@ -212,11 +212,9 @@ public ModuleRef getModuleRef() { * If an I/O exception occurred. */ public String getContentAsString() throws IOException { - try { - return new String(load(), StandardCharsets.UTF_8); - } finally { - close(); - } + final String content = new String(load(), StandardCharsets.UTF_8); + close(); + return content; } // ------------------------------------------------------------------------------------------------------------- @@ -239,7 +237,10 @@ public String getContentAsString() throws IOException { * 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(); + } // ------------------------------------------------------------------------------------------------------------- @@ -255,14 +256,37 @@ public String getContentAsString() throws IOException { /** * 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 diff --git a/src/main/java/io/github/classgraph/ResourceList.java b/src/main/java/io/github/classgraph/ResourceList.java index b551ea12e..32b3e348c 100644 --- a/src/main/java/io/github/classgraph/ResourceList.java +++ b/src/main/java/io/github/classgraph/ResourceList.java @@ -363,15 +363,12 @@ public interface ByteArrayConsumerThrowsIOException { @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(); } } } @@ -403,13 +400,10 @@ public void forEachByteArray(final ByteArrayConsumer byteArrayConsumer) { */ public void forEachByteArrayIgnoringIOException(final ByteArrayConsumer byteArrayConsumer) { for (final Resource resource : this) { - try { - final byte[] resourceContent = resource.load(); - byteArrayConsumer.accept(resource, resourceContent); + try (Resource resourceToClose = resource) { + byteArrayConsumer.accept(resourceToClose, resourceToClose.load()); } catch (final IOException e) { // Ignore - } finally { - resource.close(); } } } @@ -427,11 +421,8 @@ public void forEachByteArrayIgnoringIOException(final ByteArrayConsumer byteArra public void forEachByteArrayThrowingIOException( final ByteArrayConsumerThrowsIOException byteArrayConsumerThrowsIOException) throws IOException { for (final Resource resource : this) { - try { - final byte[] resourceContent = resource.load(); - byteArrayConsumerThrowsIOException.accept(resource, resourceContent); - } finally { - resource.close(); + try (Resource resourceToClose = resource) { + byteArrayConsumerThrowsIOException.accept(resourceToClose, resourceToClose.load()); } } } @@ -492,14 +483,12 @@ public interface InputStreamConsumerThrowsIOException { 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(); } } } @@ -531,12 +520,10 @@ public void forEachInputStream(final InputStreamConsumer inputStreamConsumer) { */ public void forEachInputStreamIgnoringIOException(final InputStreamConsumer inputStreamConsumer) { 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) { // Ignore - } finally { - resource.close(); } } } @@ -555,10 +542,8 @@ public void forEachInputStreamIgnoringIOException(final InputStreamConsumer inpu public void forEachInputStreamThrowingIOException( final InputStreamConsumerThrowsIOException inputStreamConsumerThrowsIOException) throws IOException { for (final Resource resource : this) { - try { - inputStreamConsumerThrowsIOException.accept(resource, resource.open()); - } finally { - resource.close(); + try (final Resource resourceToClose = resource) { + inputStreamConsumerThrowsIOException.accept(resourceToClose, resourceToClose.open()); } } } @@ -617,15 +602,12 @@ public interface ByteBufferConsumerThrowsIOException { @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(); } } } @@ -657,13 +639,10 @@ public void forEachByteBuffer(final ByteBufferConsumer byteBufferConsumer) { */ public void forEachByteBufferIgnoringIOException(final ByteBufferConsumer byteBufferConsumer) { 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) { // Ignore - } finally { - resource.close(); } } } @@ -681,11 +660,8 @@ public void forEachByteBufferIgnoringIOException(final ByteBufferConsumer byteBu public void forEachByteBufferThrowingIOException( final ByteBufferConsumerThrowsIOException byteBufferConsumerThrowsIOException) throws IOException { for (final Resource resource : this) { - try { - final ByteBuffer byteBuffer = resource.read(); - byteBufferConsumerThrowsIOException.accept(resource, byteBuffer); - } finally { - resource.close(); + 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 f54cdfd26..83bbaec8e 100644 --- a/src/main/java/io/github/classgraph/ScanResult.java +++ b/src/main/java/io/github/classgraph/ScanResult.java @@ -30,6 +30,7 @@ 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; @@ -55,7 +56,10 @@ 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; @@ -65,17 +69,21 @@ * 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, AutoCloseable { +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 accepted packages. */ private ResourceList allAcceptedResourcesCached; - /** The number of times {@link #getResourcesWithPath(String)} has been called. */ + /** + * The number of times {@link #getResourcesWithPath(String)} has been called. + */ private final AtomicInteger getResourcesWithPathCallCount = new AtomicInteger(); /** @@ -99,7 +107,9 @@ 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. */ @@ -117,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; @@ -140,7 +152,9 @@ public final class ScanResult implements Closeable, AutoCloseable { /** The current serialization format. */ 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; @@ -202,15 +216,21 @@ public SerializationFormat(final String serializationFormatStr, final ScanSpec s /** * Static initialization (warm up classloading), called when the ClassGraph class is initialized. */ - static void init() { + 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), /* log = */ null); + // 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); } } @@ -256,6 +276,7 @@ static void init() { this.packageNameToPackageInfo = packageNameToPackageInfo; this.moduleNameToModuleInfo = moduleNameToModuleInfo; this.nestedJarHandler = nestedJarHandler; + this.reflectionUtils = nestedJarHandler.reflectionUtils; this.topLevelLog = topLevelLog; if (classNameToClassInfo != null) { @@ -312,7 +333,8 @@ private void indexResourcesAndClassInfo(final LogNode log) { 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())) { @@ -330,6 +352,16 @@ private void indexResourcesAndClassInfo(final LogNode log) { 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); + } + } } // ------------------------------------------------------------------------------------------------------------- @@ -446,6 +478,7 @@ public List getModules() { * @return The {@link ModulePathInfo}. */ public ModulePathInfo getModulePathInfo() { + scanSpec.modulePathInfo.getRuntimeInfo(reflectionUtils); return scanSpec.modulePathInfo; } @@ -458,16 +491,18 @@ public ModulePathInfo getModulePathInfo() { * @return A list of all resources (including classfiles and non-classfiles) found in accepted packages. */ public ResourceList getAllResources() { - if (allAcceptedResourcesCached == null) { - // Index Resource objects by path - final ResourceList acceptedResourcesList = new ResourceList(); - for (final ClasspathElement classpathElt : classpathOrder) { - acceptedResourcesList.addAll(classpathElt.acceptedResources); + 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 - allAcceptedResourcesCached = acceptedResourcesList; + return allAcceptedResourcesCached; } - return allAcceptedResourcesCached; } /** @@ -478,19 +513,21 @@ public ResourceList getAllResources() { * non-classfiles) found in accepted packages. */ public Map getAllResourcesAsMap() { - 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()); + 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 - pathToAcceptedResourcesCached = pathToAcceptedResourceListMap; + return pathToAcceptedResourcesCached; } - return pathToAcceptedResourcesCached; } /** @@ -508,12 +545,14 @@ public ResourceList getResourcesWithPath(final String resourcePath) { } 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 - return getAllResourcesAsMap().get(path); + // If numerous calls are made, produce and cache a single HashMap for O(1) + // access time + matchingResources = getAllResourcesAsMap().get(path); } else { - // If just a few calls are made, directly search for resource with the requested path - ResourceList matchingResources = null; + // 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)) { @@ -524,8 +563,8 @@ public ResourceList getResourcesWithPath(final String resourcePath) { } } } - return matchingResources == null ? ResourceList.EMPTY_LIST : matchingResources; } + return matchingResources == null ? ResourceList.EMPTY_LIST : matchingResources; } /** @@ -634,7 +673,8 @@ public ResourceList getResourcesWithExtension(final String extension) { } /** - * Get the list of all resources found in accepted 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. @@ -659,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 @@ -772,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); } @@ -882,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. * @@ -923,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. * @@ -942,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. * @@ -962,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. * @@ -1001,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. @@ -1020,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). @@ -1075,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. * @@ -1095,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 @@ -1315,12 +1564,15 @@ public static ScanResult fromJSON(final String json) { final SerializationFormat deserialized = JSONDeserializer.deserializeObject(SerializationFormat.class, json); if (deserialized == null || !deserialized.format.equals(CURRENT_SERIALIZATION_FORMAT)) { - // Probably the deserialization failed before now anyway, if fields have changed, etc. + // 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; final ScanResult scanResult; @@ -1330,7 +1582,8 @@ public static ScanResult fromJSON(final String json) { } scanResult.rawClasspathEltOrderStrs = deserialized.classpath; - // 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) { @@ -1352,7 +1605,7 @@ public static ScanResult fromJSON(final String json) { } } - // Index Resource and ClassInfo objects + // Index Resource and ClassInfo objects scanResult.indexResourcesAndClassInfo(/* log = */ null); scanResult.isObtainedFromDeserialization = true; @@ -1431,10 +1684,12 @@ public void close() { } classGraphClassLoader = null; if (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; + // 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(); @@ -1448,21 +1703,33 @@ 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; classpathFinder = null; - // Flush log on exit, in case additional log entries were generated after scan() completed + 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 diff --git a/src/main/java/io/github/classgraph/ScanResultObject.java b/src/main/java/io/github/classgraph/ScanResultObject.java index 97e24a528..20d577adc 100644 --- a/src/main/java/io/github/classgraph/ScanResultObject.java +++ b/src/main/java/io/github/classgraph/ScanResultObject.java @@ -170,24 +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) { - final String className = getClassInfoNameOrClassName(); - if (scanResult != null) { - classRef = scanResult.loadClass(className, superclassOrInterfaceType, ignoreExceptions); - } else { - // Fallback, if scanResult is not set + synchronized (this) { + // If class is not already loaded, try loading class + if (classRef == null) { + final String className = getClassInfoNameOrClassName(); try { - classRef = Class.forName(className); + 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; } /** diff --git a/src/main/java/io/github/classgraph/Scanner.java b/src/main/java/io/github/classgraph/Scanner.java index 6f452d552..d0f81c76c 100644 --- a/src/main/java/io/github/classgraph/Scanner.java +++ b/src/main/java/io/github/classgraph/Scanner.java @@ -29,16 +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.net.URLDecoder; 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; @@ -48,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; @@ -63,15 +63,16 @@ import io.github.classgraph.Classfile.ClassfileFormatException; import io.github.classgraph.Classfile.SkipClassException; import nonapi.io.github.classgraph.classpath.ClasspathFinder; -import nonapi.io.github.classgraph.classpath.ClasspathOrder.ClasspathElementAndClassLoader; +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; @@ -140,7 +141,8 @@ class Scanner implements Callable { */ Scanner(final boolean performScan, final ScanSpec scanSpec, final ExecutorService executorService, final int numParallelTasks, final ScanResultProcessor scanResultProcessor, - final FailureHandler failureHandler, final LogNode topLevelLog) throws InterruptedException { + final FailureHandler failureHandler, final ReflectionUtils reflectionUtils, final LogNode topLevelLog) + throws InterruptedException { this.scanSpec = scanSpec; this.performScan = performScan; scanSpec.sortPrefixes(); @@ -158,14 +160,14 @@ 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.classpathFinder = new ClasspathFinder(scanSpec, reflectionUtils, classpathFinderLog); try { this.moduleOrder = new ArrayList<>(); @@ -193,8 +195,10 @@ class Scanner implements Callable { || scanSpec.moduleAcceptReject.isSpecificallyAcceptedAndNotRejected(moduleName)) { // Create a new ClasspathElementModule final ClasspathElementModule classpathElementModule = new ClasspathElementModule( - systemModuleRef, defaultClassLoader, - nestedJarHandler.moduleRefToModuleReaderProxyRecyclerMap, scanSpec); + systemModuleRef, nestedJarHandler.moduleRefToModuleReaderProxyRecyclerMap, + new ClasspathEntryWorkUnit(null, defaultClassLoader, null, moduleOrder.size(), + ""), + scanSpec); moduleOrder.add(classpathElementModule); // Open the ClasspathElementModule classpathElementModule.open(/* ignored */ null, classpathFinderLog); @@ -216,8 +220,10 @@ class Scanner implements Callable { if (scanSpec.moduleAcceptReject.isAcceptedAndNotRejected(moduleName)) { // Create a new ClasspathElementModule final ClasspathElementModule classpathElementModule = new ClasspathElementModule( - nonSystemModuleRef, defaultClassLoader, - nestedJarHandler.moduleRefToModuleReaderProxyRecyclerMap, scanSpec); + nonSystemModuleRef, nestedJarHandler.moduleRefToModuleReaderProxyRecyclerMap, + new ClasspathEntryWorkUnit(null, defaultClassLoader, null, moduleOrder.size(), + ""), + scanSpec); moduleOrder.add(classpathElementModule); // Open the ClasspathElementModule classpathElementModule.open(/* ignored */ null, classpathFinderLog); @@ -251,70 +257,40 @@ class Scanner implements Callable { 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); - CollectionUtils.sortIfNotEmpty(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; } @@ -352,269 +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 ClasspathElementAndClassLoader 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 ClasspathElementAndClassLoader 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 // - classpathEntryToClasspathElementSingletonMap = // - new SingletonMap() { - @Override - public ClasspathElement newInstance(final ClasspathElementAndClassLoader classpathEntry, - final LogNode log) throws IOException, InterruptedException { - Object classpathEntryObj = classpathEntry.classpathElementRoot; - String dirOrPathPackageRoot = classpathEntry.dirOrPathPackageRoot; - while (dirOrPathPackageRoot.startsWith("/")) { - dirOrPathPackageRoot = dirOrPathPackageRoot.substring(1); - } + 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 a URL instance - if (classpathEntryObj instanceof String) { - final String classpathEntryStr = (String) classpathEntryObj; - if (JarUtils.URL_SCHEME_PATTERN.matcher(classpathEntryStr).matches()) { - try { - classpathEntryObj = new URL(classpathEntryStr); - } catch (final MalformedURLException e) { - throw new IOException("Malformed URL: " + classpathEntryStr); - } - } - } + // 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; - // Check type of classpath entry object - Path classpathEntryPath = null; - if (classpathEntryObj instanceof URL) { - URL classpathEntryURL = (URL) classpathEntryObj; - String scheme = classpathEntryURL.getProtocol(); - if ("jar".equals(scheme)) { - // Strip off "jar:" scheme prefix - try { - classpathEntryURL = new URL( - URLDecoder.decode(classpathEntryURL.toString(), "UTF-8").substring(4)); - scheme = classpathEntryURL.getProtocol(); - } catch (final MalformedURLException e) { - throw new IOException("Could not strip 'jar:' prefix from " + classpathEntryObj, e); - } - } - if ("http".equals(scheme) || "https".equals(scheme)) { - // Jar URL or URI (remote URLs/URIs must be jars) - return new ClasspathElementZip(classpathEntryURL, classpathEntry.classLoader, - nestedJarHandler, scanSpec); - } else { - try { + // If this is not a multi-section URL, try converting URL to a Path + if (!isMultiSection) { + try { + 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 - classpathEntryPath = Paths.get(classpathEntryURL.toURI()); - } catch (final IllegalArgumentException | SecurityException | URISyntaxException e) { - throw new IOException( - "Cannot handle URL " + classpathEntryURL + " : " + e.getMessage()); - } catch (final FileSystemNotFoundException e) { - // This is a custom URL scheme without a backing FileSystem - return new ClasspathElementZip(classpathEntryURL, classpathEntry.classLoader, - nestedJarHandler, scanSpec); - } - } - } else if (classpathEntryObj instanceof URI) { - URI classpathEntryURI = (URI) classpathEntryObj; - String scheme = classpathEntryURI.getScheme(); - if ("jar".equals(scheme)) { - // Strip off "jar:" scheme prefix - try { - classpathEntryURI = new URI( - URLDecoder.decode(classpathEntryURI.toString(), "UTF-8").substring(4)); - scheme = classpathEntryURI.getScheme(); - } catch (final URISyntaxException e) { - throw new IOException("Could not strip 'jar:' prefix from " + classpathEntryObj, e); - } - } - if ("http".equals(scheme) || "https".equals(scheme)) { - // Jar URL or URI (remote URLs/URIs must be jars) - return new ClasspathElementZip(classpathEntryURI, classpathEntry.classLoader, - nestedJarHandler, scanSpec); - } else { - try { - // See if the URI resolves to a file or directory via the Path API - classpathEntryPath = Paths.get(classpathEntryURI); - } catch (final IllegalArgumentException | SecurityException e) { - throw new IOException( - "Cannot handle URI " + classpathEntryURI + " : " + e.getMessage()); - } catch (final FileSystemNotFoundException e) { - // This is a custom URI scheme without a backing FileSystem - return new ClasspathElementZip(classpathEntryURI, classpathEntry.classLoader, - nestedJarHandler, scanSpec); + 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 if (classpathEntryObj instanceof Path) { - classpathEntryPath = (Path) classpathEntryObj; - } else { - // Fall through for any other object type (toString will be used to get path) - } - - if (classpathEntryPath != null) { - final Path packageRootPath = classpathEntryPath.resolve(dirOrPathPackageRoot); - if (FileUtils.canReadAndIsFile(packageRootPath)) { - // classpathEntryObj is a Path which points to a lib/ext jar inside a parent Path - return new ClasspathElementZip(classpathEntryPath, classpathEntry.classLoader, - nestedJarHandler, scanSpec); - } else if (FileUtils.canReadAndIsDir(packageRootPath)) { - // classpathEntryObj is a Path which points to a dir -- need to scan it recursively - return new ClasspathElementPathDir(classpathEntryPath, dirOrPathPackageRoot, - classpathEntry.classLoader, nestedJarHandler, scanSpec); - } - } + } // else this is a remote jar URL - // Fall through for other object types (including String) - // Convert classpathEntryObj to a string - final String classpathEntryPathStr = classpathEntryObj.toString(); - - // Normalize path -- strip off any leading "jar:" / "file:", and normalize separators - final String pathNormalized = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, - classpathEntryPathStr); - - // 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 = classpathEntryPathStr.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"); + } 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 } - // 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) - try { - return this.get(new ClasspathElementAndClassLoader(canonicalPathNormalized, - dirOrPathPackageRoot, classpathEntry.classLoader), log); - } catch (final NullSingletonException e) { - throw new IOException("Cannot get classpath element for canonical path " - + canonicalPathNormalized + " : " + e); - } - } 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, classpathEntry.classLoader, - nestedJarHandler, scanSpec) - : new ClasspathElementFileDir(fileCanonicalized, dirOrPathPackageRoot, - classpathEntry.classLoader, nestedJarHandler, scanSpec); + } + } + // 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 ClasspathElementFileDir} or {@link ClasspathElementZip} -- {@link ClasspathElementModule is handled + * {@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)) { - final LogNode subLog = log == null ? null - : log.log("Opening classpath element " + 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, subLog); - - // 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.classpathElementRoot - + (workUnit.rawClasspathEntry.dirOrPathPackageRoot.isEmpty() ? "" - : "/" + workUnit.rawClasspathEntry.dirOrPathPackageRoot) - + " : " + e); + log.log("Skipping invalid classpath entry " + workUnit.classpathEntryObj + " : " + + (e.getCause() == null ? e : e.getCause())); } } } @@ -727,6 +727,7 @@ 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, @@ -737,20 +738,27 @@ public void processWorkUnit(final ClassfileScanWorkUnit workUnit, // Enqueue the classfile for linking scannedClassfiles.add(classfile); + if (subLog != null) { + subLog.addElapsedTime(); + } } catch (final SkipClassException e) { if (subLog != null) { subLog.log(workUnit.classfileResource.getPath(), "Skipping classfile: " + e.getMessage()); + subLog.addElapsedTime(); } } catch (final ClassfileFormatException e) { if (subLog != null) { subLog.log(workUnit.classfileResource.getPath(), "Invalid classfile: " + e.getMessage()); + subLog.addElapsedTime(); } } catch (final IOException e) { if (subLog != null) { 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(); } } @@ -833,10 +841,11 @@ private void preprocessClasspathElementsByType(final List fina final List> classpathEltDirs = new ArrayList<>(); final List> classpathEltZips = new ArrayList<>(); for (final ClasspathElement classpathElt : finalTraditionalClasspathEltOrder) { - if (classpathElt instanceof ClasspathElementFileDir) { - // Separate out ClasspathElementDir elements from other types - classpathEltDirs.add(new SimpleEntry<>(((ClasspathElementFileDir) classpathElt).getFile().getPath(), - classpathElt)); + if (classpathElt instanceof ClasspathElementDir) { + // 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 @@ -940,7 +949,7 @@ private ScanResult performScan(final List finalClasspathEltOrd if (scanSpec.enableClassInfo) { // Get accepted classfile order final List classfileScanWorkItems = new ArrayList<>(); - final Set acceptedClassNamesFound = new HashSet(); + final Set acceptedClassNamesFound = new HashSet<>(); for (final ClasspathElement classpathElement : finalClasspathEltOrder) { // Get classfile scan order across all classpath elements for (final Resource resource : classpathElement.acceptedClassfileResources) { @@ -1010,9 +1019,17 @@ private ScanResult performScan(final List finalClasspathEltOrd } // Return a new ScanResult - return new ScanResult(scanSpec, finalClasspathEltOrder, finalClasspathEltOrderStrs, classpathFinder, - 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; } // ------------------------------------------------------------------------------------------------------------- @@ -1031,27 +1048,30 @@ private ScanResult performScan(final List finalClasspathEltOrd private ScanResult openClasspathElementsThenScan() throws InterruptedException, ExecutionException { // Get order of elements in traditional classpath final List rawClasspathEntryWorkUnits = new ArrayList<>(); - for (final ClasspathElementAndClassLoader 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 @@ -1156,9 +1176,11 @@ public ScanResult call() throws InterruptedException, CancellationException, Exe if (scanResultProcessor != null) { try { scanResultProcessor.processScanResult(scanResult); - } finally { + } catch (final Exception e) { scanResult.close(); + throw new ExecutionException(e); } + scanResult.close(); } } catch (final Throwable e) { @@ -1181,6 +1203,11 @@ public ScanResult call() throws InterruptedException, CancellationException, Exe interruptionChecker.interrupt(); if (failureHandler == null) { + 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; } else { @@ -1198,19 +1225,23 @@ public ScanResult call() throws InterruptedException, CancellationException, Exe 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; } } + } - } finally { - if (removeTemporaryFilesAfterScan) { - // If removeTemporaryFilesAfterScan was set, remove temp files and close resources, - // zipfiles and modules - nestedJarHandler.close(topLevelLog); - } + 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 302d57034..8050cf9eb 100644 --- a/src/main/java/io/github/classgraph/TypeArgument.java +++ b/src/main/java/io/github/classgraph/TypeArgument.java @@ -103,11 +103,17 @@ protected void addTypeAnnotation(final List typePath, final Annota // 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.addTypeAnnotation(typePath.subList(1, typePath.size()), annotationInfo); + // 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.addTypeAnnotation(typePath, annotationInfo); + // 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); + } } } diff --git a/src/main/java/io/github/classgraph/TypeParameter.java b/src/main/java/io/github/classgraph/TypeParameter.java index 095bc5559..308e4d039 100644 --- a/src/main/java/io/github/classgraph/TypeParameter.java +++ b/src/main/java/io/github/classgraph/TypeParameter.java @@ -62,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; @@ -131,7 +131,7 @@ static List parseList(final Parser parser, final String definingC throw new ParseException(parser, "Missing '>'"); } // Scala can contain '$' in type parameter names (#495) - if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ false)) { + if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ false, /* stopAtDot = */ true)) { throw new ParseException(parser, "Could not parse identifier token"); } final String identifier = parser.currToken(); diff --git a/src/main/java/io/github/classgraph/TypeVariableSignature.java b/src/main/java/io/github/classgraph/TypeVariableSignature.java index d8a46a4ee..722428c72 100644 --- a/src/main/java/io/github/classgraph/TypeVariableSignature.java +++ b/src/main/java/io/github/classgraph/TypeVariableSignature.java @@ -29,6 +29,7 @@ package io.github.classgraph; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; @@ -49,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; + // ------------------------------------------------------------------------------------------------------------- /** @@ -87,36 +91,48 @@ 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); - } - 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)) { - 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; } // ------------------------------------------------------------------------------------------------------------- @@ -148,7 +164,7 @@ static TypeVariableSignature parse(final Parser parser, final String definingCla if (peek == 'T') { parser.next(); // Scala can contain '$' in type variable names (#495) - if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ false)) { + if (!TypeUtils.getIdentifierToken(parser, /* stopAtDollarSign = */ false, /* stopAtDot = */ true)) { throw new ParseException(parser, "Could not parse type variable signature"); } parser.expect(';'); @@ -191,7 +207,16 @@ protected String getClassName() { */ @Override protected void findReferencedClassNames(final Set refdClassNames) { - // No class names present in type variables + // 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); + } } // ------------------------------------------------------------------------------------------------------------- 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 070c859e2..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,11 +28,11 @@ */ 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; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** Extract classpath entries from the Ant ClassLoader. */ class AntClassLoaderHandler implements ClassLoaderHandler { @@ -50,7 +50,8 @@ private AntClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "org.apache.tools.ant.AntClassLoader".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.apache.tools.ant.AntClassLoader"); } /** @@ -84,7 +85,7 @@ public static void findClassLoaderOrder(final ClassLoader classLoader, final Cla public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { classpathOrder.addClasspathPathStr( - (String) ReflectionUtils.invokeMethod(classLoader, "getClasspath", false), classLoader, scanSpec, - log); + (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 index ca3a054f5..256157df6 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassGraphClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassGraphClassLoaderHandler.java @@ -31,6 +31,7 @@ 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; @@ -55,7 +56,8 @@ private ClassGraphClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - final boolean matches = "io.github.classgraph.ClassGraphClassLoader".equals(classLoaderClass.getName()); + 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. " 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 7cb427b85..5da03c36c 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandlerRegistry.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/ClassLoaderHandlerRegistry.java @@ -58,6 +58,7 @@ public class ClassLoaderHandlerRegistry { 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), @@ -73,7 +74,10 @@ public class ClassLoaderHandlerRegistry { new ClassLoaderHandlerRegistryEntry(URLClassLoaderHandler.class), // Placeholder for delegation to a ClassGraphClassLoader instance from an outer nested scan - new ClassLoaderHandlerRegistryEntry(ClassGraphClassLoaderHandler.class))); + new ClassLoaderHandlerRegistryEntry(ClassGraphClassLoaderHandler.class) + + // FallbackClassLoaderHandler.class is registered separately below + )); /** Fallback ClassLoaderHandler. */ public static final ClassLoaderHandlerRegistryEntry FALLBACK_HANDLER = new ClassLoaderHandlerRegistryEntry( 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 5517fd6b4..ab601a60b 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/EquinoxClassLoaderHandler.java @@ -32,11 +32,11 @@ import java.util.HashSet; import java.util.Set; +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. @@ -65,7 +65,8 @@ private EquinoxClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "org.eclipse.osgi.internal.loader.EquinoxClassLoader".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.eclipse.osgi.internal.loader.EquinoxClassLoader"); } /** @@ -106,11 +107,12 @@ private static void addBundleFile(final Object bundlefile, final Set pat // Don't get stuck in infinite loop if (bundlefile != null && path.add(bundlefile)) { // type File - final Object baseFile = ReflectionUtils.getFieldVal(bundlefile, "basefile", false); + 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); + 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/" @@ -119,15 +121,15 @@ private static void addBundleFile(final Object bundlefile, final Set pat if (bundlefile.getClass().getName() .equals("org.eclipse.osgi.storage.bundlefile.NestedDirBundleFile")) { // Handle nested ZipBundleFile with "!/" separator - final Object baseBundleFile = ReflectionUtils.getFieldVal(bundlefile, "baseBundleFile", - false); + 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.toString() + sep + fieldVal.toString(); + final String pathElement = base + sep + fieldVal; classpathOrderOut.addClasspathEntry(pathElement, classLoader, scanSpec, log); break; } @@ -138,10 +140,10 @@ private static void addBundleFile(final Object bundlefile, final Set pat } } - addBundleFile(ReflectionUtils.getFieldVal(bundlefile, "wrapped", false), path, classLoader, - classpathOrderOut, scanSpec, log); - addBundleFile(ReflectionUtils.getFieldVal(bundlefile, "next", false), path, classLoader, - classpathOrderOut, scanSpec, 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); } } @@ -162,13 +164,13 @@ private static void addBundleFile(final Object bundlefile, final Set pat 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); + final Object bundlefile = classpathOrderOut.reflectionUtils.getFieldVal(false, entry, "bundlefile"); addBundleFile(bundlefile, new HashSet<>(), classLoader, classpathOrderOut, scanSpec, log); } } @@ -189,11 +191,11 @@ private static void addClasspathEntries(final Object owner, final ClassLoader cl 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); + 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 @@ -204,33 +206,40 @@ public static void findClasspathOrder(final ClassLoader classLoader, final Class // 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) { 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 50bf9d8e0..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,11 +28,11 @@ */ 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; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** Extract classpath entries from the Eclipse Equinox ContextFinder ClassLoader. */ class EquinoxContextFinderClassLoaderHandler implements ClassLoaderHandler { @@ -50,7 +50,8 @@ private EquinoxContextFinderClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "org.eclipse.osgi.internal.framework.ContextFinder".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.eclipse.osgi.internal.framework.ContextFinder"); } /** @@ -65,9 +66,8 @@ public static boolean canHandle(final Class classLoaderClass, final LogNode l */ public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, final LogNode log) { - classLoaderOrder.delegateTo( - (ClassLoader) ReflectionUtils.getFieldVal(classLoader, "parentContextClassLoader", false), - /* isParent = */ true, log); + classLoaderOrder.delegateTo((ClassLoader) classLoaderOrder.reflectionUtils.getFieldVal(false, classLoader, + "parentContextClassLoader"), /* isParent = */ true, log); classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); classLoaderOrder.add(classLoader, log); } 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 1b222487c..646f71ff3 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FallbackClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/FallbackClassLoaderHandler.java @@ -32,7 +32,6 @@ 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. @@ -88,85 +87,113 @@ public static void findClasspathOrder(final ClassLoader classLoader, final Class final ScanSpec scanSpec, final LogNode log) { boolean valid = false; valid |= classpathOrder.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getClassPath", false), classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getClasspath", false), classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "classpath", false), classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "classPath", false), classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "cp", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject( - ReflectionUtils.getFieldVal(classLoader, "classpath", false), classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject( - ReflectionUtils.getFieldVal(classLoader, "classPath", false), classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "cp", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getPath", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getPaths", false), classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "path", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "paths", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "paths", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "paths", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getDir", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getDirs", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "dir", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "dirs", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "dir", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "dirs", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getFile", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject( - ReflectionUtils.invokeMethod(classLoader, "getFiles", false), classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "file", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "files", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "file", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "files", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getJar", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getJars", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "jar", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "jars", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "jar", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "jars", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getURL", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getURLs", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getUrl", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "getUrls", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "url", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.invokeMethod(classLoader, "urls", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "url", false), - classLoader, scanSpec, log); - valid |= classpathOrder.addClasspathEntryObject(ReflectionUtils.getFieldVal(classLoader, "urls", false), - classLoader, scanSpec, log); + 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 e5ff8f5fd..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,11 +33,12 @@ import java.util.List; import java.util.Set; +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. @@ -62,10 +63,10 @@ private FelixClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "org.apache.felix.framework.BundleWiringImpl$BundleClassLoaderJava5" - .equals(classLoaderClass.getName()) - || "org.apache.felix.framework.BundleWiringImpl$BundleClassLoader" - .equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.apache.felix.framework.BundleWiringImpl$BundleClassLoaderJava5") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.apache.felix.framework.BundleWiringImpl$BundleClassLoader"); } /** @@ -91,8 +92,8 @@ public static void findClassLoaderOrder(final ClassLoader classLoader, final Cla * the content object * @return the content location */ - private static File getContentLocation(final Object content) { - return (File) ReflectionUtils.invokeMethod(content, "getFile", false); + private static File getContentLocation(final Object content, final ReflectionUtils reflectionUtils) { + return (File) reflectionUtils.invokeMethod(false, content, "getFile"); } /** @@ -118,21 +119,24 @@ private static void addBundle(final Object bundleWiring, final ClassLoader class 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 File 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, 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 File embeddedLocation = embedded != null ? getContentLocation(embedded) : null; + final File embeddedLocation = embedded != null + ? getContentLocation(embedded, classpathOrderOut.reflectionUtils) + : null; if (embeddedLocation != null) { classpathOrderOut.addClasspathEntry(embeddedLocation, classLoader, scanSpec, log); } @@ -158,17 +162,18 @@ public static void findClasspathOrder(final ClassLoader classLoader, final Class final ScanSpec scanSpec, final LogNode log) { // Get the wiring for the ClassLoader's bundle final Set bundles = new HashSet<>(); - final Object bundleWiring = ReflectionUtils.getFieldVal(classLoader, "m_wiring", false); + 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, 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 8971ea014..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,18 +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.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: @@ -65,7 +66,8 @@ private JBossClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "org.jboss.modules.ModuleClassLoader".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.jboss.modules.ModuleClassLoader"); } /** @@ -104,47 +106,140 @@ private static void handleResourceLoader(final Object resourceLoader, final Clas 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, scanSpec, 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; } } @@ -171,12 +266,14 @@ private static void handleRealModule(final Object module, final Set visi // 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 @@ -206,29 +303,34 @@ private static void handleRealModule(final Object module, final Set visi */ public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { - final Object module = ReflectionUtils.invokeMethod(classLoader, "getModule", false); - final Object callerModuleLoader = ReflectionUtils.invokeMethod(module, "getCallerModuleLoader", false); + 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); + 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); + 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 3c4802881..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,6 +28,9 @@ */ package nonapi.io.github.classgraph.classloaderhandler; +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; @@ -52,8 +55,10 @@ private JPMSClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "jdk.internal.loader.ClassLoaders$AppClassLoader".equals(classLoaderClass.getName()) - || "jdk.internal.loader.BuiltinClassLoader".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "jdk.internal.loader.ClassLoaders$AppClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "jdk.internal.loader.BuiltinClassLoader"); } /** @@ -68,9 +73,6 @@ public static boolean canHandle(final Class classLoaderClass, final LogNode l */ public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, final LogNode log) { - // Add JPMS classloaders into classloader order, so that they can be used for classloading - // (e.g. by ClassInfo#loadClass()). However, findClasspathOrder() below cannot actually find - // classpath element locations from JPMS classloaders, so the method body is blank. classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); classLoaderOrder.add(classLoader, log); } @@ -91,5 +93,13 @@ public static void findClasspathOrder(final ClassLoader classLoader, final Class 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 dc6a74f54..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,11 +30,11 @@ import java.io.File; +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. @@ -56,7 +56,8 @@ private OSGiDefaultClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader"); } /** @@ -89,12 +90,16 @@ public static void findClassLoaderOrder(final ClassLoader classLoader, final Cla */ public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { - final Object classpathManager = ReflectionUtils.invokeMethod(classLoader, "getClasspathManager", false); - final Object[] entries = (Object[]) ReflectionUtils.getFieldVal(classpathManager, "entries", false); + 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) { 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 9d26a0fcc..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,11 +28,11 @@ */ 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; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** ClassLoaderHandler that is used to test PARENT_LAST delegation order. */ class ParentLastDelegationOrderTestClassLoaderHandler implements ClassLoaderHandler { @@ -50,7 +50,8 @@ private ParentLastDelegationOrderTestClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "io.github.classgraph.issues.issue267.FakeRestartClassLoader".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "io.github.classgraph.issues.issue267.FakeRestartClassLoader"); } /** @@ -84,8 +85,8 @@ public static void findClassLoaderOrder(final ClassLoader classLoader, final Cla */ public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { - final String classpath = (String) ReflectionUtils.invokeMethod(classLoader, "getClasspath", - /* throwException = */ true); + 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 index 4714f16c2..e48ed2a77 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/PlexusClassWorldsClassRealmClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/PlexusClassWorldsClassRealmClassLoaderHandler.java @@ -30,11 +30,12 @@ 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; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * Handle the Plexus ClassWorlds ClassRealm ClassLoader. @@ -56,7 +57,8 @@ private PlexusClassWorldsClassRealmClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "org.codehaus.plexus.classworlds.realm.ClassRealm".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.codehaus.plexus.classworlds.realm.ClassRealm"); } /** @@ -66,8 +68,9 @@ public static boolean canHandle(final Class classLoaderClass, final LogNode l * the ClassRealm instance * @return true if classloader uses a parent-first strategy */ - private static boolean isParentFirstStrategy(final ClassLoader classRealmInstance) { - final Object strategy = ReflectionUtils.getFieldVal(classRealmInstance, "strategy", false); + 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") @@ -93,20 +96,21 @@ private static boolean isParentFirstStrategy(final ClassLoader classRealmInstanc public static void findClassLoaderOrder(final ClassLoader classRealm, final ClassLoaderOrder classLoaderOrder, final LogNode log) { // From ClassRealm#loadClassFromImport(String) -> getImportClassLoader(String) - final Object foreignImports = ReflectionUtils.getFieldVal(classRealm, "foreignImports", false); + 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) ReflectionUtils.invokeMethod(entry, - "getClassLoader", false); + 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); + final boolean isParentFirst = isParentFirstStrategy(classRealm, classLoaderOrder.reflectionUtils); // From ClassRealm#loadClassFromSelf(String) -> findLoadedClass(String) for self-first strategy if (!isParentFirst) { @@ -117,8 +121,8 @@ public static void findClassLoaderOrder(final ClassLoader classRealm, final Clas // 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) ReflectionUtils.invokeMethod(classRealm, - "getParentClassLoader", false); + final ClassLoader parentClassLoader = (ClassLoader) classLoaderOrder.reflectionUtils.invokeMethod(false, + classRealm, "getParentClassLoader"); classLoaderOrder.delegateTo(parentClassLoader, /* isParent = */ true, log); classLoaderOrder.delegateTo(classRealm.getParent(), /* isParent = */ true, 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 index a057642eb..9476d2115 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/QuarkusClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/QuarkusClassLoaderHandler.java @@ -28,14 +28,20 @@ */ 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; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * Extract classpath entries from the Quarkus ClassLoader. @@ -47,6 +53,18 @@ class QuarkusClassLoaderHandler implements ClassLoaderHandler { // 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. */ @@ -63,8 +81,9 @@ private QuarkusClassLoaderHandler() { * @return true, if classLoaderClass is the Quarkus RuntimeClassloader or QuarkusClassloader */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return RUNTIME_CLASSLOADER.equals(classLoaderClass.getName()) - || QUARKUS_CLASSLOADER.equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, RUNTIME_CLASSLOADER) + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, QUARKUS_CLASSLOADER) + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, RUNNER_CLASSLOADER); } /** @@ -103,33 +122,83 @@ public static void findClasspathOrder(final ClassLoader classLoader, final Class 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); } } - @SuppressWarnings("unchecked") private static void findClasspathOrderForQuarkusClassloader(final ClassLoader classLoader, final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { - for (final Object element : (Collection) ReflectionUtils.getFieldVal(classLoader, "elements", - false)) { + + final Collection elements = findQuarkusClassLoaderElements(classLoader, classpathOrder); + + for (final Object element : elements) { final String elementClassName = element.getClass().getName(); - if ("io.quarkus.bootstrap.classloading.JarClassPathElement".equals(elementClassName)) { - classpathOrder.addClasspathEntry(ReflectionUtils.getFieldVal(element, "file", false), classLoader, - scanSpec, log); - } else if ("io.quarkus.bootstrap.classloading.DirectoryClassPathElement".equals(elementClassName)) { - classpathOrder.addClasspathEntry(ReflectionUtils.getFieldVal(element, "root", false), classLoader, + 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) ReflectionUtils - .getFieldVal(classLoader, "applicationClassDirectories", false); + final Collection applicationClassDirectories = (Collection) classpathOrder.reflectionUtils + .getFieldVal(false, classLoader, "applicationClassDirectories"); if (applicationClassDirectories != null) { for (final Path path : applicationClassDirectories) { - classpathOrder.addClasspathEntryObject(path.toUri(), classLoader, scanSpec, log); + 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 ad5f92ebd..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,6 +28,7 @@ */ 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; @@ -55,8 +56,8 @@ private SpringBootRestartClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "org.springframework.boot.devtools.restart.classloader.RestartClassLoader" - .equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.springframework.boot.devtools.restart.classloader.RestartClassLoader"); } /** @@ -75,7 +76,7 @@ public static void findClassLoaderOrder(final ClassLoader classLoader, final Cla // classloader order first classLoaderOrder.add(classLoader, log); - // Finally delegate to the parent of the RestartClassLoader + // Delegate to the parent of the RestartClassLoader classLoaderOrder.delegateTo(classLoader.getParent(), /* isParent = */ true, log); } 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 22ff1682e..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,11 +31,12 @@ import java.io.File; import java.util.List; +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 { @@ -53,7 +54,8 @@ private TomcatWebappClassLoaderBaseHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "org.apache.catalina.loader.WebappClassLoaderBase".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "org.apache.catalina.loader.WebappClassLoaderBase"); } /** @@ -63,8 +65,8 @@ public static boolean canHandle(final Class classLoaderClass, final LogNode l * the {@link ClassLoader}. * @return true if this classloader delegates to its parent. */ - private static boolean isParentFirst(final ClassLoader classLoader) { - final Object delegateObject = ReflectionUtils.getFieldVal(classLoader, "delegate", false); + private static boolean isParentFirst(final ClassLoader classLoader, final ReflectionUtils reflectionUtils) { + final Object delegateObject = reflectionUtils.getFieldVal(false, classLoader, "delegate"); if (delegateObject != null) { return (boolean) delegateObject; } @@ -84,11 +86,22 @@ private static boolean isParentFirst(final ClassLoader classLoader) { */ public static void findClassLoaderOrder(final ClassLoader classLoader, final ClassLoaderOrder classLoaderOrder, final LogNode log) { - final boolean isParentFirst = isParentFirst(classLoader); + 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 @@ -111,38 +124,45 @@ public static void findClassLoaderOrder(final ClassLoader classLoader, final Cla 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); + 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 for (final List webResourceSetList : allResources) { // type WebResourceSet - // {DirResourceSet, FileResourceSet, JarResourceSet, JarWarResourceSet, EmptyResourceSet} + // {DirResourceSet, FileResourceSet, JarResourceSet, JarWarResourceSet, + // EmptyResourceSet} for (final Object webResourceSet : webResourceSetList) { if (webResourceSet != null) { // For DirResourceSet - final File file = (File) ReflectionUtils.invokeMethod(webResourceSet, "getFileBase", false); + final File file = (File) classpathOrder.reflectionUtils.invokeMethod(false, webResourceSet, + "getFileBase"); String base = file == null ? null : file.getPath(); if (base == null) { // For FileResourceSet - base = (String) ReflectionUtils.invokeMethod(webResourceSet, "getBase", false); + 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) ReflectionUtils.invokeMethod(webResourceSet, "getBaseUrlString", false); + // 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"); } 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); + // 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); @@ -153,8 +173,8 @@ public static void findClasspathOrder(final ClassLoader classLoader, final Class || 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); + 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), @@ -168,7 +188,7 @@ public static void findClasspathOrder(final ClassLoader classLoader, final Class } } // This may or may not duplicate the above - final Object urls = ReflectionUtils.invokeMethod(classLoader, "getURLs", false); + 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 bf7399876..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,6 +31,7 @@ import java.net.URL; import java.net.URLClassLoader; +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; @@ -52,7 +53,7 @@ private URLClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "java.net.URLClassLoader".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, "java.net.URLClassLoader"); } /** diff --git a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/UnoOneJarClassLoaderHandler.java b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/UnoOneJarClassLoaderHandler.java index 17fc15f60..570705e0b 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/UnoOneJarClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/UnoOneJarClassLoaderHandler.java @@ -28,11 +28,11 @@ */ 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; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** Extract classpath entries from the Uno-Jar's JarClassLoader and One-Jar's JarClassLoader. */ class UnoOneJarClassLoaderHandler implements ClassLoaderHandler { @@ -50,8 +50,10 @@ private UnoOneJarClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "com.needhamsoftware.unojar.JarClassLoader".equals(classLoaderClass.getName()) - || "com.simontuffs.onejar.JarClassLoader".equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "com.needhamsoftware.unojar.JarClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "com.simontuffs.onejar.JarClassLoader"); } /** @@ -87,7 +89,8 @@ public static void findClasspathOrder(final ClassLoader classLoader, final Class // For Uno-Jar: - final String unoJarOneJarPath = (String) ReflectionUtils.invokeMethod(classLoader, "getOneJarPath", false); + 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 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 50cffe710..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,11 +28,11 @@ */ 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; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** Extract classpath entries from the Weblogic ClassLoaders. */ class WeblogicClassLoaderHandler implements ClassLoaderHandler { @@ -50,13 +50,18 @@ private WeblogicClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "weblogic.utils.classloaders.ChangeAwareClassLoader".equals(classLoaderClass.getName()) - || "weblogic.utils.classloaders.GenericClassLoader".equals(classLoaderClass.getName()) - || "weblogic.utils.classloaders.FilteringClassLoader".equals(classLoaderClass.getName()) + 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. - || "weblogic.servlet.jsp.JspClassLoader".equals(classLoaderClass.getName()) - || "weblogic.servlet.jsp.TagFileClassLoader".equals(classLoaderClass.getName()); + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "weblogic.servlet.jsp.JspClassLoader") + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + "weblogic.servlet.jsp.TagFileClassLoader"); } /** @@ -90,10 +95,10 @@ public static void findClassLoaderOrder(final ClassLoader classLoader, final Cla public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { classpathOrder.addClasspathPathStr( // - (String) ReflectionUtils.invokeMethod(classLoader, "getFinderClassPath", false), classLoader, - scanSpec, log); + (String) classpathOrder.reflectionUtils.invokeMethod(false, classLoader, "getFinderClassPath"), + classLoader, scanSpec, log); classpathOrder.addClasspathPathStr( // - (String) ReflectionUtils.invokeMethod(classLoader, "getClassPath", false), classLoader, scanSpec, - log); + (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 835f5bff8..d47958beb 100644 --- a/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereLibertyClassLoaderHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/classloaderhandler/WebsphereLibertyClassLoaderHandler.java @@ -32,17 +32,17 @@ import java.io.File; import java.net.URL; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +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. @@ -76,8 +76,9 @@ private WebsphereLibertyClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return IBM_APP_CLASS_LOADER.equals(classLoaderClass.getName()) - || IBM_THREAD_CONTEXT_CLASS_LOADER.equals(classLoaderClass.getName()); + return ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, IBM_APP_CLASS_LOADER) + || ClassLoaderFinder.classIsOrExtendsOrImplements(classLoaderClass, + IBM_THREAD_CONTEXT_CLASS_LOADER); } /** @@ -108,55 +109,56 @@ public static void findClassLoaderOrder(final ClassLoader classLoader, final Cla * the containerClassLoader object * @return Collection of path objects as a {@link URL} or {@link String}. */ - private static Collection getPaths(final Object containerClassLoader) { + private static Collection getPaths(final Object containerClassLoader, + final ReflectionUtils reflectionUtils) { if (containerClassLoader == null) { - return Collections. emptyList(); + 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"); + 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(containerClassLoader, "container", false); + final Object container = reflectionUtils.getFieldVal(false, containerClassLoader, "container"); if (container == null) { - return Collections. emptyList(); + return Collections.emptyList(); } // Should be an instance of "com.ibm.wsspi.adaptable.module.Container". // Call "getURLs" to get its classpath. - urls = callGetUrls(container, "getURLs"); + urls = callGetUrls(container, "getURLs", reflectionUtils); if (urls != null && !urls.isEmpty()) { return urls; } // "getURLs" did not work, reverting to previous logic of introspection of the "delegate". - final Object delegate = ReflectionUtils.getFieldVal(container, "delegate", false); + final Object delegate = reflectionUtils.getFieldVal(false, container, "delegate"); if (delegate == null) { - return Collections. emptyList(); + 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 Arrays.asList((Object) 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 Collections. emptyList(); + 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 Arrays.asList((Object) file.getAbsolutePath()); + return Collections.singletonList((Object) file.getAbsolutePath()); } - return Collections. emptyList(); + return Collections.emptyList(); } /** @@ -171,11 +173,12 @@ private static Collection getPaths(final Object containerClassLoader) { * of the locations on disk that contribute to this container" */ @SuppressWarnings("unchecked") - private static Collection callGetUrls(final Object container, final String methodName) { + private static Collection callGetUrls(final Object container, final String methodName, + final ReflectionUtils reflectionUtils) { if (container != null) { try { - final Collection results = (Collection) ReflectionUtils.invokeMethod(container, - methodName, false); + final Collection results = (Collection) reflectionUtils.invokeMethod(false, + container, methodName); if (results != null && !results.isEmpty()) { final Collection allUrls = new HashSet<>(); for (final Object result : results) { @@ -196,7 +199,7 @@ private static Collection callGetUrls(final Object container, final Stri /* ignore */ } } - return Collections. emptyList(); + return Collections.emptyList(); } /** @@ -214,16 +217,17 @@ private static Collection callGetUrls(final Object container, final Stri 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) { // "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"); + final Collection paths = callGetUrls(smartClassPath, "getClassPath", + classpathOrder.reflectionUtils); if (!paths.isEmpty()) { for (final Object path : paths) { classpathOrder.addClasspathEntry(path, classLoader, scanSpec, log); @@ -231,11 +235,12 @@ public static void findClasspathOrder(final ClassLoader classLoader, final Class } else { // "getClassPath" didn't work... reverting to looping over "classPath" elements. @SuppressWarnings("unchecked") - final List classPathElements = (List) ReflectionUtils.getFieldVal(smartClassPath, - "classPath", false); + 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); + 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 79d18093a..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,11 +28,11 @@ */ 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; -import nonapi.io.github.classgraph.utils.ReflectionUtils; /** * Handle the WebSphere traditonal ClassLoaders. @@ -54,9 +54,12 @@ private WebsphereTraditionalClassLoaderHandler() { * @return true if this {@link ClassLoaderHandler} can handle the {@link ClassLoader}. */ public static boolean canHandle(final Class classLoaderClass, final LogNode log) { - return "com.ibm.ws.classloader.CompoundClassLoader".equals(classLoaderClass.getName()) - || "com.ibm.ws.classloader.ProtectionClassLoader".equals(classLoaderClass.getName()) - || "com.ibm.ws.bootstrap.ExtClassLoader".equals(classLoaderClass.getName()); + 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"); } /** @@ -89,7 +92,8 @@ public static void findClassLoaderOrder(final ClassLoader classLoader, final Cla */ public static void findClasspathOrder(final ClassLoader classLoader, final ClasspathOrder classpathOrder, final ScanSpec scanSpec, final LogNode log) { - final String classpath = (String) ReflectionUtils.invokeMethod(classLoader, "getClassPath", false); + 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 772f60b8a..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,26 +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 { - private static Class[] callStack; + ReflectionUtils reflectionUtils; /** * Constructor. */ - private CallStackReader() { - // Cannot be constructed + public CallStackReader(final ReflectionUtils reflectionUtils) { + this.reflectionUtils = reflectionUtils; } /** @@ -75,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) @@ -96,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 @@ -121,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; } @@ -141,67 +140,90 @@ private static Class[] getCallStackViaSecurityManager(final LogNode log) { * the log * @return The classes in the call stack. */ - static Class[] getClassContext(final LogNode log) { - if (callStack == null) { - // For JRE 9+, use StackWalker to get call stack. - // N.B. need to work around StackWalker bug fixed in JDK 13, and backported to 12.0.2 and 11.0.4 - // (probably introduced in JDK 9, when StackWalker was introduced): + 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 - if ((VersionFinder.JAVA_MAJOR_VERSION == 11 - && (VersionFinder.JAVA_MINOR_VERSION >= 1 || VersionFinder.JAVA_SUB_VERSION >= 4) - && !VersionFinder.JAVA_IS_EA_VERSION) - || (VersionFinder.JAVA_MAJOR_VERSION == 12 - && (VersionFinder.JAVA_MINOR_VERSION >= 1 || VersionFinder.JAVA_SUB_VERSION >= 2) - && !VersionFinder.JAVA_IS_EA_VERSION) - || (VersionFinder.JAVA_MAJOR_VERSION == 13 && !VersionFinder.JAVA_IS_EA_VERSION) - || VersionFinder.JAVA_MAJOR_VERSION > 13) { - // Invoke with doPrivileged -- see: - // http://mail.openjdk.java.net/pipermail/jigsaw-dev/2018-October/013974.html - callStack = AccessController.doPrivileged(new PrivilegedAction[]>() { + // -- fall through + } else { + // Get the stack via StackWalker. + // Invoke with doPrivileged -- see: + // http://mail.openjdk.java.net/pipermail/jigsaw-dev/2018-October/013974.html + try { + callStack = reflectionUtils.doPrivileged(new Callable[]>() { @Override - public Class[] run() { + 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 (callStack == null || callStack.length == 0) { - callStack = AccessController.doPrivileged(new PrivilegedAction[]>() { + // 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[] run() { + 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 (callStack == null || callStack.length == 0) { - StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); - if (stackTrace == null || stackTrace.length == 0) { - try { - throw new Exception(); - } catch (final Exception e) { - stackTrace = e.getStackTrace(); - } - } - final List> stackClassesList = new ArrayList<>(); - for (final StackTraceElement elt : stackTrace) { - try { - stackClassesList.add(Class.forName(elt.getClassName())); - } catch (final ClassNotFoundException | LinkageError ignored) { - // Ignored - } + // As a fallback, use getStackTrace() to try to get the call stack + if (callStack == null || callStack.length == 0) { + StackTraceElement[] stackTrace = null; + try { + 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 (!stackClassesList.isEmpty()) { - callStack = stackClassesList.toArray(new Class[0]); - } else { - // Last-ditch effort -- include just this class in the call stack - callStack = 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]); + } } + + // 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 index 4368bef07..95a958a06 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderFinder.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderFinder.java @@ -30,6 +30,7 @@ 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; @@ -51,6 +52,28 @@ public ClassLoader[] getContextClassLoaders() { // ------------------------------------------------------------------------------------------------------------- + /** 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. * @@ -59,7 +82,7 @@ public ClassLoader[] getContextClassLoaders() { * @param log * The log. */ - ClassLoaderFinder(final ScanSpec scanSpec, final LogNode log) { + ClassLoaderFinder(final ScanSpec scanSpec, final ReflectionUtils reflectionUtils, final LogNode log) { LinkedHashSet classLoadersUnique; LogNode classLoadersFoundLog; if (scanSpec.overrideClassLoaders == null) { @@ -101,7 +124,7 @@ public ClassLoader[] getContextClassLoaders() { // Find classloaders for classes on callstack, in case any were missed try { - final Class[] callStack = CallStackReader.getClassContext(log); + 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) { diff --git a/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderOrder.java b/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderOrder.java index 520d57d70..2394178a5 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderOrder.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/ClassLoaderOrder.java @@ -28,58 +28,65 @@ */ package nonapi.io.github.classgraph.classpath; -import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; +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.ClassLoaderHandler; 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 List> classLoaderOrder = new ArrayList<>(); + 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. */ - private final Set added = new HashSet<>(); + // 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 = new HashSet<>(); + 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 = new HashSet<>(); - - /** A map from {@link ClassLoader} to {@link ClassLoaderHandlerRegistryEntry}. */ - private final Map classLoaderToClassLoaderHandlerRegistryEntry = // - new HashMap<>(); + 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 classLoaderOrder; + public List>> getClassLoaderOrder() { + return new ArrayList<>(classLoaderOrder.entrySet()); } /** @@ -91,46 +98,26 @@ public Set getAllParentClassLoaders() { return allParentClassLoaders; } - /** - * Find the {@link ClassLoaderHandler} that can handle a given {@link ClassLoader} instance. - * - * @param classLoader - * the {@link ClassLoader}. - * @param log - * the log - * @return the {@link ClassLoaderHandlerRegistryEntry} for the {@link ClassLoader}. - */ - private ClassLoaderHandlerRegistryEntry getRegistryEntry(final ClassLoader classLoader, final LogNode log) { - ClassLoaderHandlerRegistryEntry entry = classLoaderToClassLoaderHandlerRegistryEntry.get(classLoader); - if (entry == null) { - // Try all superclasses of classloader in turn - for (Class currClassLoaderClass = classLoader.getClass(); // - currClassLoaderClass != Object.class && currClassLoaderClass != null; // - currClassLoaderClass = currClassLoaderClass.getSuperclass()) { - // Find a ClassLoaderHandler that can handle the ClassLoader - for (final ClassLoaderHandlerRegistryEntry ent : ClassLoaderHandlerRegistry.CLASS_LOADER_HANDLERS) { - if (ent.canHandle(currClassLoaderClass, log)) { - // This ClassLoaderHandler can handle the ClassLoader class, or one of its superclasses - entry = ent; - break; - } - } - if (entry != null) { - // Don't iterate to next superclass if a matching ClassLoaderHandler was found - break; - } - } - if (entry == null) { - // Use fallback handler - entry = ClassLoaderHandlerRegistry.FALLBACK_HANDLER; + /** 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; } - classLoaderToClassLoaderHandlerRegistryEntry.put(classLoader, entry); } - return entry; + if (!matched) { + ents.add(ClassLoaderHandlerRegistry.FALLBACK_HANDLER); + } + return ents; } /** - * Add a {@link ClassLoader} to the {@link ClassLoader} order at the current position. + * Add a {@link ClassLoader} to the ClassLoader order at the current position. * * @param classLoader * the class loader @@ -142,10 +129,7 @@ public void add(final ClassLoader classLoader, final LogNode log) { return; } if (added.add(classLoader)) { - final ClassLoaderHandlerRegistryEntry entry = getRegistryEntry(classLoader, log); - if (entry != null) { - classLoaderOrder.add(new SimpleEntry<>(classLoader, entry)); - } + classLoaderOrder.put(classLoader, getClassLoaderHandlerRegistryEntries(classLoader, log)); } } @@ -171,10 +155,13 @@ public void delegateTo(final ClassLoader classLoader, final boolean isParent, fi } // Don't delegate to a classloader twice if (delegatedTo.add(classLoader)) { - // Find ClassLoaderHandlerRegistryEntry for this classloader - final ClassLoaderHandlerRegistryEntry entry = getRegistryEntry(classLoader, log); - // Delegate to this classloader, by recursing to that classloader to get its classloader order - entry.findClassLoaderOrder(classLoader, this, log); + 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 16c727158..09e3581bb 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathFinder.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/ClasspathFinder.java @@ -36,6 +36,7 @@ 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; @@ -114,56 +115,66 @@ public ClassGraphClassLoader getDelegateClassGraphClassLoader() { * @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"); + // 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 module scanning. If not, disable module scanning, since only the provided - // classloader(s) should be scanned. (#382) - boolean scanModules; + // If so, need to enable non-system module scanning. + boolean scanNonSystemModules; if (scanSpec.overrideClasspath != null) { - // Don't scan modules if classpath is overridden - scanModules = false; + // Don't scan non-system modules if classpath is overridden + scanNonSystemModules = false; } else if (scanSpec.overrideClassLoaders != null) { - // If classloaders are overridden, only scan modules if an override classloader is a JPMS + // If classloaders are overridden, only scan non-system modules if an override classloader is a JPMS // AppClassLoader or PlatformClassLoader - scanModules = false; + 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 (classLoaderClassName.equals("jdk.internal.loader.ClassLoaders$AppClassLoader")) { - scanModules = true; - } else if (classLoaderClassName.equals("jdk.internal.loader.ClassLoaders$PlatformClassLoader")) { - scanModules = true; - // The platform classloader was passed in, so specifically enable system module scanning - if (!scanSpec.enableSystemJarsAndModules) { - if (classpathFinderLog != null) { - classpathFinderLog.log("overrideClassLoaders() was called with an instance of " - + "jdk.internal.loader.ClassLoaders$PlatformClassLoader, which is a system " - + "classloader, so enableSystemJarsAndModules() was called automatically"); - } - scanSpec.enableSystemJarsAndModules = true; + 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 modules + // If classloaders are not overridden and classpath is not overridden, only scan non-system modules // if module scanning is enabled - scanModules = scanSpec.scanModules; + scanNonSystemModules = scanSpec.scanModules; } - moduleFinder = scanModules - ? new ModuleFinder(CallStackReader.getClassContext(classpathFinderLog), scanSpec, + // 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); + classpathOrder = new ClasspathOrder(scanSpec, reflectionUtils); // Only look for environment classloaders if classpath and classloaders are not overridden final ClassLoaderFinder classLoaderFinder = scanSpec.overrideClasspath == null - && scanSpec.overrideClassLoaders == null ? new ClassLoaderFinder(scanSpec, classpathFinderLog) + && scanSpec.overrideClassLoaders == null + ? new ClassLoaderFinder(scanSpec, reflectionUtils, classpathFinderLog) : null; final ClassLoader[] contextClassLoaders = classLoaderFinder == null ? new ClassLoader[0] : classLoaderFinder.getContextClassLoaders(); @@ -187,9 +198,10 @@ public ClasspathFinder(final ScanSpec scanSpec, final LogNode log) { + "context classloader"); } classLoaderOrderRespectingParentDelegation = contextClassLoaders; + } - } else if (scanSpec.overrideClassLoaders == null) { - // If system jars are not rejected, add JRE rt.jar to the beginning of the classpath + // 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 @@ -233,7 +245,7 @@ public ClasspathFinder(final ScanSpec scanSpec, final LogNode log) { // 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(); + final ClassLoaderOrder classLoaderOrder = new ClassLoaderOrder(reflectionUtils); final ClassLoader[] origClassLoaderOrder = scanSpec.overrideClassLoaders != null ? scanSpec.overrideClassLoaders.toArray(new ClassLoader[0]) : contextClassLoaders; @@ -250,27 +262,30 @@ public ClasspathFinder(final ScanSpec scanSpec, final LogNode log) { 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 + for (final Entry> ent : classLoaderOrder .getClassLoaderOrder()) { final ClassLoader classLoader = ent.getKey(); - 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 + " 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()); - } - // See if a previous scan's ClassGraphClassLoader should be delegated to first - if (classLoader instanceof ClassGraphClassLoader) { - delegateClassGraphClassLoader = (ClassGraphClassLoader) classLoader; + 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()); + } + // See if a previous scan's ClassGraphClassLoader should be delegated to first + if (classLoader instanceof ClassGraphClassLoader) { + delegateClassGraphClassLoader = (ClassGraphClassLoader) classLoader; + } } } @@ -283,8 +298,9 @@ public ClasspathFinder(final ScanSpec scanSpec, final LogNode log) { // 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 ((!scanSpec.ignoreParentClassLoaders && scanSpec.overrideClassLoaders == null - && scanSpec.overrideClasspath == null) + 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) { @@ -292,7 +308,7 @@ public ClasspathFinder(final ScanSpec scanSpec, final LogNode log) { : 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.CURR_DIR_PATH, + final String pathElementResolved = FastPathResolver.resolve(FileUtils.currDirPath(), pathElement); classpathOrder.addClasspathEntry(pathElementResolved, defaultClassLoader, scanSpec, sysPropLog); } 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 97650ffb4..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,6 +29,7 @@ package nonapi.io.github.classgraph.classpath; import java.io.File; +import java.io.IOError; import java.lang.reflect.Array; import java.net.MalformedURLException; import java.net.URI; @@ -42,10 +43,13 @@ import java.util.List; 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 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; @@ -57,15 +61,20 @@ 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. Keys are instances of {@link String} or {@link URL}. */ - private final List order = new ArrayList<>(); + 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)); @@ -75,71 +84,45 @@ public class ClasspathOrder { /** * A classpath element and the {@link ClassLoader} it was obtained from. */ - public static class ClasspathElementAndClassLoader { - /** - * The classpath element root (a {@link String} path, {@link Path}, {@link URL} or {@link URI}). - */ - public final Object classpathElementRoot; - - /** The classpath element package root, prefix, e.g. "BOOT-INF/classes" or "". */ - public final String dirOrPathPackageRoot; + 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 for directory or {@link Path} classpath entries. - * - * @param classpathElementRoot - * the classpath element root (a {@link String} path, {@link Path}, {@link URL} or {@link URI}). - * @param dirOrPathPackageRoot - * the classpath element package root prefix, e.g. "BOOT-INF/classes" or "". Only used for - * directory or {@link Path} classpath entries. - * @param classLoader - * the classloader the classpath element was obtained from. - */ - public ClasspathElementAndClassLoader(final Object classpathElementRoot, final String dirOrPathPackageRoot, - final ClassLoader classLoader) { - this.classpathElementRoot = classpathElementRoot; - this.dirOrPathPackageRoot = dirOrPathPackageRoot; - this.classLoader = classLoader; - } - /** * Constructor. * - * @param classpathElementRoot - * the classpath element root (a {@link String} or {@link URL} or {@link Path}). + * @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 ClasspathElementAndClassLoader(final Object classpathElementRoot, final ClassLoader classLoader) { - this.classpathElementRoot = classpathElementRoot; - this.dirOrPathPackageRoot = ""; + public ClasspathEntry(final Object classpathEntryObj, final ClassLoader classLoader) { + this.classpathEntryObj = classpathEntryObj; this.classLoader = classLoader; } @Override public int hashCode() { - return Objects.hash(classpathElementRoot, dirOrPathPackageRoot, classLoader); + return Objects.hash(classpathEntryObj); } @Override public boolean equals(final Object obj) { if (obj == this) { return true; - } else if (!(obj instanceof ClasspathElementAndClassLoader)) { + } else if (!(obj instanceof ClasspathEntry)) { return false; } - final ClasspathElementAndClassLoader other = (ClasspathElementAndClassLoader) obj; - return Objects.equals(this.dirOrPathPackageRoot, other.dirOrPathPackageRoot) - && Objects.equals(this.classpathElementRoot, other.classpathElementRoot) - && Objects.equals(this.classLoader, other.classLoader); + final ClasspathEntry other = (ClasspathEntry) obj; + return Objects.equals(this.classpathEntryObj, other.classpathEntryObj); } @Override public String toString() { - return classpathElementRoot + " [" + classLoader + "]"; + return classpathEntryObj + " [" + classLoader + "]"; } } @@ -149,8 +132,9 @@ public String toString() { * @param scanSpec * the scan spec */ - ClasspathOrder(final ScanSpec scanSpec) { + ClasspathOrder(final ScanSpec scanSpec, final ReflectionUtils reflectionUtils) { this.scanSpec = scanSpec; + this.reflectionUtils = reflectionUtils; } /** @@ -158,7 +142,7 @@ public String toString() { * * @return the classpath order. */ - public List getOrder() { + public List getOrder() { return order; } @@ -200,14 +184,14 @@ private boolean filter(final URL classpathElementURL, final String classpathElem * * @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 ClasspathElementAndClassLoader(pathEntry, classLoader)); + order.add(new ClasspathEntry(pathEntry, classLoader)); return true; } return false; @@ -253,17 +237,24 @@ private boolean addClasspathEntry(final Object pathElement, final String pathEle // For File, just use path string : pathElementStrWithoutSuffix; } catch (MalformedURLException | URISyntaxException | InvalidPathException e) { - return false; + 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 ClasspathElementAndClassLoader(pathElementWithoutSuffix, classLoader)); + order.add(new ClasspathEntry(pathElementWithoutSuffix, classLoader)); return true; } } else { - final String pathElementStrResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, + final String pathElementStrResolved = FastPathResolver.resolve(FileUtils.currDirPath(), pathElementStrWithoutSuffix); if (scanSpec.overrideClasspath == null // && (SystemJarFinder.getJreLibOrExtJars().contains(pathElementStrResolved) @@ -273,7 +264,7 @@ private boolean addClasspathEntry(final Object pathElement, final String pathEle return false; } if (classpathEntryUniqueResolvedPaths.add(pathElementStrResolved)) { - order.add(new ClasspathElementAndClassLoader(pathElementStrResolved, classLoader)); + order.add(new ClasspathEntry(pathElementStrResolved, classLoader)); return true; } } @@ -301,36 +292,78 @@ public boolean addClasspathEntry(final Object pathElement, final ClassLoader cla if (pathElement == null) { return false; } - // Path objects have to be converted to URIs before calling .toString(), otherwise scheme is dropped - String pathElementStr = pathElement instanceof Path ? ((Path) pathElement).toUri().toString() - : pathElement.toString(); - pathElementStr = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, pathElementStr); + 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(); + } + } catch (IOError | SecurityException e) { + pathElementStr = pathElement.toString(); + } + } else { + pathElementStr = pathElement.toString(); + } + pathElementStr = FastPathResolver.resolve(FileUtils.currDirPath(), pathElementStr); if (pathElementStr.isEmpty()) { return false; } - URL pathElementURL; - 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; - if (pathElementURL == null) { - // Fallback -- call toString() on the path element, then try converting to a URL via File - final String pathElementToStr = pathElement.toString(); + 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 = new File(pathElementToStr).toURI().toURL(); - } catch (final MalformedURLException e) { - // Final fallback -- try prepending "file:" to create a URL - pathElementURL = new URL("file:" + pathElementToStr); + 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 (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 + } + } + } + } + if (pathElementURL == null) { + if (log != null) { + log.log("Failed to convert classpath element to URL: " + pathElement); + } } } - } catch (final MalformedURLException e1) { - if (log != null) { - log.log("Cannot convert to URL: " + pathElement); - } - pathElementURL = null; } - if (pathElement instanceof URL || pathElement instanceof URI || pathElement instanceof File + if (pathElementURL != null || pathElement instanceof URI || pathElement instanceof File || pathElement instanceof Path) { if (!filter(pathElementURL, pathElementStr)) { if (log != null) { @@ -342,7 +375,7 @@ public boolean addClasspathEntry(final Object pathElement, final ClassLoader cla // 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 - : pathElement instanceof Path || pathElement instanceof URI ? pathElementURL : pathElement; + : pathElementURL != null ? pathElementURL : pathElement; if (addClasspathEntry(classpathElementObj, pathElementStr, classLoader, scanSpec)) { if (log != null) { log.log("Found classpath element: " + pathElementStr); @@ -354,113 +387,128 @@ public boolean addClasspathEntry(final Object pathElement, final ClassLoader cla } return false; } - } else { - // Check for wildcard path element (allowable for local classpaths as of JDK 6) - if (pathElementStr.endsWith("*")) { - if (pathElementStr.length() == 1 || // - (pathElementStr.length() > 2 && pathElementStr.charAt(pathElementStr.length() - 1) == '*' - && (pathElementStr.charAt(pathElementStr.length() - 2) == File.separatorChar - || (File.separatorChar != '/' - && pathElementStr.charAt(pathElementStr.length() - 2) == '/')))) { - // Apply classpath element filters, if any - final String baseDirPath = pathElementStr.length() == 1 ? "" - : pathElementStr.substring(0, pathElementStr.length() - 2); - final String baseDirPathResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, - 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; - } + } + 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); + // 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: " + 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 false; } - if (!baseDir.isDirectory()) { + } + return true; + } else { + return false; + } + } else { + // Non-wildcarded (standard) classpath element + if (pathElementStr.indexOf('*') >= 0) { + if (log != null) { + log.log("Wildcard classpath elements can only end with a suffix of \"/*\", " + + "can't use globs elsewhere in the path: " + pathElementStr); + } + return false; + } + final String pathElementResolved = FastPathResolver.resolve(FileUtils.currDirPath(), pathElementStr); + if (!filter(pathElementURL, pathElementStr) || (!pathElementResolved.equals(pathElementStr) + && !filter(pathElementURL, pathElementResolved))) { + if (log != null) { + 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("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: " + 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.CURR_DIR_PATH, 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)); - } - } - } + 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; } - } 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: " + pathElementStr); - } - return false; + } catch (final Exception e) { + // Fall through } - } else { - // Non-wildcarded (standard) classpath element - final String pathElementResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, - pathElementStr); - if (!filter(pathElementURL, pathElementStr) || (!pathElementResolved.equals(pathElementStr) - && !filter(pathElementURL, pathElementResolved))) { - if (log != null) { - log.log("Classpath element did not match filter criterion, skipping: " + pathElementStr - + (pathElementStr.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); - } - return false; + } + if (addClasspathEntry(pathElementResolved, pathElementResolved, classLoader, scanSpec)) { + if (log != null) { + log.log("Found classpath element: " + pathElementStr + + (pathElementStr.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); } - 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: " + pathElementStr - + (pathElementStr.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); - } - return false; + return true; + } else { + if (log != null) { + log.log("Ignoring duplicate classpath element: " + pathElementStr + + (pathElementStr.equals(pathElementResolved) ? "" : " -> " + pathElementResolved)); } + return false; } } } diff --git a/src/main/java/nonapi/io/github/classgraph/classpath/ModuleFinder.java b/src/main/java/nonapi/io/github/classgraph/classpath/ModuleFinder.java index 6013e8b53..8edc7599d 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/ModuleFinder.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/ModuleFinder.java @@ -38,10 +38,10 @@ import java.util.Set; import io.github.classgraph.ModuleRef; +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 visible modules. */ public class ModuleFinder { @@ -54,6 +54,8 @@ public class ModuleFinder { /** If true, must forcibly scan {@code java.class.path}, since there was an anonymous module layer. */ private boolean forceScanJavaClassPath; + private final ReflectionUtils reflectionUtils; + // ------------------------------------------------------------------------------------------------------------- /** @@ -104,14 +106,14 @@ public boolean forceScanJavaClassPath() { * @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) { @@ -133,7 +135,7 @@ private static void findLayerOrder(final Object /* ModuleLayer */ layer, * the log * @return the list */ - private static List findModuleRefs(final LinkedHashSet layers, final ScanSpec scanSpec, + private List findModuleRefs(final LinkedHashSet layers, final ScanSpec scanSpec, final LogNode log) { if (layers.isEmpty()) { return Collections.emptyList(); @@ -172,21 +174,21 @@ private static List findModuleRefs(final LinkedHashSet layers 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); @@ -210,23 +212,25 @@ private static List findModuleRefs(final LinkedHashSet layers * 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 List findModuleRefsFromCallstack(final Class[] callStack, final ScanSpec scanSpec, - final LogNode log) { + 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(stackFrameClass, "getModule", - /* throwException = */ false); + final Object /* Module */ module = reflectionUtils.invokeMethod(/* throwException = */ false, + stackFrameClass, "getModule"); if (module != null) { - final Object /* ModuleLayer */ layer = ReflectionUtils.invokeMethod(module, "getLayer", - /* throwException = */ true); + final Object /* ModuleLayer */ layer = reflectionUtils.invokeMethod(/* throwException = */ true, + module, "getLayer"); if (layer != null) { layers.add(layer); - } else { + } 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; @@ -242,11 +246,11 @@ private List findModuleRefsFromCallstack(final Class[] callStack, // 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 { + } 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.) @@ -265,45 +269,52 @@ private List findModuleRefsFromCallstack(final Class[] callStack, * the callstack. * @param scanSpec * The scan spec. + * @param scanNonSystemModules + * whether to scan unnamed and non-system modules + * @param scanSystemModules + * whether to scan system modules * @param log * The log. */ - public ModuleFinder(final Class[] callStack, final ScanSpec scanSpec, final LogNode log) { - if (scanSpec.scanModules) { - // 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, log); - } - } else { - if (log != null) { - final LogNode subLog = log.log("Overriding module layers"); - for (final Object moduleLayer : scanSpec.overrideModuleLayers) { - subLog.log(moduleLayer.toString()); - } + public ModuleFinder(final Class[] callStack, final ScanSpec scanSpec, final boolean scanNonSystemModules, + final boolean scanSystemModules, final ReflectionUtils reflectionUtils, final LogNode log) { + this.reflectionUtils = reflectionUtils; + + // 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); + } + } else { + if (log != null) { + final LogNode subLog = log.log("Overriding module layers"); + for (final Object moduleLayer : scanSpec.overrideModuleLayers) { + subLog.log(moduleLayer.toString()); } - 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) { - if (moduleRef.isSystemModule()) { - systemModuleRefs.add(moduleRef); - } else { - nonSystemModuleRefs.add(moduleRef); - } + 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 if (!isSystemModule && scanNonSystemModules) { + nonSystemModuleRefs.add(moduleRef); } } } - // Log any identified modules - if (log != null) { - final LogNode sysSubLog = log.log("Found system modules:"); + } + // Log any identified modules + if (log != null) { + if (scanSystemModules) { + final LogNode sysSubLog = log.log("System modules found:"); if (systemModuleRefs != null && !systemModuleRefs.isEmpty()) { for (final ModuleRef moduleRef : systemModuleRefs) { sysSubLog.log(moduleRef.toString()); @@ -311,7 +322,11 @@ public ModuleFinder(final Class[] callStack, final ScanSpec scanSpec, final L } else { sysSubLog.log("[None]"); } - final LogNode nonSysSubLog = log.log("Found non-system modules:"); + } else { + log.log("Scanning of system modules is not enabled"); + } + 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()); @@ -319,10 +334,8 @@ public ModuleFinder(final Class[] callStack, final ScanSpec scanSpec, final L } else { nonSysSubLog.log("[None]"); } - } - } else { - if (log != null) { - log.log("Module scanning is disabled, because classloaders or classpath was overridden"); + } else { + log.log("Scanning of non-system modules is not enabled"); } } } 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 be2116d8e..3e51b6478 100644 --- a/src/main/java/nonapi/io/github/classgraph/classpath/SystemJarFinder.java +++ b/src/main/java/nonapi/io/github/classgraph/classpath/SystemJarFinder.java @@ -70,7 +70,7 @@ private static boolean addJREPath(final File dir) { for (final File file : dirFiles) { final String filePath = file.getPath(); if (filePath.endsWith(".jar")) { - final String jarPathResolved = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, filePath); + final String jarPathResolved = FastPathResolver.resolve(FileUtils.currDirPath(), filePath); if (jarPathResolved.endsWith("/rt.jar")) { RT_JARS.add(jarPathResolved); } else { @@ -81,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) { 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 808f0619c..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; /** @@ -36,7 +37,6 @@ * @author Johno Crawford (johno@sulake.com) */ public class SimpleThreadFactory implements java.util.concurrent.ThreadFactory { - /** The thread name prefix. */ private final String threadNamePrefix; @@ -68,10 +68,22 @@ public class SimpleThreadFactory implements java.util.concurrent.ThreadFactory { */ @Override public Thread newThread(final Runnable runnable) { - final SecurityManager s = System.getSecurityManager(); + // 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( - s != null ? s.getThreadGroup() : new ThreadGroup("ClassGraph-thread-group"), runnable, - threadNamePrefix + threadIdx.getAndIncrement()); + 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 3294337b8..61fdb4014 100644 --- a/src/main/java/nonapi/io/github/classgraph/concurrency/SingletonMap.java +++ b/src/main/java/nonapi/io/github/classgraph/concurrency/SingletonMap.java @@ -74,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); + } + } + // ------------------------------------------------------------------------------------------------------------- /** @@ -143,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 @@ -154,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 @@ -166,8 +206,11 @@ 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; @@ -186,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) { @@ -204,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. * 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 29d9aa99e..60de7e1e6 100644 --- a/src/main/java/nonapi/io/github/classgraph/concurrency/WorkQueue.java +++ b/src/main/java/nonapi/io/github/classgraph/concurrency/WorkQueue.java @@ -229,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 08936bed6..11afcfbfd 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/FastZipEntry.java +++ b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/FastZipEntry.java @@ -111,7 +111,7 @@ public class FastZipEntry implements Comparable { FastZipEntry(final LogicalZipFile parentLogicalZipFile, final long locHeaderPos, final String entryName, final boolean isDeflated, final long compressedSize, final long uncompressedSize, final long lastModifiedTimeMillis, final int lastModifiedTimeMSDOS, final int lastModifiedDateMSDOS, - final int fileAttributes) { + final int fileAttributes, final boolean enableMultiReleaseVersions) { this.parentLogicalZipFile = parentLogicalZipFile; this.locHeaderPos = locHeaderPos; this.entryName = entryName; @@ -159,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: 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 8d3a96d99..007224d6f 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/LogicalZipFile.java +++ b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/LogicalZipFile.java @@ -83,6 +83,9 @@ public class LogicalZipFile extends ZipFileSlice { /** 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/"}. */ @@ -148,9 +151,10 @@ public class LogicalZipFile extends ZipFileSlice { * @throws InterruptedException * if the thread was interrupted. */ - LogicalZipFile(final ZipFileSlice zipFileSlice, final NestedJarHandler nestedJarHandler, final LogNode log) - throws IOException, InterruptedException { + LogicalZipFile(final ZipFileSlice zipFileSlice, final NestedJarHandler nestedJarHandler, final LogNode log, + final boolean enableMultiReleaseVersions) throws IOException, InterruptedException { super(zipFileSlice); + this.enableMultiReleaseVersions = enableMultiReleaseVersions; readCentralDirectory(nestedJarHandler, log); } @@ -482,10 +486,6 @@ private void readCentralDirectory(final NestedJarHandler nestedJarHandler, final throw new IOException("Multi-disk jarfiles not supported: " + getPath()); } long cenSize = reader.readUnsignedInt(eocdPos + 12); - if (cenSize > eocdPos) { - throw new IOException( - "Central directory size out of range: " + cenSize + " vs. " + eocdPos + ": " + getPath()); - } long cenOff = reader.readUnsignedInt(eocdPos + 16); long cenPos = eocdPos - cenSize; @@ -532,6 +532,11 @@ private void readCentralDirectory(final NestedJarHandler nestedJarHandler, final } } + if (cenSize > eocdPos) { + throw new IOException( + "Central directory size out of range: " + cenSize + " vs. " + eocdPos + ": " + getPath()); + } + // Get offset of first local file header final long locPos = cenPos - cenOff; if (locPos < 0) { @@ -780,7 +785,7 @@ private void readCentralDirectory(final NestedJarHandler nestedJarHandler, final // Add zip entry final FastZipEntry entry = new FastZipEntry(this, locHeaderPos, entryNameSanitized, isDeflated, compressedSize, uncompressedSize, lastModifiedMillis, lastModifiedTimeMSDOS, - lastModifiedDateMSDOS, fileAttributes); + lastModifiedDateMSDOS, fileAttributes, enableMultiReleaseVersions); entries.add(entry); // Record manifest entry 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 6abefd569..2bad6e807 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/NestedJarHandler.java +++ b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/NestedJarHandler.java @@ -35,6 +35,7 @@ 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; @@ -71,6 +72,7 @@ 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; @@ -82,6 +84,8 @@ public class NestedJarHandler { /** The {@link 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 * that the {@link RandomAccessFile} and {@link FileChannel} for any given zipfile is opened only once. @@ -106,7 +110,8 @@ public ZipFileSlice newInstance(final FastZipEntry childZipEntry, final LogNode throws IOException, InterruptedException { ZipFileSlice childZipEntrySlice; if (!childZipEntry.isDeflated) { - // The child zip entry is a stored nested zipfile -- wrap it in a new ZipFileSlice. + // 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); @@ -119,7 +124,8 @@ public ZipFileSlice newInstance(final FastZipEntry childZipEntry, final LogNode + childZipEntry.uncompressedSize); } - // Read the InputStream for the child zip entry to a RAM buffer, or spill to disk if it's too large + // 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 @@ -134,14 +140,17 @@ public ZipFileSlice newInstance(final FastZipEntry childZipEntry, final LogNode } }; - /** 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 { // Read the central directory for the zipfile - return new LogicalZipFile(zipFileSlice, NestedJarHandler.this, log); + return new LogicalZipFile(zipFileSlice, NestedJarHandler.this, log, + scanSpec.enableMultiReleaseVersions); } }; @@ -149,7 +158,7 @@ public LogicalZipFile newInstance(final ZipFileSlice zipFileSlice, final LogNode * 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 @@ -158,11 +167,13 @@ public Entry newInstance(final String nestedJarPathRaw, 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://" or "https://" or any other URI/URL scheme, - // download the jar to a temp file or to a ByteBuffer in RAM. ("jar:" and "file:" + // 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; @@ -170,7 +181,8 @@ public Entry newInstance(final String nestedJarPathRaw, 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 + // 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: " @@ -187,10 +199,10 @@ public Entry newInstance(final String nestedJarPathRaw, 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 e) { + } catch (final NullSingletonException | NewInstanceException e) { // If getting PhysicalZipFile failed, re-wrap in IOException - throw new IOException( - "Could not get PhysicalZipFile for path " + nestedJarPath + " : " + e); + 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( @@ -205,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 @@ -218,9 +232,12 @@ public Entry newInstance(final String nestedJarPathRaw, 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 { @@ -228,27 +245,33 @@ 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 + // 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 + // 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). @@ -260,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) { @@ -299,10 +323,14 @@ public Entry newInstance(final String nestedJarPathRaw, "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 @@ -312,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 @@ -326,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 @@ -334,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>() { @@ -393,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; } // ------------------------------------------------------------------------------------------------------------- @@ -453,12 +489,8 @@ public File makeTempFile(final String filePathBase, final boolean onlyUseLeafnam * If the temporary file is inaccessible. */ void removeTempFile(final File tempFile) throws IOException, SecurityException { - if (tempFiles.contains(tempFile)) { - try { - Files.delete(tempFile.toPath()); - } finally { - tempFiles.remove(tempFile); - } + if (tempFiles.remove(tempFile)) { + Files.delete(tempFile.toPath()); } else { throw new IOException("Not a temp file: " + tempFile); } @@ -513,15 +545,16 @@ private PhysicalZipFile downloadJarFromURL(final String jarURL, final LogNode lo } catch (final MalformedURLException e1) { try { url = new URI(jarURL).toURL(); - } catch (final URISyntaxException e2) { + } catch (final MalformedURLException | IllegalArgumentException | URISyntaxException e2) { throw new IOException("Could not parse URL: " + jarURL); } } 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 + // 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 @@ -531,48 +564,48 @@ private PhysicalZipFile downloadJarFromURL(final String jarURL, final LogNode lo } // Wrap Path in PhysicalZipFile and return it return new PhysicalZipFile(path, this, log); - } catch (final URISyntaxException e) { - throw new IOException("Could not convert URL to URI: " + url); + } 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 } } - - final URLConnection conn = url.openConnection(); - HttpURLConnection httpConn = null; - try { + try (final CloseableUrlConnection urlConn = new CloseableUrlConnection(url)) { long contentLengthHint = -1L; - if (conn instanceof HttpURLConnection) { + urlConn.conn.setConnectTimeout(HTTP_TIMEOUT); + urlConn.conn.connect(); + if (urlConn.httpConn != null) { // Get content length from HTTP headers, if available - httpConn = (HttpURLConnection) url.openConnection(); - httpConn.setRequestMethod("GET"); - httpConn.setConnectTimeout(HTTP_TIMEOUT); - if (httpConn.getResponseCode() == HttpURLConnection.HTTP_OK) { - contentLengthHint = httpConn.getContentLengthLong(); - if (contentLengthHint < -1L) { - contentLengthHint = -1L; - } - } else { - throw new IOException("Got response code " + httpConn.getResponseCode() + " for URL " + url); + if (urlConn.httpConn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException( + "Got response code " + urlConn.httpConn.getResponseCode() + " for URL " + url); } - } else if (conn.getURL().getProtocol().equalsIgnoreCase("file")) { - // We ended up with a "file:" URL, which can happen as a result of a custom URL scheme that + } 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 = new File(conn.getURL().toURI()); + // 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 URISyntaxException e) { - // Fall through to open URL as InputStream below + } 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 = conn.getInputStream()) { - // Fetch the jar contents from the URL's InputStream. If it doesn't fit in RAM, spill over to disk. + 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) { @@ -586,7 +619,20 @@ private PhysicalZipFile downloadJarFromURL(final String jarURL, final LogNode lo } catch (final MalformedURLException e) { throw new IOException("Malformed URL: " + jarURL); } - } finally { + } + } + + 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(); } @@ -622,7 +668,9 @@ public void reset() { inflater.reset(); } - /** Called when the {@link Recycler} instance is closed, to destroy the {@link Inflater} instance. */ + /** + * Called when the {@link Recycler} instance is closed, to destroy the {@link Inflater} instance. + */ @Override public void close() { inflater.end(); @@ -639,10 +687,14 @@ public void close() { * 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 RecyclableInflater recyclableInflater = inflaterRecycler.acquire(); - private final Inflater inflater = recyclableInflater.getInflater(); private final AtomicBoolean closed = new AtomicBoolean(); private final byte[] buf = new byte[INFLATE_BUF_SIZE]; private static final int INFLATE_BUF_SIZE = 8192; @@ -672,7 +724,8 @@ public int read(final byte[] outBuf, final int off, final int len) throws IOExce return 0; } try { - // Keep fetching data from rawInputStream until buffer is full or inflater has finished + // 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, @@ -741,7 +794,8 @@ public int available() throws IOException { 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 + // 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; } @@ -766,12 +820,11 @@ public void close() { if (!closed.getAndSet(true)) { try { rawInputStream.close(); - } catch (final IOException e) { + } catch (final Exception e) { // Ignore - } finally { - // Reset and recycle inflater instance - inflaterRecycler.recycle(recyclableInflater); } + // Reset and recycle inflater instance + inflaterRecycler.recycle(recyclableInflater); } } }; @@ -804,10 +857,14 @@ public Slice readAllBytesWithSpilloverToDisk(final InputStream inputStream, fina // 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 + // 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 @@ -822,20 +879,25 @@ public Slice readAllBytesWithSpilloverToDisk(final InputStream inputStream, fina 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 + // 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, + // 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 + // 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 + // Trim array if needed (this is needed if inputStreamLengthHint was -1, or + // overestimated // the length of the InputStream) buf = Arrays.copyOf(buf, bufBytesUsed); } @@ -844,7 +906,8 @@ public Slice readAllBytesWithSpilloverToDisk(final InputStream inputStream, fina 0L, this); } - // inputStreamLengthHint is longer than scanSpec.maxJarRamSize, so immediately spill to disk + // inputStreamLengthHint is longer than scanSpec.maxJarRamSize, so immediately + // spill to disk return spillToDisk(inptStream, tempFileBaseName, /* buf = */ null, /* overflowBuf = */ null, log); } } @@ -881,7 +944,8 @@ private FileSlice spillToDisk(final InputStream inputStream, final String tempFi + tempFileBaseName + " -> " + tempFile); } - // Copy everything read so far and the rest of the InputStream to the temporary file + // 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) { @@ -917,9 +981,10 @@ public static byte[] readAllBytesAsArray(final InputStream inputStream, final lo } try (InputStream inptStream = inputStream) { final int bufferSize = uncompressedLengthHint < 1L - // If fileSizeHint is zero or unknown, use default buffer size + // 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 + // 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]; @@ -934,8 +999,10 @@ public static byte[] readAllBytesAsArray(final InputStream inputStream, final lo 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 + // 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) { @@ -943,7 +1010,8 @@ public static byte[] readAllBytesAsArray(final InputStream inputStream, final lo break; } - // Haven't reached end of stream yet. Need to grow the buffer (double its size), and append + // 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"); @@ -1016,9 +1084,9 @@ public void close(final LogNode log) { } if (inflaterRecycler != null) { inflaterRecycler.forceClose(); - inflaterRecycler = null; } - // Temp files have to be deleted last, after all PhysicalZipFiles are closed and files are unmapped + // 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"); @@ -1040,4 +1108,27 @@ public void close(final LogNode log) { } } } + + /** + * 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 b06781e7c..df212f4bd 100644 --- a/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/PhysicalZipFile.java +++ b/src/main/java/nonapi/io/github/classgraph/fastzipfilereader/PhysicalZipFile.java @@ -78,12 +78,8 @@ class PhysicalZipFile { PhysicalZipFile(final File file, final NestedJarHandler nestedJarHandler, final LogNode log) throws IOException { this.nestedJarHandler = nestedJarHandler; - - // Make sure the File is readable and is a regular file - FileUtils.checkCanReadAndIsFile(file); - this.file = file; - this.pathStr = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, file.getPath()); + this.pathStr = FastPathResolver.resolve(FileUtils.currDirPath(), file.getPath()); this.slice = new FileSlice(file, nestedJarHandler, log); } @@ -102,12 +98,8 @@ class PhysicalZipFile { PhysicalZipFile(final Path path, final NestedJarHandler nestedJarHandler, final LogNode log) throws IOException { this.nestedJarHandler = nestedJarHandler; - - // Make sure the File is readable and is a regular file - FileUtils.checkCanReadAndIsFile(path); - this.path = path; - this.pathStr = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH, path.toString()); + this.pathStr = FastPathResolver.resolve(FileUtils.currDirPath(), path.toString()); this.slice = new PathSlice(path, nestedJarHandler); } diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/FileSlice.java b/src/main/java/nonapi/io/github/classgraph/fileslice/FileSlice.java index 2635a8d2c..d230cfc23 100644 --- a/src/main/java/nonapi/io/github/classgraph/fileslice/FileSlice.java +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/FileSlice.java @@ -137,6 +137,8 @@ public FileSlice(final File file, final boolean isDeflatedZipEntry, final long i 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) @@ -144,7 +146,7 @@ public FileSlice(final File file, final boolean isDeflatedZipEntry, final long i } catch (IOException | OutOfMemoryError e) { // Try running garbage collection then try mapping the file again System.gc(); - System.runFinalization(); + nestedJarHandler.runFinalizationMethod(); try { backingByteBuffer = fileChannel.map(MapMode.READ_ONLY, 0L, fileLength); } catch (IOException | OutOfMemoryError e2) { @@ -295,7 +297,7 @@ public void close() { 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) - FileUtils.closeDirectByteBuffer(backingByteBuffer, /* log = */ null); + nestedJarHandler.closeDirectByteBuffer(backingByteBuffer); } backingByteBuffer = null; fileChannel = null; diff --git a/src/main/java/nonapi/io/github/classgraph/fileslice/PathSlice.java b/src/main/java/nonapi/io/github/classgraph/fileslice/PathSlice.java index 321e23109..2c490c637 100644 --- a/src/main/java/nonapi/io/github/classgraph/fileslice/PathSlice.java +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/PathSlice.java @@ -28,7 +28,6 @@ */ package nonapi.io.github.classgraph.fileslice; -import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -45,7 +44,7 @@ import nonapi.io.github.classgraph.utils.FileUtils; /** A {@link Path} slice. */ -public class PathSlice extends Slice implements Closeable { +public class PathSlice extends Slice { /** The {@link Path}. */ public final Path path; @@ -88,8 +87,10 @@ private PathSlice(final PathSlice parentSlice, final long offset, final long len 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) + // 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) } /** @@ -109,17 +110,42 @@ private PathSlice(final PathSlice parentSlice, final long offset, final long len */ 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); - // Make sure the File is readable and is a regular file - FileUtils.checkCanReadAndIsFile(path); + 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 + // 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 @@ -217,8 +243,10 @@ public byte[] load() throws IOException { @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) + // 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"); } @@ -245,17 +273,16 @@ public int hashCode() { @Override public void close() { if (!isClosed.getAndSet(true)) { - if (isTopLevelFileSlice) { - // Only close the FileChannel in the toplevel file slice, so that it is only closed once - if (fileChannel != null) { - try { - // Closing raf will also close the associated FileChannel - fileChannel.close(); - } catch (final IOException e) { - // Ignore - } - fileChannel = null; + 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 index 8a4f29c20..78c842198 100644 --- a/src/main/java/nonapi/io/github/classgraph/fileslice/Slice.java +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/Slice.java @@ -36,10 +36,9 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.zip.Inflater; +import io.github.classgraph.Resource; import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler; -import nonapi.io.github.classgraph.fileslice.reader.ClassfileReader; import nonapi.io.github.classgraph.fileslice.reader.RandomAccessReader; import nonapi.io.github.classgraph.utils.FileUtils; @@ -158,13 +157,13 @@ public InputStream open() throws IOException { /** * Open this {@link Slice} as an {@link InputStream}. * - * @param onClose - * a method to run when the returned {@code InputStream} is closed, or null if none. + * @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 Runnable onClose) throws IOException { + public InputStream open(final Resource resourceToClose) throws IOException { final InputStream rawInputStream = new InputStream() { RandomAccessReader randomAccessReader = randomAccessReader(); private long currOff; @@ -234,10 +233,14 @@ public boolean markSupported() { @Override public void close() { - closed.getAndSet(true); - if (onClose != null) { - onClose.run(); + if (resourceToClose != null) { + try { + resourceToClose.close(); + } catch (final Exception e) { + // Ignore + } } + closed.getAndSet(true); } }; return isDeflatedZipEntry ? nestedJarHandler.openInflaterInputStream(rawInputStream) : rawInputStream; @@ -250,22 +253,6 @@ public void close() { */ public abstract RandomAccessReader randomAccessReader(); - /** - * Open this {@link Slice} for buffered sequential reading. Make sure you close this when you have finished with - * it, in order to recycle any {@link Inflater} instances. - * - * @return the classfile reader - * @throws IOException - * Signals that an I/O exception has occurred. - */ - public ClassfileReader openClassfileReader() throws IOException { - if (sliceLength > FileUtils.MAX_BUFFER_SIZE) { - throw new IllegalArgumentException( - "Cannot open slices larger than 2GB for sequential buffered reading"); - } - return new ClassfileReader(this); - } - /** * Load the slice as a byte array. * 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 index 8326940ec..e007169ce 100644 --- a/src/main/java/nonapi/io/github/classgraph/fileslice/reader/ClassfileReader.java +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/ClassfileReader.java @@ -38,6 +38,7 @@ 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; @@ -50,6 +51,9 @@ * 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; @@ -93,11 +97,14 @@ public class ClassfileReader implements RandomAccessReader, SequentialReader, Cl * * @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) throws IOException { + 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(); @@ -133,12 +140,15 @@ public ClassfileReader(final Slice slice) throws IOException { * * @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) throws IOException { + public ClassfileReader(final InputStream inputStream, final Resource resourceToClose) throws IOException { inflaterInputStream = inputStream; arr = new byte[INITIAL_BUF_SIZE]; + this.resourceToClose = resourceToClose; } /** @@ -442,6 +452,11 @@ 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/RandomAccessReader.java b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessReader.java index 4d9f770fa..65f6b3816 100644 --- a/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessReader.java +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/RandomAccessReader.java @@ -48,7 +48,7 @@ public interface RandomAccessReader { * @throws IOException * If there was an exception while reading. */ - public int read(long srcOffset, ByteBuffer dstBuf, int dstBufStart, int numBytes) throws IOException; + int read(long srcOffset, ByteBuffer dstBuf, int dstBufStart, int numBytes) throws IOException; /** * Read bytes into a byte array. @@ -65,7 +65,7 @@ public interface RandomAccessReader { * @throws IOException * If there was an exception while reading. */ - public int read(long srcOffset, byte[] dstArr, int dstArrStart, int numBytes) throws IOException; + 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). @@ -76,7 +76,7 @@ public interface RandomAccessReader { * @throws IOException * If there was an exception while reading. */ - public byte readByte(final long offset) throws IOException; + byte readByte(final long offset) throws IOException; /** * Read an unsigned byte at a specific offset (without changing the current cursor offset). @@ -87,7 +87,7 @@ public interface RandomAccessReader { * @throws IOException * If there was an exception while reading. */ - public int readUnsignedByte(final long offset) throws IOException; + int readUnsignedByte(final long offset) throws IOException; /** * Read a short at a specific offset (without changing the current cursor offset). @@ -98,7 +98,7 @@ public interface RandomAccessReader { * @throws IOException * If there was an exception while reading. */ - public short readShort(final long offset) throws IOException; + short readShort(final long offset) throws IOException; /** * Read a unsigned short at a specific offset (without changing the current cursor offset). @@ -109,7 +109,7 @@ public interface RandomAccessReader { * @throws IOException * If there was an exception while reading. */ - public int readUnsignedShort(final long offset) throws IOException; + int readUnsignedShort(final long offset) throws IOException; /** * Read a int at a specific offset (without changing the current cursor offset). @@ -120,7 +120,7 @@ public interface RandomAccessReader { * @throws IOException * If there was an exception while reading. */ - public int readInt(final long offset) throws IOException; + int readInt(final long offset) throws IOException; /** * Read a unsigned int at a specific offset (without changing the current cursor offset). @@ -131,7 +131,7 @@ public interface RandomAccessReader { * @throws IOException * If there was an exception while reading. */ - public long readUnsignedInt(final long offset) throws IOException; + long readUnsignedInt(final long offset) throws IOException; /** * Read a long at a specific offset (without changing the current cursor offset). @@ -142,7 +142,7 @@ public interface RandomAccessReader { * @throws IOException * If there was an exception while reading. */ - public long readLong(final long offset) throws IOException; + long readLong(final long offset) throws IOException; /** * Reads the "modified UTF8" format defined in the Java classfile spec, optionally replacing '/' with '.', and @@ -160,7 +160,7 @@ public interface RandomAccessReader { * @throws IOException * If an I/O exception occurs. */ - public String readString(final long offset, final int numBytes, final boolean replaceSlashWithDot, + String readString(final long offset, final int numBytes, final boolean replaceSlashWithDot, final boolean stripLSemicolon) throws IOException; /** @@ -174,5 +174,5 @@ public String readString(final long offset, final int numBytes, final boolean re * @throws IOException * If an I/O exception occurs. */ - public String readString(final long offset, final int numBytes) throws IOException; + 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 index 5cf4b6835..a86ad8c79 100644 --- a/src/main/java/nonapi/io/github/classgraph/fileslice/reader/SequentialReader.java +++ b/src/main/java/nonapi/io/github/classgraph/fileslice/reader/SequentialReader.java @@ -39,7 +39,7 @@ public interface SequentialReader { * @throws IOException * If there was an exception while reading. */ - public byte readByte() throws IOException; + byte readByte() throws IOException; /** * Read an unsigned byte at the current cursor position. @@ -48,7 +48,7 @@ public interface SequentialReader { * @throws IOException * If there was an exception while reading. */ - public int readUnsignedByte() throws IOException; + int readUnsignedByte() throws IOException; /** * Read a short at the current cursor position. @@ -57,7 +57,7 @@ public interface SequentialReader { * @throws IOException * If there was an exception while reading. */ - public short readShort() throws IOException; + short readShort() throws IOException; /** * Read a unsigned short at the current cursor position. @@ -66,7 +66,7 @@ public interface SequentialReader { * @throws IOException * If there was an exception while reading. */ - public int readUnsignedShort() throws IOException; + int readUnsignedShort() throws IOException; /** * Read a int at the current cursor position. @@ -75,7 +75,7 @@ public interface SequentialReader { * @throws IOException * If there was an exception while reading. */ - public int readInt() throws IOException; + int readInt() throws IOException; /** * Read a unsigned int at the current cursor position. @@ -84,7 +84,7 @@ public interface SequentialReader { * @throws IOException * If there was an exception while reading. */ - public long readUnsignedInt() throws IOException; + long readUnsignedInt() throws IOException; /** * Read a long at the current cursor position. @@ -93,7 +93,7 @@ public interface SequentialReader { * @throws IOException * If there was an exception while reading. */ - public long readLong() throws IOException; + long readLong() throws IOException; /** * Skip the given number of bytes. @@ -103,7 +103,7 @@ public interface SequentialReader { * @throws IOException * If there was an exception while reading. */ - public void skip(final int bytesToSkip) throws IOException; + void skip(final int bytesToSkip) throws IOException; /** * Reads the "modified UTF8" format defined in the Java classfile spec, optionally replacing '/' with '.', and @@ -119,7 +119,7 @@ public interface SequentialReader { * @throws IOException * If an I/O exception occurs. */ - public String readString(final int numBytes, final boolean replaceSlashWithDot, final boolean stripLSemicolon) + String readString(final int numBytes, final boolean replaceSlashWithDot, final boolean stripLSemicolon) throws IOException; /** @@ -131,5 +131,5 @@ public String readString(final int numBytes, final boolean replaceSlashWithDot, * @throws IOException * If an I/O exception occurs. */ - public String readString(final int numBytes) throws IOException; + 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 68d261fe9..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,6 +61,8 @@ import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; + /** * A cache of field types and associated constructors for each encountered class, used to speed up constructor * lookup. @@ -88,6 +90,8 @@ 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(); @@ -116,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; } /** @@ -132,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; } @@ -204,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; @@ -239,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 56cd29abf..dae47b6c0 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/ClassFields.java +++ b/src/main/java/nonapi/io/github/classgraph/json/ClassFields.java @@ -41,6 +41,7 @@ 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 @@ -107,7 +108,7 @@ public int compare(final Field a, final Field b) { * 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<>(); @@ -150,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 f511d66c7..c4de8f3fe 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/FieldTypeInfo.java +++ b/src/main/java/nonapi/io/github/classgraph/json/FieldTypeInfo.java @@ -98,7 +98,7 @@ private enum PrimitiveType { /** Character type. */ CHARACTER, /** Class reference */ - CLASS_REF; + CLASS_REF } /** 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 2b2ce173e..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,6 +40,7 @@ import java.util.Map; import java.util.Map.Entry; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; import nonapi.io.github.classgraph.types.ParseException; /** @@ -700,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 @@ -766,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 e7aab663a..ac398c548 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/JSONParser.java +++ b/src/main/java/nonapi/io/github/classgraph/json/JSONParser.java @@ -403,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 4b8d53b34..63f23956b 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/JSONSerializer.java +++ b/src/main/java/nonapi/io/github/classgraph/json/JSONSerializer.java @@ -43,6 +43,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import nonapi.io.github.classgraph.reflection.ReflectionUtils; import nonapi.io.github.classgraph.utils.CollectionUtils; /** @@ -456,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); } } @@ -499,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. @@ -516,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()); } /** @@ -562,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"); } @@ -587,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 d6afa882e..beadd071a 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/JSONUtils.java +++ b/src/main/java/nonapi/io/github/classgraph/json/JSONUtils.java @@ -28,23 +28,100 @@ */ package nonapi.io.github.classgraph.json; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodHandles.Lookup; -import java.lang.invoke.MethodType; 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}. @@ -60,13 +137,6 @@ 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]; - // private static MethodHandle canAccessMethodHandle = null; - // private static Method canAccessMethod = null; - private static MethodHandle isAccessibleMethodHandle = null; - private static Method isAccessibleMethod = null; - private static MethodHandle trySetAccessibleMethodHandle = null; - private static Method trySetAccessibleMethod = null; - static { for (int c = 0; c < 256; c++) { if (c == 32) { @@ -76,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['\\'] = "\\\\"; @@ -85,46 +155,6 @@ public final class JSONUtils { JSON_CHAR_REPLACEMENTS['\t'] = "\\t"; JSON_CHAR_REPLACEMENTS['\b'] = "\\b"; JSON_CHAR_REPLACEMENTS['\f'] = "\\f"; - - final Lookup lookup = MethodHandles.lookup(); - // try { - // // JDK 9+: use AccessibleObject::canAccess(instance) - // canAccessMethodHandle = lookup.findVirtual(AccessibleObject.class, "canAccess", - // MethodType.methodType(boolean.class, Object.class)); - // } catch (NoSuchMethodException | IllegalAccessException e) { - // // Ignore - // } - // try { - // canAccessMethod = AccessibleObject.class.getDeclaredMethod("canAccess"); - // } catch (NoSuchMethodException | SecurityException e1) { - // // Ignore - // } - try { - // JDK 7/8: use AccessibleObject::isAccessible() - isAccessibleMethodHandle = lookup.findVirtual(AccessibleObject.class, "isAccessible", - MethodType.methodType(boolean.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - // Ignore - } - - try { - isAccessibleMethod = AccessibleObject.class.getDeclaredMethod("isAccessible", Object.class); - } catch (NoSuchMethodException | SecurityException e1) { - // Ignore - } - try { - // JDK 9+: use AccessibleObject::trySetAccessible() rather than - // AccessibleObject::setAccessible(true) - trySetAccessibleMethodHandle = lookup.findVirtual(AccessibleObject.class, "trySetAccessible", - MethodType.methodType(boolean.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - // Ignore - } - try { - trySetAccessibleMethod = AccessibleObject.class.getDeclaredMethod("trySetAccessible"); - } catch (NoSuchMethodException | SecurityException e1) { - // Ignore - } } /** Lookup table for fast indenting. */ @@ -247,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); @@ -358,114 +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) { - // Test if field or constructor is already accessible - final AtomicBoolean accessible = new AtomicBoolean(false); - // // TODO: this method needs to take an object instance reference before canAccess can be used - // if (canAccessMethodHandle != null) { - // // JDK 9+: use canAccess(instance) - // try { - // accessible.set((Boolean) canAccessMethodHandle.invokeExact(fieldOrConstructor, instance)); - // } catch (Throwable e) { - // // Ignore - // } - // } - // if (canAccessMethod != null) { - // accessible.set((Boolean) canAccessMethod.invoke(fieldOrConstructor, instance)); - // } - if (!accessible.get()) { - if (isAccessibleMethodHandle != null) { - // JDK 7/8: use isAccessible (deprecated in JDK 9+) - try { - // Have to use double casting and wrap in new Object[] due to Animal Sniffer bug: - // https://github.com/mojohaus/animal-sniffer/issues/67 - final Object invokeResult = isAccessibleMethodHandle - .invoke(new Object[] { fieldOrConstructor }); - accessible.set((Boolean) invokeResult); - } catch (final Throwable e) { - // Ignore - } - } else if (isAccessibleMethod != null) { - // JDK 7/8: use isAccessible (deprecated in JDK 9+) - try { - accessible.set((Boolean) isAccessibleMethod.invoke(fieldOrConstructor)); - } catch (final Throwable e) { - // Ignore - } - } - } - - // Only set accessible if field or constructor is not yet accessible - if (!accessible.get()) { - if (trySetAccessibleMethodHandle != null) { - try { - // Have to use double casting and wrap in new Object[] due to Animal Sniffer bug: - // https://github.com/mojohaus/animal-sniffer/issues/67 - final Object invokeResult = trySetAccessibleMethodHandle - .invoke(new Object[] { fieldOrConstructor }); - accessible.set((Boolean) invokeResult); - } catch (final Throwable e) { - // Ignore - } - } else if (trySetAccessibleMethod != null) { - try { - accessible.set((Boolean) trySetAccessibleMethod.invoke(fieldOrConstructor)); - } catch (final Throwable e) { - // Ignore - } - } - if (!accessible.get()) { - try { - fieldOrConstructor.setAccessible(true); - accessible.set(true); - } catch (final Throwable t) { - // Ignore - } - } - if (!accessible.get()) { - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Void run() { - if (trySetAccessibleMethodHandle != null) { - try { - // Have to use double casting and wrap in new Object[] due to Animal Sniffer bug: - // https://github.com/mojohaus/animal-sniffer/issues/67 - final Object invokeResult = trySetAccessibleMethodHandle - .invoke(new Object[] { fieldOrConstructor }); - accessible.set((Boolean) invokeResult); - } catch (final Throwable e) { - // Ignore - } - } else if (trySetAccessibleMethod != null) { - try { - accessible.set((Boolean) trySetAccessibleMethod.invoke(fieldOrConstructor)); - } catch (final Throwable e) { - // Ignore - } - } - if (!accessible.get()) { - try { - fieldOrConstructor.setAccessible(true); - accessible.set(true); - } catch (final Throwable t) { - // Ignore - } - } - return null; - } - }); - } - } - return accessible.get(); - } - /** * Check if a field is serializable. Don't serialize transient, final, synthetic, or inaccessible fields. * @@ -479,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 26c76fcdb..9419ad082 100644 --- a/src/main/java/nonapi/io/github/classgraph/json/ParameterizedTypeImpl.java +++ b/src/main/java/nonapi/io/github/classgraph/json/ParameterizedTypeImpl.java @@ -133,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/recycler/Recycler.java b/src/main/java/nonapi/io/github/classgraph/recycler/Recycler.java index 148f0a7eb..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()); } /** 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 index 21fa8a945..f7b098b75 100644 --- a/src/main/java/nonapi/io/github/classgraph/scanspec/AcceptReject.java +++ b/src/main/java/nonapi/io/github/classgraph/scanspec/AcceptReject.java @@ -245,7 +245,7 @@ public void addToAccept(final String str) { this.acceptPatterns = new ArrayList<>(); } this.acceptGlobs.add(str); - this.acceptPatterns.add(globToPattern(str)); + this.acceptPatterns.add(globToPattern(str, /* simpleGlob = */ true)); } else { if (this.accept == null) { this.accept = new HashSet<>(); @@ -301,7 +301,7 @@ public void addToReject(final String str) { this.rejectPatterns = new ArrayList<>(); } this.rejectGlobs.add(str); - this.rejectPatterns.add(globToPattern(str)); + this.rejectPatterns.add(globToPattern(str, /* simpleGlob = */ true)); } else { if (this.reject == null) { this.reject = new HashSet<>(); @@ -565,15 +565,33 @@ public static String classNameToClassfilePath(final String className) { } /** - * Convert a spec with a '*' glob character into a regular expression. Replaces "." with "\." and "*" with ".*", - * then compiles a regular expression. + * 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) { - return Pattern.compile("^" + glob.replace(".", "\\.").replace("*", ".*") + "$"); + 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('?', '.') // + ) // + + "$"); } /** diff --git a/src/main/java/nonapi/io/github/classgraph/scanspec/ScanSpec.java b/src/main/java/nonapi/io/github/classgraph/scanspec/ScanSpec.java index ae7ad0e70..39e06b28c 100644 --- a/src/main/java/nonapi/io/github/classgraph/scanspec/ScanSpec.java +++ b/src/main/java/nonapi/io/github/classgraph/scanspec/ScanSpec.java @@ -258,6 +258,9 @@ public class ScanSpec { /** 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. */ @@ -476,11 +479,7 @@ public enum ScanSpecPathMatch { */ public ScanSpecPathMatch dirAcceptMatchStatus(final String relativePath) { // In rejected path - if (pathAcceptReject.isRejected(relativePath)) { - // The directory is rejected. - return ScanSpecPathMatch.HAS_REJECTED_PATH_PREFIX; - } - if (pathPrefixAcceptReject.isRejected(relativePath)) { + if (pathAcceptReject.isRejected(relativePath) || pathPrefixAcceptReject.isRejected(relativePath)) { // An prefix of this path is rejected. return ScanSpecPathMatch.HAS_REJECTED_PATH_PREFIX; } @@ -508,16 +507,13 @@ public ScanSpecPathMatch dirAcceptMatchStatus(final String relativePath) { } // Ancestor of accepted path - if (relativePath.equals("/")) { - // The default package is always the ancestor of accepted paths (need to keep recursing) - return ScanSpecPathMatch.ANCESTOR_OF_ACCEPTED_PATH; - } - if (pathAcceptReject.acceptHasPrefix(relativePath)) { - // relativePath is an ancestor (prefix) of an accepted path - return ScanSpecPathMatch.ANCESTOR_OF_ACCEPTED_PATH; - } - if (classfilePathAcceptReject.acceptHasPrefix(relativePath)) { - // relativePath is an ancestor (prefix) of an accepted class' parent directory + 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; } 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 afb56c92f..7dd48db8a 100644 --- a/src/main/java/nonapi/io/github/classgraph/types/TypeUtils.java +++ b/src/main/java/nonapi/io/github/classgraph/types/TypeUtils.java @@ -51,9 +51,12 @@ private TypeUtils() { * The parser. * @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 boolean stopAtDollarSign) { + public static boolean getIdentifierToken(final Parser parser, final boolean stopAtDollarSign, + final boolean stopAtDot) { boolean consumedChar = false; while (parser.hasMore()) { final char c = parser.peek(); @@ -61,8 +64,8 @@ public static boolean getIdentifierToken(final Parser parser, final boolean stop parser.appendToToken('.'); parser.next(); consumedChar = true; - } else if (c != ';' && c != '[' && c != '<' && c != '>' && c != ':' - && (!stopAtDollarSign || c != '$')) { + } else if (c != ';' && c != '[' && c != '<' && c != '>' && c != ':' && (!stopAtDollarSign || c != '$') + && (!stopAtDot || c != '.')) { parser.appendToToken(c); parser.next(); consumedChar = true; @@ -80,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 index 6134df200..c3767c0f5 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/CollectionUtils.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/CollectionUtils.java @@ -28,6 +28,8 @@ */ 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; @@ -53,7 +55,7 @@ private CollectionUtils() { * the list */ public static > void sortIfNotEmpty(final List list) { - if (!list.isEmpty()) { + if (list.size() > 1) { Collections.sort(list); } } @@ -71,8 +73,23 @@ public static > void sortIfNotEmpty(final List void sortIfNotEmpty(final List list, final Comparator comparator) { - if (!list.isEmpty()) { + 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 abfe6f824..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,14 +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])+"); - /** Match custom URLs that are followed by two slashes. */ - private static final Pattern schemeTwoSlashMatcher = Pattern.compile("^[a-zA-Z+\\-.]+://"); - - /** Match custom URLs that are followed by one slash. */ - private static final Pattern schemeOneSlashMatcher = Pattern.compile("^[a-zA-Z+\\-.]+:/"); - - /** 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. @@ -200,85 +195,63 @@ public static String resolve(final String resolveBasePath, final String relative boolean isAbsolutePath = false; boolean isFileOrJarURL = false; int startIdx = 0; - if (relativePath.regionMatches(true, 0, "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 "file:" prefix from relative path - startIdx += 5; - while (startIdx < relativePath.length() - 1 && relativePath.charAt(startIdx) == '/' - && relativePath.charAt(startIdx + 1) == '/') { - // Strip off all but one '/' after "file:" - startIdx++; - } - isFileOrJarURL = true; - } else { - // Preserve the number of slashes on custom URL schemes (#420) - final String relPath = startIdx == 0 ? relativePath : relativePath.substring(startIdx); - final Matcher m2 = schemeTwoSlashMatcher.matcher(relPath); - if (m2.find()) { - final String m2Match = m2.group(); - startIdx += m2Match.length(); - prefix = m2Match; - // 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. + 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 { - final Matcher m1 = schemeOneSlashMatcher.matcher(relPath); - if (m1.find()) { - final String m1Match = m1.group(); - startIdx += m1Match.length(); - prefix = m1Match; - isAbsolutePath = true; - } - } - } - if (isFileOrJarURL) { - if (WINDOWS) { - if (relativePath.startsWith("\\\\\\\\", startIdx) || relativePath.startsWith("////", startIdx)) { - // Windows UNC URL - startIdx += 4; - prefix += "//"; + // 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; - } } } - // "file:///" or "jar:///" URL - if (relativePath.startsWith("///", startIdx)) { - startIdx += 2; - } - } + } while (matchedPrefix); // Handle Windows paths starting with a drive designation as an absolute path - if (WINDOWS) { - if ((relativePath.startsWith("//", startIdx) || relativePath.startsWith("\\\\", startIdx))) { + if (VersionFinder.OS == OperatingSystem.Windows) { + if (relativePath.startsWith("//", startIdx) || relativePath.startsWith("\\\\", startIdx)) { // Windows UNC path startIdx += 2; - prefix = "//"; + prefix += "//"; isAbsolutePath = true; } else if (relativePath.length() - startIdx > 2 && Character.isLetter(relativePath.charAt(startIdx)) && relativePath.charAt(startIdx + 1) == ':') { 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 b697e4172..395ac3ca7 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/FileUtils.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/FileUtils.java @@ -36,13 +36,20 @@ import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.file.Files; -import java.nio.file.LinkOption; +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.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; + +import nonapi.io.github.classgraph.reflection.ReflectionUtils; +import nonapi.io.github.classgraph.utils.VersionFinder.OperatingSystem; /** * File utilities. @@ -69,11 +76,14 @@ public final class FileUtils { /** The Unsafe object. */ private static Object theUnsafe; + /** 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; + private static String currDirPath; /** * The maximum size of a file buffer array. Eight bytes smaller than {@link Integer#MAX_VALUE}, since some VMs @@ -90,23 +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 new RuntimeException("Could not resolve current directory: " + currDirPathStr, e); + // ------------------------------------------------------------------------------------------------------------- + + /** + * Get the current directory (only looks at the current directory the first time it is called, then caches this + * value for future reads). + * + * @return The current directory, as a string + */ + 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 + } + } + 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 + } + } + + // Normalize current directory the same way all other paths are normalized in ClassGraph, + // for consistency + currDirPath = FastPathResolver.resolve(path == null ? "" : path.toString()); } - CURR_DIR_PATH = currDirPathStr; + return currDirPath; } // ------------------------------------------------------------------------------------------------------------- @@ -158,7 +187,8 @@ public static String sanitizeEntryPath(final String path, final boolean removeIn } // Handle "..", "." and empty path segments, if any were found - final boolean pathHasInitialSlash = pathLen > 0 && pathChars[0] == '/'; + 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 "!") @@ -211,6 +241,12 @@ public static String sanitizeEntryPath(final String path, final boolean removeIn pathSanitized.append(path); } + // 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 @@ -260,6 +296,25 @@ 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. * @@ -287,7 +342,11 @@ public static boolean canReadAndIsFile(final File file) { */ public static boolean canReadAndIsFile(final Path path) { try { - if (!Files.exists(path)) { + return canReadAndIsFile(path.toFile()); + } catch (final UnsupportedOperationException ignored) { + } + try { + if (!Files.isReadable(path)) { return false; } } catch (final SecurityException e) { @@ -296,6 +355,16 @@ public static boolean canReadAndIsFile(final Path path) { 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. * @@ -327,7 +396,12 @@ public static void checkCanReadAndIsFile(final File file) throws IOException { */ public static void checkCanReadAndIsFile(final Path path) throws IOException { try { - if (!Files.exists(path)) { + 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) { @@ -365,7 +439,11 @@ public static boolean canReadAndIsDir(final File file) { */ public static boolean canReadAndIsDir(final Path path) { try { - if (!Files.exists(path)) { + return canReadAndIsDir(path.toFile()); + } catch (final UnsupportedOperationException ignored) { + } + try { + if (!Files.isReadable(path)) { return false; } } catch (final SecurityException e) { @@ -374,6 +452,16 @@ public static boolean canReadAndIsDir(final Path path) { 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. * @@ -449,30 +537,12 @@ private static void lookupCleanMethodPrivileged() { } catch (final ReflectiveOperationException | LinkageError e) { // Ignore } - } else { - //boolean jdkSuccess = false; - // // TODO: This feature is in incubation now -- enable after it leaves incubation. - // // To enable this feature, need to: - // // -- add whatever the "jdk.incubator.foreign" module name is replaced with to - // // in pom.xml, as an optional dependency - // // -- add the same module name to module-info.java as a "requires static" optional dependency - // // -- build two versions of module.java: the existing one, for --release=9, and a new version, - // // for --release=15 (or whatever the final release version ends up being when the feature is - // // moved out of incubation). - // try { - // // JDK 14+ Invoke MemorySegment.ofByteBuffer(myByteBuffer).close() - // // https://stackoverflow.com/a/26777380/3950982 - // memorySegmentClass = Class.forName("jdk.incubator.foreign.MemorySegment"); - // memorySegmentCloseMethod = AutoCloseable.class.getDeclaredMethod("close"); - // memorySegmentOfByteBufferMethod = memorySegmentClass.getMethod("ofByteBuffer", - // ByteBuffer.class); - // jdk14Success = true; - // } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e1) { - // // Fall through - // } - //if (!jdk14Success) { // In JDK9+, calling sun.misc.Cleaner.clean() gives a reflection warning on stderr, - // so we need to call Unsafe.theUnsafe.invokeCleaner(byteBuffer) instead, which makes - // the same call, but does not print the reflection warning. + } 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 { @@ -497,16 +567,6 @@ private static void lookupCleanMethodPrivileged() { } } - static { - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Object run() { - lookupCleanMethodPrivileged(); - return null; - } - }); - } - /** * Close a direct byte buffer (run in doPrivileged). * @@ -585,7 +645,7 @@ private static boolean closeDirectByteBufferPrivileged(final ByteBuffer byteBuff // } // memorySegmentCloseMethod.invoke(memorySegment); // return true; - } else { + } else if (VersionFinder.JAVA_MAJOR_VERSION < 24) { if (theUnsafe == null) { if (log != null) { log.log("Could not unmap ByteBuffer, theUnsafe == null"); @@ -605,6 +665,9 @@ private static boolean closeDirectByteBufferPrivileged(final ByteBuffer byteBuff // 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) { @@ -623,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/JarUtils.java b/src/main/java/nonapi/io/github/classgraph/utils/JarUtils.java index 43bd70a77..10b7dc382 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/JarUtils.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/JarUtils.java @@ -64,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 ':'. @@ -193,7 +196,7 @@ public static String[] smartPathSplit(final String pathStr, final char separator 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/LogNode.java b/src/main/java/nonapi/io/github/classgraph/utils/LogNode.java index 361f67c57..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,6 +49,12 @@ * 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 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 9e283f045..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 | SecurityException 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/URLPathEncoder.java b/src/main/java/nonapi/io/github/classgraph/utils/URLPathEncoder.java index 5ac5eb4d8..28ed7bb8f 100644 --- a/src/main/java/nonapi/io/github/classgraph/utils/URLPathEncoder.java +++ b/src/main/java/nonapi/io/github/classgraph/utils/URLPathEncoder.java @@ -154,8 +154,8 @@ public static String encodePath(final String path) { // Also accept ':' after a Windows drive letter if (VersionFinder.OS == OperatingSystem.Windows) { int i = validColonPrefixLen; - if (i < path.length() && path.charAt(i) == '/') { - i++; + 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; @@ -219,15 +219,19 @@ public static String normalizeURLPath(final String urlPath) { // Any URL containing "!" segments must have "/" after "!" for the "jar:" URL scheme to work urlPathNormalized = urlPathNormalized.replace("/!", "!").replace("!/", "!").replace("!", "!/"); - // Prepend "file:/" + // Prepend "file:///" to absolute paths and "file:" to relative paths if (windowsDrivePrefix.isEmpty()) { // There is no Windows drive - urlPathNormalized = urlPathNormalized.startsWith("/") ? "file:" + urlPathNormalized - : "file:/" + urlPathNormalized; + if (urlPathNormalized.startsWith("/")) { + // Absolute path: file:///xyz + urlPathNormalized = "file://" + urlPathNormalized; + } else { + // Relative path: file:xyz + urlPathNormalized = "file:" + urlPathNormalized; + } } else { - // There is a Windows drive - urlPathNormalized = "file:/" + windowsDrivePrefix - + (urlPathNormalized.startsWith("/") ? urlPathNormalized : "/" + urlPathNormalized); + // There is a Windows drive, path must be absolute + urlPathNormalized = "file:///" + windowsDrivePrefix + urlPathNormalized; } // Prepend "jar:" if path contains a "!" segment 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 cef6e6959..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,6 +28,7 @@ */ package nonapi.io.github.classgraph.utils; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -39,9 +40,12 @@ 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; @@ -134,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")) { @@ -221,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(); @@ -276,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 index e5dc4e111..8c20e98f9 100644 --- a/src/module-info/COMPILING +++ b/src/module-info/COMPILING @@ -1,3 +1,4 @@ # This is compiled separately from ClassGraph, so that ClassGraph can be built on JDK 8 in JDK 7 mode -javac --release 9 io.github.classgraph/module-info.java io.github.classgraph/io/github/classgraph/Placeholder.java +# 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/module-info.class b/src/module-info/io.github.classgraph/module-info.class index 10722691b..9b7f52051 100644 Binary files a/src/module-info/io.github.classgraph/module-info.class 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 index 344d7cadf..4901edf83 100644 --- a/src/module-info/io.github.classgraph/module-info.java +++ b/src/module-info/io.github.classgraph/module-info.java @@ -36,6 +36,8 @@ // 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) @@ -44,6 +46,8 @@ requires java.management; // LogNode requires java.logging requires java.logging; - - // N.B. make sure the "Import-Package" entries in the manifest (in pom.xml) match these "requires" statements + + // 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/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/test/java/io/github/classgraph/features/DeclaredVsNonDeclaredTest.java b/src/test/java/io/github/classgraph/features/DeclaredVsNonDeclaredTest.java index d571b0a23..4e81fbc35 100644 --- a/src/test/java/io/github/classgraph/features/DeclaredVsNonDeclaredTest.java +++ b/src/test/java/io/github/classgraph/features/DeclaredVsNonDeclaredTest.java @@ -128,9 +128,11 @@ public void declaredVsNonDeclaredMethods() { 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()]"); } } 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/MethodParameterAnnotationsTest.java b/src/test/java/io/github/classgraph/features/MethodParameterAnnotationsTest.java index f4d0d3cb4..9373a1d7c 100644 --- a/src/test/java/io/github/classgraph/features/MethodParameterAnnotationsTest.java +++ b/src/test/java/io/github/classgraph/features/MethodParameterAnnotationsTest.java @@ -68,9 +68,9 @@ public void annotationEquality() { .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/MultiReleaseJarTest.java b/src/test/java/io/github/classgraph/features/MultiReleaseJarTest.java index c573b0b63..7ca13b4ab 100644 --- a/src/test/java/io/github/classgraph/features/MultiReleaseJarTest.java +++ b/src/test/java/io/github/classgraph/features/MultiReleaseJarTest.java @@ -1,6 +1,7 @@ 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; @@ -79,4 +80,58 @@ public void multiReleaseVersioningOfResources() throws Exception { } } } + + /** + * 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/issues/GenericInnerClassTypedField.java b/src/test/java/io/github/classgraph/issues/GenericInnerClassTypedField.java index dc23ccbc9..ad08d5c66 100644 --- a/src/test/java/io/github/classgraph/issues/GenericInnerClassTypedField.java +++ b/src/test/java/io/github/classgraph/issues/GenericInnerClassTypedField.java @@ -44,8 +44,8 @@ public void testGenericInnerClassTypedField() { .getFieldInfo(); final ClassRefTypeSignature classRefTypeSignature = (ClassRefTypeSignature) fields.get(0) .getTypeSignature(); - assertThat(classRefTypeSignature.toString()).isEqualTo(A.class.getName().replace('$', '.') + "<" - + Integer.class.getName() + ", " + String.class.getName() + ">.B"); + assertThat(classRefTypeSignature.toString()).isEqualTo( + 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 1ca9adecc..6a8291f5a 100644 --- a/src/test/java/io/github/classgraph/issues/IssuesTest.java +++ b/src/test/java/io/github/classgraph/issues/IssuesTest.java @@ -21,7 +21,7 @@ public class IssuesTest { @Test public void issue70() { try (ScanResult scanResult = new ClassGraph().acceptPackages(Impl1.class.getPackage().getName()).scan()) { - assertThat(scanResult.getSubclasses(Object.class.getName()).getNames()).contains(Impl1.class.getName()); + assertThat(scanResult.getSubclasses(Object.class).getNames()).contains(Impl1.class.getName()); } } @@ -32,7 +32,7 @@ public void issue70() { public void issue70EnableExternalClasses() { 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()) .containsOnly(Impl1.class.getName()); } @@ -70,7 +70,7 @@ public void extendsExternalWithEnableExternal() { public void extendsExternalSubclass() { try (ScanResult scanResult = new ClassGraph() .acceptPackages(InternalExtendsExternal.class.getPackage().getName()).scan()) { - assertThat(scanResult.getSubclasses(ExternalSuperclass.class.getName()).getNames()) + assertThat(scanResult.getSubclasses(ExternalSuperclass.class).getNames()) .containsOnly(InternalExtendsExternal.class.getName()); } } @@ -83,7 +83,7 @@ public void nonStrictExtendsExternalSubclass() { try (ScanResult scanResult = new ClassGraph() .acceptPackages(InternalExtendsExternal.class.getPackage().getName()).enableExternalClasses() .scan()) { - assertThat(scanResult.getSubclasses(ExternalSuperclass.class.getName()).getNames()) + assertThat(scanResult.getSubclasses(ExternalSuperclass.class).getNames()) .containsOnly(InternalExtendsExternal.class.getName()); } } 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 421326983..1d60e27c1 100644 --- a/src/test/java/io/github/classgraph/issues/issue101/Issue101Test.java +++ b/src/test/java/io/github/classgraph/issues/issue101/Issue101Test.java @@ -46,7 +46,7 @@ public class Issue101Test { public void nonInheritedAnnotation() { try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue101Test.class.getPackage().getName()) .enableAllInfo().scan()) { - assertThat(scanResult.getClassesWithAnnotation(NonInheritedAnnotation.class.getName()).getNames()) + assertThat(scanResult.getClassesWithAnnotation(NonInheritedAnnotation.class).getNames()) .containsOnly(AnnotatedClass.class.getName()); } } @@ -58,9 +58,8 @@ public void nonInheritedAnnotation() { public void inheritedMetaAnnotation() { try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue101Test.class.getPackage().getName()) .enableAllInfo().scan()) { - assertThat(scanResult.getClassesWithAnnotation(InheritedMetaAnnotation.class.getName()) - .getStandardClasses().getNames()).containsOnly(AnnotatedClass.class.getName(), - NonAnnotatedSubclass.class.getName()); + assertThat(scanResult.getClassesWithAnnotation(InheritedMetaAnnotation.class).getStandardClasses() + .getNames()).containsOnly(AnnotatedClass.class.getName(), NonAnnotatedSubclass.class.getName()); } } @@ -71,9 +70,9 @@ public void inheritedMetaAnnotation() { public void inheritedAnnotation() { try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue101Test.class.getPackage().getName()) .enableAllInfo().scan()) { - assertThat(scanResult.getClassesWithAnnotation(InheritedAnnotation.class.getName()).getNames()) - .containsOnly(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/issue107/Issue107Test.java b/src/test/java/io/github/classgraph/issues/issue107/Issue107Test.java index 629848cac..729c8d85f 100644 --- a/src/test/java/io/github/classgraph/issues/issue107/Issue107Test.java +++ b/src/test/java/io/github/classgraph/issues/issue107/Issue107Test.java @@ -47,14 +47,13 @@ public class Issue107Test { @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().acceptPackages(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()).containsOnly(PackageAnnotation.class.getName()); } 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 89508258f..ee90e9f9a 100644 --- a/src/test/java/io/github/classgraph/issues/issue151/Issue151Test.java +++ b/src/test/java/io/github/classgraph/issues/issue151/Issue151Test.java @@ -62,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 bc4b6abf0..ff5bdac79 100644 --- a/src/test/java/io/github/classgraph/issues/issue152/Issue152Test.java +++ b/src/test/java/io/github/classgraph/issues/issue152/Issue152Test.java @@ -98,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().replace('$', '.') + "[], 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 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 cfb017f06..d3020717b 100644 --- a/src/test/java/io/github/classgraph/issues/issue175/Issue175Test.java +++ b/src/test/java/io/github/classgraph/issues/issue175/Issue175Test.java @@ -181,34 +181,34 @@ public void testResultTypesNotReconciled2() { 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()", - "@org.jetbrains.annotations.NotNull public final java.util.List> getNotaryNodes()", - "@org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode getDefaultNotaryNode()", + "@org.jetbrains.annotations.NotNull public final java.util.List getNodes()", + "@org.jetbrains.annotations.NotNull public final java.util.List> getNotaryNodes()", + "@org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode getDefaultNotaryNode()", "@org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getDefaultNotaryIdentity()", "@org.jetbrains.annotations.NotNull public final net.corda.core.identity.PartyAndCertificate getDefaultNotaryIdentityAndCert()", "private final java.util.List generateNotaryIdentities()", - "@org.jetbrains.annotations.NotNull public java.util.List> createNotaries$node_driver_main()", - "@org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetwork.MockNode createUnstartedNode(@org.jetbrains.annotations.NotNull net.corda.testing.node.MockNodeParameters parameters)", - "@org.jetbrains.annotations.NotNull public static synthetic bridge net.corda.testing.node.MockNetwork.MockNode createUnstartedNode$default(net.corda.testing.node.MockNetwork, net.corda.testing.node.MockNodeParameters, int, java.lang.Object)", - "@org.jetbrains.annotations.NotNull public final N createUnstartedNode(@org.jetbrains.annotations.NotNull net.corda.testing.node.MockNodeParameters parameters, @org.jetbrains.annotations.NotNull kotlin.jvm.functions.Function1 nodeFactory)", - "@org.jetbrains.annotations.NotNull public static synthetic bridge net.corda.testing.node.MockNetwork.MockNode createUnstartedNode$default(net.corda.testing.node.MockNetwork, net.corda.testing.node.MockNodeParameters, kotlin.jvm.functions.Function1, int, java.lang.Object)", - "@org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode createNode(@org.jetbrains.annotations.NotNull net.corda.testing.node.MockNodeParameters parameters)", + "@org.jetbrains.annotations.NotNull public java.util.List> createNotaries$node_driver_main()", + "@org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetwork$MockNode createUnstartedNode(@org.jetbrains.annotations.NotNull net.corda.testing.node.MockNodeParameters parameters)", + "@org.jetbrains.annotations.NotNull public static synthetic bridge net.corda.testing.node.MockNetwork$MockNode createUnstartedNode$default(net.corda.testing.node.MockNetwork, net.corda.testing.node.MockNodeParameters, int, java.lang.Object)", + "@org.jetbrains.annotations.NotNull public final N createUnstartedNode(@org.jetbrains.annotations.NotNull net.corda.testing.node.MockNodeParameters parameters, @org.jetbrains.annotations.NotNull kotlin.jvm.functions.Function1 nodeFactory)", + "@org.jetbrains.annotations.NotNull public static synthetic bridge net.corda.testing.node.MockNetwork$MockNode createUnstartedNode$default(net.corda.testing.node.MockNetwork, net.corda.testing.node.MockNodeParameters, kotlin.jvm.functions.Function1, int, java.lang.Object)", + "@org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode createNode(@org.jetbrains.annotations.NotNull net.corda.testing.node.MockNodeParameters parameters)", "@org.jetbrains.annotations.NotNull public static synthetic bridge net.corda.node.internal.StartedNode createNode$default(net.corda.testing.node.MockNetwork, net.corda.testing.node.MockNodeParameters, int, java.lang.Object)", - "@org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode createNode(@org.jetbrains.annotations.NotNull net.corda.testing.node.MockNodeParameters parameters, @org.jetbrains.annotations.NotNull kotlin.jvm.functions.Function1 nodeFactory)", + "@org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode createNode(@org.jetbrains.annotations.NotNull net.corda.testing.node.MockNodeParameters parameters, @org.jetbrains.annotations.NotNull kotlin.jvm.functions.Function1 nodeFactory)", "@org.jetbrains.annotations.NotNull public static synthetic bridge net.corda.node.internal.StartedNode createNode$default(net.corda.testing.node.MockNetwork, net.corda.testing.node.MockNodeParameters, kotlin.jvm.functions.Function1, int, java.lang.Object)", - "private final N createNodeImpl(net.corda.testing.node.MockNodeParameters parameters, kotlin.jvm.functions.Function1 nodeFactory, boolean start)", + "private final N createNodeImpl(net.corda.testing.node.MockNodeParameters parameters, kotlin.jvm.functions.Function1 nodeFactory, boolean start)", "@org.jetbrains.annotations.NotNull public final java.nio.file.Path baseDirectory(int nodeId)", "@kotlin.jvm.JvmOverloads public final void runNetwork(int rounds)", "@kotlin.jvm.JvmOverloads public static synthetic bridge void runNetwork$default(net.corda.testing.node.MockNetwork, int, int, java.lang.Object)", "@kotlin.jvm.JvmOverloads public final void runNetwork()", - "@kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode createPartyNode(@org.jetbrains.annotations.Nullable net.corda.core.identity.CordaX500Name legalName)", + "@kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode createPartyNode(@org.jetbrains.annotations.Nullable net.corda.core.identity.CordaX500Name legalName)", "@kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public static synthetic bridge net.corda.node.internal.StartedNode createPartyNode$default(net.corda.testing.node.MockNetwork, net.corda.core.identity.CordaX500Name, int, java.lang.Object)", - "@kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode createPartyNode()", - "@org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetwork.MockNode addressToNode(@org.jetbrains.annotations.NotNull net.corda.core.messaging.MessageRecipients msgRecipient)", + "@kotlin.jvm.JvmOverloads @org.jetbrains.annotations.NotNull public final net.corda.node.internal.StartedNode createPartyNode()", + "@org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetwork$MockNode addressToNode(@org.jetbrains.annotations.NotNull net.corda.core.messaging.MessageRecipients msgRecipient)", "public final void startNodes()", "public final void stopNodes()", "public final void waitQuiescent()", - "public (@org.jetbrains.annotations.NotNull java.util.List cordappPackages, @org.jetbrains.annotations.NotNull net.corda.testing.node.MockNetworkParameters defaultParameters, boolean networkSendManuallyPumped, boolean threadPerNode, @org.jetbrains.annotations.NotNull net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy servicePeerAllocationStrategy, @org.jetbrains.annotations.NotNull kotlin.jvm.functions.Function1 defaultFactory, boolean initialiseSerialization, @org.jetbrains.annotations.NotNull java.util.List notarySpecs)", - "public synthetic (java.util.List, net.corda.testing.node.MockNetworkParameters, boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy, kotlin.jvm.functions.Function1, boolean, java.util.List, int, kotlin.jvm.internal.DefaultConstructorMarker)", + "public (@org.jetbrains.annotations.NotNull java.util.List cordappPackages, @org.jetbrains.annotations.NotNull net.corda.testing.node.MockNetworkParameters defaultParameters, boolean networkSendManuallyPumped, boolean threadPerNode, @org.jetbrains.annotations.NotNull net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy servicePeerAllocationStrategy, @org.jetbrains.annotations.NotNull kotlin.jvm.functions.Function1 defaultFactory, boolean initialiseSerialization, @org.jetbrains.annotations.NotNull java.util.List notarySpecs)", + "public synthetic (java.util.List, net.corda.testing.node.MockNetworkParameters, boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, kotlin.jvm.functions.Function1, boolean, java.util.List, int, kotlin.jvm.internal.DefaultConstructorMarker)", "@kotlin.jvm.JvmOverloads public (@org.jetbrains.annotations.NotNull java.util.List cordappPackages, @org.jetbrains.annotations.NotNull net.corda.testing.node.MockNetworkParameters parameters)", "@kotlin.jvm.JvmOverloads public synthetic (java.util.List, net.corda.testing.node.MockNetworkParameters, int, kotlin.jvm.internal.DefaultConstructorMarker)", "@kotlin.jvm.JvmOverloads public (@org.jetbrains.annotations.NotNull java.util.List)", @@ -217,7 +217,7 @@ public void testResultTypesNotReconciled2() { "@org.jetbrains.annotations.NotNull public static final synthetic java.util.List access$getCordappPackages$p(net.corda.testing.node.MockNetwork)", "@org.jetbrains.annotations.NotNull public static final synthetic org.apache.activemq.artemis.utils.ReusableLatch access$getBusyLatch$p(net.corda.testing.node.MockNetwork)", "@org.jetbrains.annotations.NotNull public static final synthetic java.util.concurrent.atomic.AtomicInteger access$getSharedUserCount$p(net.corda.testing.node.MockNetwork)", - "@org.jetbrains.annotations.NotNull public static final synthetic net.corda.testing.node.MockNetwork.sharedServerThread$1 access$getSharedServerThread$p(net.corda.testing.node.MockNetwork)"); + "@org.jetbrains.annotations.NotNull public static final synthetic net.corda.testing.node.MockNetwork$sharedServerThread$1 access$getSharedServerThread$p(net.corda.testing.node.MockNetwork)"); } } @@ -242,8 +242,8 @@ public void testAttributeParameterMismatch() { } 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)", + "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)", "@org.jetbrains.annotations.NotNull public final java.lang.String getColumnName()"); } } @@ -272,8 +272,8 @@ public void testResultTypeReconciliationIssue() { "@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)", @@ -304,7 +304,7 @@ public void testParameterArityMismatch() { } } 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)"); + "@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)"); } } 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 0ead78230..f8818b24a 100644 --- a/src/test/java/io/github/classgraph/issues/issue216/Issue216Test.java +++ b/src/test/java/io/github/classgraph/issues/issue216/Issue216Test.java @@ -54,7 +54,7 @@ public void testSpringBootJarWithLibJars() { 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()).containsOnly(Issue216Test.class.getName()); } 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 6f52e4580..d2415fad0 100644 --- a/src/test/java/io/github/classgraph/issues/issue261/Issue261Test.java +++ b/src/test/java/io/github/classgraph/issues/issue261/Issue261Test.java @@ -64,7 +64,7 @@ private static class Cls extends SuperCls { public void issue261Test() { // 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.getName()).getNames()) + assertThat(scanResult.getSubclasses(SuperSuperCls.class).getNames()) .containsOnly(SuperCls.class.getName(), Cls.class.getName()); } } 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 744448a03..5e188a3c6 100644 --- a/src/test/java/io/github/classgraph/issues/issue289/Issue289.java +++ b/src/test/java/io/github/classgraph/issues/issue289/Issue289Test.java @@ -14,19 +14,15 @@ /** * Issue289. */ -public class Issue289 { - +public class Issue289Test { /** * Issue 289. - * - * @throws Exception - * the exception */ @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/issue305/Issue305.java b/src/test/java/io/github/classgraph/issues/issue305/Issue305.java deleted file mode 100644 index a44e0b3c2..000000000 --- a/src/test/java/io/github/classgraph/issues/issue305/Issue305.java +++ /dev/null @@ -1,88 +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.jupiter.api.Test; - -import io.github.classgraph.ClassGraph; -import io.github.classgraph.ScanResult; - -/** - * Issue305. - */ -public class Issue305 { - - /** - * 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 { - 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") })) - // 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); - - } 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 78% 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 f8048b768..4602a8372 100644 --- a/src/test/java/io/github/classgraph/issues/issue310/Issue310.java +++ b/src/test/java/io/github/classgraph/issues/issue310/Issue310Test.java @@ -10,7 +10,7 @@ /** * Issue310. */ -public class Issue310 { +public class Issue310Test { /** The Constant A. */ static final double A = 3.0; @@ -35,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) - .acceptClasses(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 73% 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 d0016228c..e1056b89f 100644 --- a/src/test/java/io/github/classgraph/issues/issue314/Issue314.java +++ b/src/test/java/io/github/classgraph/issues/issue314/Issue314Test.java @@ -10,7 +10,7 @@ /** * Issue314. */ -public class Issue314 { +public class Issue314Test { /** * The Class A. */ @@ -32,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) - .acceptPackages(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); @@ -44,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 89% 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 b207e598d..1ff94c85c 100644 --- a/src/test/java/io/github/classgraph/issues/issue318/Issue318.java +++ b/src/test/java/io/github/classgraph/issues/issue318/Issue318Test.java @@ -16,7 +16,7 @@ /** * Unit test. */ -public class Issue318 { +public class Issue318Test { /** * The Interface MyAnn. */ @@ -75,14 +75,14 @@ class With3MyAnn { */ @Test public void issue318() { - try (final ScanResult scanResult = new ClassGraph().acceptPackages(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/Issue329.java b/src/test/java/io/github/classgraph/issues/issue329/Issue329Test.java similarity index 92% rename from src/test/java/io/github/classgraph/issues/issue329/Issue329.java rename to src/test/java/io/github/classgraph/issues/issue329/Issue329Test.java index 25b19ed10..1c2c1d895 100644 --- a/src/test/java/io/github/classgraph/issues/issue329/Issue329.java +++ b/src/test/java/io/github/classgraph/issues/issue329/Issue329Test.java @@ -11,7 +11,7 @@ /** * Unit test. */ -public class Issue329 { +public class Issue329Test { /** The Class Foo. */ public class Foo { /** Constructor. */ @@ -30,7 +30,7 @@ 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(Issue329.class.getName(), + assertThat(classInfo.getClassDependencies().getNames()).containsOnly(Issue329Test.class.getName(), Bar.class.getName()); } } diff --git a/src/test/java/io/github/classgraph/issues/issue339/Issue339.java b/src/test/java/io/github/classgraph/issues/issue339/Issue339Test.java similarity index 98% rename from src/test/java/io/github/classgraph/issues/issue339/Issue339.java rename to src/test/java/io/github/classgraph/issues/issue339/Issue339Test.java index d4cb248cc..b9c864c31 100644 --- a/src/test/java/io/github/classgraph/issues/issue339/Issue339.java +++ b/src/test/java/io/github/classgraph/issues/issue339/Issue339Test.java @@ -18,7 +18,7 @@ /** * Unit test. */ -public class Issue339 { +public class Issue339Test { /** * Grade. */ diff --git a/src/test/java/io/github/classgraph/issues/issue340/Issue340.java b/src/test/java/io/github/classgraph/issues/issue340/Issue340Test.java similarity index 98% rename from src/test/java/io/github/classgraph/issues/issue340/Issue340.java rename to src/test/java/io/github/classgraph/issues/issue340/Issue340Test.java index 113d49fe0..52b836d54 100644 --- a/src/test/java/io/github/classgraph/issues/issue340/Issue340.java +++ b/src/test/java/io/github/classgraph/issues/issue340/Issue340Test.java @@ -16,7 +16,7 @@ /** * Unit test. */ -public class Issue340 { +public class Issue340Test { /** Test. */ @Test public void test() { diff --git a/src/test/java/io/github/classgraph/issues/issue345/Issue345.java b/src/test/java/io/github/classgraph/issues/issue345/Issue345Test.java similarity index 90% rename from src/test/java/io/github/classgraph/issues/issue345/Issue345.java rename to src/test/java/io/github/classgraph/issues/issue345/Issue345Test.java index ee8ee9a3c..32c9aa186 100644 --- a/src/test/java/io/github/classgraph/issues/issue345/Issue345.java +++ b/src/test/java/io/github/classgraph/issues/issue345/Issue345Test.java @@ -15,7 +15,7 @@ /** * Issue345. */ -public class Issue345 { +public class Issue345Test { /** * Superclass. */ @@ -81,7 +81,7 @@ public void testExtensionToParent() { public void testExtensionToOuterClass() { try (ScanResult scanResult = new ClassGraph().acceptClasses(Super.class.getName()).ignoreClassVisibility() .scan()) { - final ClassInfo outerClassInfo = scanResult.getClassInfo(Issue345.class.getName()); + final ClassInfo outerClassInfo = scanResult.getClassInfo(Issue345Test.class.getName()); assertThat(outerClassInfo).isNotNull(); assertThat(outerClassInfo.getResource()).isNotNull(); } @@ -92,7 +92,7 @@ public void testExtensionToOuterClass() { */ @Test public void testNonExtensionToInnerClass() { - try (ScanResult scanResult = new ClassGraph().acceptClasses(Issue345.class.getName()) + try (ScanResult scanResult = new ClassGraph().acceptClasses(Issue345Test.class.getName()) .ignoreClassVisibility().scan()) { final ClassInfo innerClassInfo = scanResult.getClassInfo(Super.class.getName()); assertThat(innerClassInfo).isNotNull(); @@ -102,23 +102,20 @@ public void testNonExtensionToInnerClass() { /** * Test that overriding classloaders does not allow other classloaders to be scanned. - * - * @throws Exception - * the exception */ @Test - public void issue345b() throws Exception { + public void issue345b() { // Find URL of this class' classpath element URL classpathURL; - try (ScanResult scanResult = new ClassGraph().acceptClasses(Issue345.class.getName()).scan()) { - classpathURL = scanResult.getClassInfo(Issue345.class.getName()).getClasspathElementURL(); + 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(Issue345.class.getName())).isNotNull(); + 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(); } @@ -148,7 +145,7 @@ public static class C extends B { @Test public void issue345c() { try (ScanResult scanResult = new ClassGraph().enableClassInfo() - .acceptPackages(Issue345.class.getPackage().getName()).ignoreClassVisibility().scan()) { + .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()); diff --git a/src/test/java/io/github/classgraph/issues/issue348/Issue348.java b/src/test/java/io/github/classgraph/issues/issue348/Issue348Test.java similarity index 98% rename from src/test/java/io/github/classgraph/issues/issue348/Issue348.java rename to src/test/java/io/github/classgraph/issues/issue348/Issue348Test.java index 8f7dfd850..f0a89abb6 100644 --- a/src/test/java/io/github/classgraph/issues/issue348/Issue348.java +++ b/src/test/java/io/github/classgraph/issues/issue348/Issue348Test.java @@ -16,7 +16,7 @@ /** * Issue345. */ -public class Issue348 { +public class Issue348Test { /** Test for wildcarded jars. */ @Test public void testWildcard() { diff --git a/src/test/java/io/github/classgraph/issues/issue350/Issue350.java b/src/test/java/io/github/classgraph/issues/issue350/Issue350Test.java similarity index 88% rename from src/test/java/io/github/classgraph/issues/issue350/Issue350.java rename to src/test/java/io/github/classgraph/issues/issue350/Issue350Test.java index 5cbb2729e..6f7405cbc 100644 --- a/src/test/java/io/github/classgraph/issues/issue350/Issue350.java +++ b/src/test/java/io/github/classgraph/issues/issue350/Issue350Test.java @@ -13,7 +13,7 @@ /** * Unit test. */ -public class Issue350 { +public class Issue350Test { /** * The Interface SuperclassAnnotation. @@ -68,19 +68,19 @@ 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(Issue350.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue350Test.class.getPackage().getName()) .enableClassInfo().enableFieldInfo().enableMethodInfo().enableAnnotationInfo().scan()) { - assertThat(scanResult.getClassesWithFieldAnnotation(SuperclassAnnotation.class.getName()).getNames()) + assertThat(scanResult.getClassesWithFieldAnnotation(SuperclassAnnotation.class).getNames()) .containsOnly(Pub.class.getName(), PubSub.class.getName()); - assertThat(scanResult.getClassesWithMethodAnnotation(SuperclassAnnotation.class.getName()).getNames()) + assertThat(scanResult.getClassesWithMethodAnnotation(SuperclassAnnotation.class).getNames()) .containsOnly(Pub.class.getName(), PubSub.class.getName()); } - try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue350.class.getPackage().getName()) + try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue350Test.class.getPackage().getName()) .enableClassInfo().enableFieldInfo().enableMethodInfo().enableAnnotationInfo() .ignoreFieldVisibility().ignoreMethodVisibility().scan()) { - assertThat(scanResult.getClassesWithFieldAnnotation(SuperclassAnnotation.class.getName()).getNames()) + assertThat(scanResult.getClassesWithFieldAnnotation(SuperclassAnnotation.class).getNames()) .containsOnly(Pub.class.getName(), PubSub.class.getName(), Priv.class.getName()); - assertThat(scanResult.getClassesWithMethodAnnotation(SuperclassAnnotation.class.getName()).getNames()) + 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/Issue352.java b/src/test/java/io/github/classgraph/issues/issue352/Issue352Test.java similarity index 96% rename from src/test/java/io/github/classgraph/issues/issue352/Issue352.java rename to src/test/java/io/github/classgraph/issues/issue352/Issue352Test.java index 0d2a10ce3..795b50de9 100644 --- a/src/test/java/io/github/classgraph/issues/issue352/Issue352.java +++ b/src/test/java/io/github/classgraph/issues/issue352/Issue352Test.java @@ -15,7 +15,7 @@ /** * Unit test. */ -public class Issue352 { +public class Issue352Test { /** * Test *. @@ -46,7 +46,7 @@ public void test() throws IOException { .enableClassInfo().scan()) { assertThat(scanResult.getAllResources().getPaths()).contains(pkgInfoPath); } - try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue352.class.getPackage().getName()) + 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/Issue355.java b/src/test/java/io/github/classgraph/issues/issue355/Issue355Test.java similarity index 95% rename from src/test/java/io/github/classgraph/issues/issue355/Issue355.java rename to src/test/java/io/github/classgraph/issues/issue355/Issue355Test.java index 1f8042da9..adb2df02d 100644 --- a/src/test/java/io/github/classgraph/issues/issue355/Issue355.java +++ b/src/test/java/io/github/classgraph/issues/issue355/Issue355Test.java @@ -20,7 +20,7 @@ /** * Unit test. */ -public class Issue355 { +public class Issue355Test { /** * Annotation parameter class. @@ -67,7 +67,7 @@ public void y(final X[] x) { @Test public void test() throws IOException { try (ScanResult scanResult = new ClassGraph() - .acceptPackagesNonRecursive(Issue355.class.getPackage().getName()).enableClassInfo() + .acceptPackagesNonRecursive(Issue355Test.class.getPackage().getName()).enableClassInfo() .enableInterClassDependencies().scan()) { final ClassInfo y = scanResult.getClassInfo(Y.class.getName()); final ClassInfo x = scanResult.getClassInfo(X.class.getName()); diff --git a/src/test/java/io/github/classgraph/issues/issue370/Issue370Test.java b/src/test/java/io/github/classgraph/issues/issue370/Issue370Test.java index c099dc214..24d88eebb 100644 --- a/src/test/java/io/github/classgraph/issues/issue370/Issue370Test.java +++ b/src/test/java/io/github/classgraph/issues/issue370/Issue370Test.java @@ -54,7 +54,7 @@ public void issue370Test() { 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.getName()); + 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/issue38/Issue38Test.java b/src/test/java/io/github/classgraph/issues/issue38/Issue38Test.java index 402e3bf22..f42ebf12d 100644 --- a/src/test/java/io/github/classgraph/issues/issue38/Issue38Test.java +++ b/src/test/java/io/github/classgraph/issues/issue38/Issue38Test.java @@ -29,7 +29,7 @@ public static abstract class AnnotationLiteral implements void testImplementsSuppressWarnings() { try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue38Test.class.getPackage().getName()) .scan()) { - assertThat(scanResult.getClassesImplementing(SuppressWarnings.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(SuppressWarnings.class).getNames()) .containsOnly(ImplementsSuppressWarnings.class.getName()); } } diff --git a/src/test/java/io/github/classgraph/issues/issue402/TypeAnnotationTest.java b/src/test/java/io/github/classgraph/issues/issue402/TypeAnnotationTest.java index a895adb16..5709d4c0a 100644 --- a/src/test/java/io/github/classgraph/issues/issue402/TypeAnnotationTest.java +++ b/src/test/java/io/github/classgraph/issues/issue402/TypeAnnotationTest.java @@ -164,33 +164,33 @@ void typeAnnotations() { .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(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"); + .isEqualTo("Outer$@A MiddleStatic$@B Inner2 inner2"); assertThat(shortNames(classInfo.getFieldInfo("inner3"))) - .isEqualTo("Outer.MiddleStatic.@A InnerStatic inner3"); + .isEqualTo("Outer$MiddleStatic$@A InnerStatic inner3"); assertThat(shortNames(classInfo.getFieldInfo("inner4"))) - .isEqualTo("Outer.MiddleGeneric<@A Foo.@B Bar>.InnerGeneric<@D String @C []> 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(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("xyz2"))).isEqualTo("List<@A X2$@B Y2$@C Z2> xyz2"); - assertThat(shortNames(classInfo.getFieldInfo("xyz3"))).isEqualTo("List xyz3"); + assertThat(shortNames(classInfo.getFieldInfo("xyz3"))).isEqualTo("List xyz3"); - assertThat(shortNames(classInfo.getFieldInfo("xyz4"))).isEqualTo("List xyz4"); + assertThat(shortNames(classInfo.getFieldInfo("xyz4"))).isEqualTo("List xyz4"); assertThat(shortNames(classInfo.getMethodInfo("t").get(0))) - .isEqualTo("<@A T extends @B U> @D U t(@E T)"); + .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(@E T)"); + .isEqualTo("<@A T extends @B U> @D U t(final @E T t)"); final ClassInfo personClassInfo = scanResult.getClassInfo(Person.class.getName()); diff --git a/src/test/java/io/github/classgraph/issues/issue420/Issue420Test.java b/src/test/java/io/github/classgraph/issues/issue420/Issue420Test.java index 8e53da1eb..f4cecb075 100644 --- a/src/test/java/io/github/classgraph/issues/issue420/Issue420Test.java +++ b/src/test/java/io/github/classgraph/issues/issue420/Issue420Test.java @@ -96,7 +96,7 @@ private void testDir(final String packageRootPrefix) throws IOException, URISynt final String packagePath = packageName.replace('.', '/'); final String classFullyQualifiedName = packageName + ".CompiledWithJDK8"; final String classFilePath = classFullyQualifiedName.replace('.', '/') + ".class"; - final Path jarPath = Paths.get(getClass().getClassLoader().getResource(classFilePath).toURI()); + 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"); 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 a76cb62fa..3b9f79483 100644 --- a/src/test/java/io/github/classgraph/issues/issue46/Issue46Test.java +++ b/src/test/java/io/github/classgraph/issues/issue46/Issue46Test.java @@ -46,7 +46,7 @@ public class Issue46Test { public void issue46Test() { final String jarPath = "jar:file://" + Issue46Test.class.getClassLoader().getResource("nested-jars-level1.zip").getPath() - + "!level2.jar!level3.jar!classpath1/classpath2"; + + "!/level2.jar!/level3.jar!/classpath1/classpath2"; try (ScanResult scanResult = new ClassGraph().overrideClasspath(jarPath).enableClassInfo().scan()) { assertThat(scanResult.getAllClasses().getNames()).containsOnly("com.test.Test"); } 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 77be1fa95..99a8e6f6f 100644 --- a/src/test/java/io/github/classgraph/issues/issue74/Issue74Test.java +++ b/src/test/java/io/github/classgraph/issues/issue74/Issue74Test.java @@ -42,7 +42,7 @@ public class ImplementsFunction implements Function { public void issue74() { try (ScanResult scanResult = new ClassGraph().acceptPackages(Issue74Test.class.getPackage().getName()) .scan()) { - assertThat(scanResult.getClassesImplementing(Function.class.getName()).getNames()).containsOnly( + 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/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/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/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 89% 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 54ae8b607..cf03090f8 100644 --- a/src/test/java/io/github/classgraph/issues/issue93/Issue93.java +++ b/src/test/java/io/github/classgraph/issues/issue93/Issue93Test.java @@ -13,9 +13,9 @@ /** * 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. @@ -50,9 +50,9 @@ static class RetentionRuntimeAnnotated { public void classRetentionIsDefault() { try (ScanResult scanResult = new ClassGraph().acceptPackages(PKG).enableAnnotationInfo() .ignoreClassVisibility().scan()) { - assertThat(scanResult.getClassesWithAnnotation(RetentionClass.class.getName()).getNames()) + assertThat(scanResult.getClassesWithAnnotation(RetentionClass.class).getNames()) .containsOnly(RetentionClassAnnotated.class.getName()); - assertThat(scanResult.getClassesWithAnnotation(RetentionRuntime.class.getName()).getNames()) + assertThat(scanResult.getClassesWithAnnotation(RetentionRuntime.class).getNames()) .containsOnly(RetentionRuntimeAnnotated.class.getName()); } } @@ -65,8 +65,8 @@ public void classRetentionIsDefault() { public void classRetentionIsNotVisibleWithRetentionPolicyRUNTIME() { 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()) + 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/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 0558e6957..5f0e2a986 100644 --- a/src/test/java/io/github/classgraph/json/JSONSerializationTest.java +++ b/src/test/java/io/github/classgraph/json/JSONSerializationTest.java @@ -13,6 +13,7 @@ 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; /** * JSONSerializationTest. @@ -252,6 +253,7 @@ 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 = // diff --git a/src/test/java/io/github/classgraph/test/ClassGraphTest.java b/src/test/java/io/github/classgraph/test/ClassGraphTest.java index 1a4ab2170..eeeed4ab0 100644 --- a/src/test/java/io/github/classgraph/test/ClassGraphTest.java +++ b/src/test/java/io/github/classgraph/test/ClassGraphTest.java @@ -125,7 +125,7 @@ public void scanWithAcceptAndReject() { @Test public void scanSubAndSuperclasses() { try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { - final List subclasses = scanResult.getSubclasses(Cls.class.getName()).getNames(); + 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()); @@ -142,7 +142,7 @@ public void scanSubAndSuperclasses() { @Test public void scanSubAndSuperinterface() { try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { - final List subinterfaces = scanResult.getClassesImplementing(Iface.class.getName()).getNames(); + 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()); @@ -159,47 +159,45 @@ public void scanSubAndSuperinterface() { @Test public void scanTransitiveImplements() { try (ScanResult scanResult = new ClassGraph().acceptPackages(ACCEPT_PACKAGE).scan()) { - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()) + 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()) + assertThat(scanResult.getClassesImplementing(Iface.class).getNames()).contains(Impl1.class.getName()); + assertThat(scanResult.getClassesImplementing(IfaceSub.class).getNames()) .contains(Impl1.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSub.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(IfaceSubSub.class).getNames()) .contains(Impl1.class.getName()); - assertThat(scanResult.getClassesImplementing(IfaceSubSub.class.getName()).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()); } } @@ -212,9 +210,9 @@ public void testExternalSuperclassReturned() { 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.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesImplementing(AcceptedInterface.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesImplementing(AcceptedInterface.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(); } } @@ -236,7 +234,7 @@ public void testAcceptedWithoutExceptionWithoutStrictAccept() { 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.getName()).getNames()) + assertThat(scanResult.getClassesWithAnnotation(RejectedAnnotation.class).getNames()) .containsExactly(Accepted.class.getName()); } } @@ -249,9 +247,9 @@ 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.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesImplementing(AcceptedInterface.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesImplementing(AcceptedInterface.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(); } } @@ -299,11 +297,10 @@ public void testRejectedPackage() { try (ScanResult scanResult = new ClassGraph() .acceptPackages(ROOT_PACKAGE, "-" + RejectedSuperclass.class.getPackage().getName()).scan()) { assertThat(scanResult.getSuperclasses(Accepted.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getSubclasses(Accepted.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesImplementing(AcceptedInterface.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesImplementing(AcceptedInterface.class.getName()).getNames()).isEmpty(); - assertThat(scanResult.getClassesWithAnnotation(RejectedAnnotation.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(); } } @@ -337,13 +334,13 @@ 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.getName()).getNames()) + assertThat(scanResult.getSubclasses(Accepted.class).getNames()) .containsExactly(RejectedSubclass.class.getName()); - assertThat(scanResult.getClassesImplementing(AcceptedInterface.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()) .containsExactly(RejectedSubinterface.class.getName()); - assertThat(scanResult.getClassesImplementing(AcceptedInterface.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(AcceptedInterface.class).getNames()) .containsExactly(RejectedSubinterface.class.getName()); - assertThat(scanResult.getClassesWithAnnotation(RejectedAnnotation.class.getName()).getNames()) + assertThat(scanResult.getClassesWithAnnotation(RejectedAnnotation.class).getNames()) .containsExactly(Accepted.class.getName()); } } diff --git a/src/test/java/io/github/classgraph/test/ClassInfoTest.java b/src/test/java/io/github/classgraph/test/ClassInfoTest.java index 52ddc8a6a..546db809f 100644 --- a/src/test/java/io/github/classgraph/test/ClassInfoTest.java +++ b/src/test/java/io/github/classgraph/test/ClassInfoTest.java @@ -104,7 +104,7 @@ public boolean accept(final ClassInfo ci) { */ @Test public void implementsInterfaceDirect() { - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).directOnly().getNames()) + assertThat(scanResult.getClassesImplementing(Iface.class).directOnly().getNames()) .containsOnly(IfaceSub.class.getName(), Impl2.class.getName()); } @@ -113,8 +113,8 @@ public void implementsInterfaceDirect() { */ @Test public void implementsInterface() { - assertThat(scanResult.getClassesImplementing(Iface.class.getName()).getNames()).containsOnly( - 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()); } 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 133cfea2c..076118a3b 100644 --- a/src/test/java/io/github/classgraph/test/classrefannotation/AnnotationClassRefTest.java +++ b/src/test/java/io/github/classgraph/test/classrefannotation/AnnotationClassRefTest.java @@ -85,8 +85,7 @@ public void testClassRefAnnotation() { try (ScanResult scanResult = new ClassGraph() .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/fieldannotation/FieldAndMethodAnnotationTest.java b/src/test/java/io/github/classgraph/test/fieldannotation/FieldAndMethodAnnotationTest.java index 360ee5e01..2c535d1cb 100644 --- a/src/test/java/io/github/classgraph/test/fieldannotation/FieldAndMethodAnnotationTest.java +++ b/src/test/java/io/github/classgraph/test/fieldannotation/FieldAndMethodAnnotationTest.java @@ -60,8 +60,8 @@ public void testGetNamesOfClassesWithFieldAnnotation() { try (ScanResult scanResult = new ClassGraph() .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(); } } @@ -74,8 +74,8 @@ public void testGetNamesOfClassesWithFieldAnnotationIgnoringVisibility() { try (ScanResult scanResult = new ClassGraph() .acceptPackages(FieldAndMethodAnnotationTest.class.getPackage().getName()).enableFieldInfo() .ignoreFieldVisibility().enableAnnotationInfo().scan()) { - final List testClasses = scanResult - .getClassesWithFieldAnnotation(ExternalAnnotation.class.getName()).getNames(); + final List testClasses = scanResult.getClassesWithFieldAnnotation(ExternalAnnotation.class) + .getNames(); assertThat(testClasses).containsOnly(FieldAndMethodAnnotationTest.class.getName()); } } @@ -89,8 +89,8 @@ public void testGetNamesOfClassesWithMethodAnnotation() { try (ScanResult scanResult = new ClassGraph() .acceptPackages(FieldAndMethodAnnotationTest.class.getPackage().getName()).enableMethodInfo() .enableAnnotationInfo().scan()) { - final List testClasses = scanResult - .getClassesWithMethodAnnotation(ExternalAnnotation.class.getName()).getNames(); + final List testClasses = scanResult.getClassesWithMethodAnnotation(ExternalAnnotation.class) + .getNames(); assertThat(testClasses).containsOnly(FieldAndMethodAnnotationTest.class.getName()); } } 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 9ce34401b..b70710262 100644 --- a/src/test/java/io/github/classgraph/test/internal/InternalExternalTest.java +++ b/src/test/java/io/github/classgraph/test/internal/InternalExternalTest.java @@ -55,13 +55,13 @@ public void testAcceptingExternalClassesWithoutEnablingExternalClasses() { assertThat(scanResult.getAllStandardClasses().getNames()).containsOnly( InternalExternalTest.class.getName(), InternalExtendsExternal.class.getName(), InternalImplementsExternal.class.getName(), InternalAnnotatedByExternal.class.getName()); - assertThat(scanResult.getSubclasses(ExternalSuperclass.class.getName()).getNames()) + assertThat(scanResult.getSubclasses(ExternalSuperclass.class).getNames()) .containsOnly(InternalExtendsExternal.class.getName()); assertThat(scanResult.getAllInterfaces()).isEmpty(); - assertThat(scanResult.getClassesImplementing(ExternalInterface.class.getName()).getNames()) + assertThat(scanResult.getClassesImplementing(ExternalInterface.class).getNames()) .containsOnly(InternalImplementsExternal.class.getName()); assertThat(scanResult.getAllAnnotations().getNames()).isEmpty(); - assertThat(scanResult.getClassesWithAnnotation(ExternalAnnotation.class.getName()).getNames()) + assertThat(scanResult.getClassesWithAnnotation(ExternalAnnotation.class).getNames()) .containsOnly(InternalAnnotatedByExternal.class.getName()); } } @@ -75,14 +75,14 @@ public void testIncludeReferencedClasses() { .acceptPackages(InternalExternalTest.class.getPackage().getName()).enableAllInfo().scan()) { assertThat(scanResult.getAllStandardClasses().getNames()) .doesNotContain(ExternalSuperclass.class.getName()); - assertThat(scanResult.getSubclasses(ExternalSuperclass.class.getName()).getNames()) + 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()) + 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()) + assertThat(scanResult.getClassesWithAnnotation(ExternalAnnotation.class).getNames()) .containsOnly(InternalAnnotatedByExternal.class.getName()); } } 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 c7f5bde00..69c88195b 100644 --- a/src/test/java/io/github/classgraph/test/methodannotation/MethodAnnotationTest.java +++ b/src/test/java/io/github/classgraph/test/methodannotation/MethodAnnotationTest.java @@ -53,8 +53,8 @@ public void testGetNamesOfClassesWithMethodAnnotation() { try (ScanResult scanResult = new ClassGraph() .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(); } } @@ -68,7 +68,7 @@ public void testGetNamesOfClassesWithMethodAnnotationIgnoringVisibility() { .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).containsOnly(MethodAnnotationTest.class.getName()); boolean found = false; 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 2dae751f4..228cffd25 100644 --- a/src/test/java/io/github/classgraph/test/methodannotation2/TestMethodMetaAnnotation.java +++ b/src/test/java/io/github/classgraph/test/methodannotation2/TestMethodMetaAnnotation.java @@ -107,8 +107,7 @@ public void testMetaAnnotation() { try (ScanResult scanResult = new ClassGraph() .acceptPackages(TestMethodMetaAnnotation.class.getPackage().getName()).enableAnnotationInfo() .scan()) { - final List testClasses = scanResult.getClassesWithAnnotation(MetaAnnotation.class.getName()) - .getNames(); + final List testClasses = scanResult.getClassesWithAnnotation(MetaAnnotation.class).getNames(); assertThat(testClasses).containsOnly(MethodAnnotation.class.getName(), ClassAnnotation.class.getName(), MetaAnnotatedClass.class.getName()); } @@ -123,7 +122,7 @@ public void testMetaAnnotationStandardClassesOnly() { try (ScanResult scanResult = new ClassGraph() .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).containsOnly(MetaAnnotatedClass.class.getName()); } @@ -138,8 +137,8 @@ public void testMethodMetaAnnotation() { try (ScanResult scanResult = new ClassGraph() .acceptPackages(TestMethodMetaAnnotation.class.getPackage().getName()).enableMethodInfo() .enableAnnotationInfo().scan()) { - final List testClasses = scanResult - .getClassesWithMethodAnnotation(MetaAnnotation.class.getName()).getNames(); + 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 2a722c2f3..e7eecc145 100644 --- a/src/test/java/io/github/classgraph/test/methodinfo/MethodInfoTest.java +++ b/src/test/java/io/github/classgraph/test/methodinfo/MethodInfoTest.java @@ -54,7 +54,10 @@ public class MethodInfoTest { /** * The Class X. */ - public static class X { + public static class X extends Exception { + /***/ + private static final long serialVersionUID = 1L; + /** * Method. */ @@ -99,6 +102,12 @@ private static String[] privateMethod() { return null; } + public void throwsException() throws X { + } + + public void throwsGenericException() throws X, X2 { + } + /** * Method info not enabled. */ @@ -129,14 +138,18 @@ public boolean accept(final MethodInfo methodInfo) { } }).getAsStrings()).containsOnly( // "@" + ExternalAnnotation.class.getName() // - + " public final int publicMethodWithArgs" - + "(java.lang.String, char, long, float[], byte[][], " - + "java.util.List, " + X.class.getName().replace('$', '.') - + "[][][], java.lang.String[]...)", + + " 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()"); } } @@ -170,15 +183,19 @@ public boolean accept(final MethodInfo methodInfo) { } }).getAsStrings()).containsOnly( // "@" + ExternalAnnotation.class.getName() // - + " public final int publicMethodWithArgs" - + "(java.lang.String, char, long, float[], byte[][], " - + "java.util.List, " + X.class.getName().replace('$', '.') - + "[][][], java.lang.String[]...)", + + " 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()", + "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()"); } } @@ -207,7 +224,7 @@ public void testMethodInfoLoadMethodForArrayArg() { } } assertThat(arrayClassInfoList.toString()).isEqualTo("[class float[], class byte[][], " + "class " - + X.class.getName().replace('$', '.') + "[][][], " + "class java.lang.String[][]]"); + + 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); @@ -226,4 +243,21 @@ public void testMethodInfoLoadMethodForArrayArg() { 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 index 53e3399c1..a2bb6aaa2 100644 --- a/src/test/java/io/github/classgraph/test/parameterannotation/RetentionPolicyForFunctionParameterAnnotationsTest.java +++ b/src/test/java/io/github/classgraph/test/parameterannotation/RetentionPolicyForFunctionParameterAnnotationsTest.java @@ -112,7 +112,7 @@ public void canDetect_ParameterAnnotation_WithRuntimeRetention() { final MethodInfo methodInfo = classInfo.getMethodInfo() .getSingleMethod("parameterAnnotation_WithRuntimeRetention"); - assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class.getName())).isTrue(); + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); } /** @@ -142,9 +142,9 @@ public void canDetect_TwoAnnotations_WithRuntimeRetention_ForSingleParam() { final MethodInfo methodInfo = classInfo.getMethodInfo() .getSingleMethod("twoAnnotations_WithRuntimeRetention_ForSingleParam"); - assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class.getName())).isTrue(); + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); - assertThat(methodInfo.hasParameterAnnotation(SecondParamAnnoRuntime.class.getName())).isTrue(); + assertThat(methodInfo.hasParameterAnnotation(SecondParamAnnoRuntime.class)).isTrue(); } /** @@ -168,7 +168,7 @@ public void canDetect_ParameterAnnotation_OneRuntimeRetention_OneClassRetention( final MethodInfo methodInfo = classInfo.getMethodInfo() .getSingleMethod("oneRuntimeRetention_OneClassRetention"); - assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class.getName())).isTrue(); + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); } /** @@ -193,7 +193,7 @@ public void canDetect_ParameterAnnotation_OneRuntimeRetention_OneClassRetention_ final MethodInfo methodInfo = classInfo.getMethodInfo() .getSingleMethod("oneRuntimeRetention_OneClassRetention_ChangedAnnotationOrder"); - assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class.getName())).isTrue(); + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); } /** @@ -217,7 +217,7 @@ public void canDetect_ParameterAnnotation_OneRuntimeRetention_OneSourceRetention final MethodInfo methodInfo = classInfo.getMethodInfo() .getSingleMethod("oneRuntimeRetention_OneSourceRetention"); - assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class.getName())).isTrue(); + assertThat(methodInfo.hasParameterAnnotation(ParamAnnoRuntime.class)).isTrue(); } /** 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 94c1ee508..18beaa73e 100644 --- a/src/test/java/io/github/classgraph/test/utils/LogNodeTest.java +++ b/src/test/java/io/github/classgraph/test/utils/LogNodeTest.java @@ -8,6 +8,7 @@ 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; @@ -17,42 +18,43 @@ * 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/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/perf/io/github/classgraph/issues/issue400/Issue400.java b/src/test/perf/io/github/classgraph/issues/issue400/Issue400.java index 8cb370b6b..ece4090ff 100644 --- a/src/test/perf/io/github/classgraph/issues/issue400/Issue400.java +++ b/src/test/perf/io/github/classgraph/issues/issue400/Issue400.java @@ -57,15 +57,19 @@ private void loadsJarWithManyNestedEntriesAndDoesNotUseMuchMemory(final URL... j } final long ramAtEnd = usedRam(); - 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 < 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)); + } - 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)); + 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)); + } } /** 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/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