Merge pull request #977 from corda/chrisr3-os-merge

Merge from OS up to 8087f3c5d3c823a5fbda9a728f129b0925c1fddb.
This commit is contained in:
Chris Rankin 2018-06-12 18:45:12 +01:00 committed by GitHub
commit 2522cee010
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
377 changed files with 12434 additions and 170 deletions

View File

@ -61,12 +61,14 @@ see changes to this list.
* cncorda
* Credit Suisse
* cyrsis
* Dan Newton (Accenture)
* Daniel Roig (SEB)
* Dave Hudson (R3)
* David John Grundy (Dankse Bank)
* David Lee (BCS)
* Dirk Hermans (KBC)
* Edward Greenwood (State Street)
* Elendu Uche (APPZONE)
* Farzad Pezeshkpour (RBS)
* fracting
* Frederic Dalibard (Natixis)

View File

@ -91,6 +91,7 @@ buildscript {
ext.ghostdriver_version = '2.1.0'
ext.eaagentloader_version = '1.0.3'
ext.curator_version = '4.0.0'
ext.proguard_version = constants.getProperty('proguardVersion')
ext.jsch_version = '0.1.54'
ext.protonj_version = '0.27.1'
ext.commons_cli_version = '1.4'
@ -98,6 +99,8 @@ buildscript {
ext.fast_classpath_scanner_version = '2.12.3'
ext.jcabi_manifests_version = '1.1'
ext.deterministic_rt_version = '1.0-SNAPSHOT'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131'
@ -122,6 +125,7 @@ buildscript {
classpath "net.corda.plugins:cordformation:$gradle_plugins_version"
classpath "net.corda.plugins:cordapp:$gradle_plugins_version"
classpath "net.corda.plugins:api-scanner:$gradle_plugins_version"
classpath "net.sf.proguard:proguard-gradle:$proguard_version"
classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0'
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
@ -160,7 +164,6 @@ targetCompatibility = 1.8
allprojects {
apply plugin: 'kotlin'
apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'org.owasp.dependencycheck'
apply plugin: 'kotlin-allopen'
@ -208,7 +211,7 @@ allprojects {
tasks.withType(Test) {
// Prevent the project from creating temporary files outside of the build directory.
systemProperties['java.io.tmpdir'] = buildDir
systemProperty 'java.io.tmpdir', buildDir.absolutePath
// Ensures that "net.corda.testing.amqp.enable" is passed correctly from Gradle command line
// down to JVM executing unit test. It looks like we are running unit tests in the forked mode
@ -310,6 +313,7 @@ if (!JavaVersion.current().java8Compatible)
throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_$java8_minUpdateVersion")
repositories {
mavenLocal()
mavenCentral()
jcenter()
}
@ -380,7 +384,34 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda'
gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities', 'corda-launcher', 'corda-shell', 'corda-serialization', 'corda-bridgeserver', 'corda-ptflows', 'jmeter-corda', 'tools-database-migration', 'tools-network-bootstrapper', 'tools-blob-inspector']
publications = [
'corda-jfx',
'corda-mock',
'corda-rpc',
'corda-core',
'corda-core-deterministic',
'corda',
'corda-finance',
'corda-node',
'corda-node-api',
'corda-test-common',
'corda-test-utils',
'corda-jackson',
'corda-webserver-impl',
'corda-webserver',
'corda-node-driver',
'corda-confidential-identities',
'corda-launcher',
'corda-shell',
'corda-serialization',
'corda-serialization-deterministic',
'corda-bridgeserver',
'corda-ptflows',
'jmeter-corda',
'tools-database-migration',
'tools-blob-inspector',
'tools-network-bootstrapper'
]
license {
name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0'

View File

@ -12,7 +12,24 @@ buildscript {
Properties constants = new Properties()
file("../constants.properties").withInputStream { constants.load(it) }
ext.guava_version = constants.getProperty("guavaVersion")
ext {
guava_version = constants.getProperty("guavaVersion")
kotlin_version = constants.getProperty("kotlinVersion")
proguard_version = constants.getProperty("proguardVersion")
assertj_version = '3.9.1'
junit_version = '4.12'
asm_version = '6.2'
}
repositories {
mavenLocal()
mavenCentral()
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "net.sf.proguard:proguard-gradle:$proguard_version"
}
}
apply plugin: 'maven'
@ -23,6 +40,27 @@ repositories {
mavenCentral()
}
allprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
languageVersion = "1.2"
apiVersion = "1.2"
jvmTarget = "1.8"
javaParameters = true // Useful for reflection.
}
}
tasks.withType(Test) {
// Prevent the project from creating temporary files outside of the build directory.
systemProperty 'java.io.tmpdir', buildDir.absolutePath
// Tell the tests where Gradle's current module cache is.
// We need the tests to share this module cache to prevent the
// Gradle Test-Kit from downloading its own copy of Kotlin etc.
systemProperty 'test.gradle.user.home', project.gradle.gradleUserHomeDir
}
}
dependencies {
// Add the top-level projects ONLY to the host project.
runtime project.childProjects.values().collect {

View File

@ -0,0 +1,152 @@
# JarFilter
Deletes annotated elements at the byte-code level from a JAR of Java/Kotlin code. In the case of Kotlin
code, it also modifies the `@kotlin.Metadata` annotations not to contain any functions, properties or
type aliases that have been deleted. This prevents the Kotlin compiler from successfully compiling against
any elements which no longer exist.
We use this plugin together with ProGuard to generate Corda's `core-deterministic` and `serialization-deterministic`
modules. See [here](../../docs/source/deterministic-modules.rst) for more information.
## Usage
This plugin is automatically available on Gradle's classpath since it lives in Corda's `buildSrc` directory.
You need only `import` the plugin's task classes in the `build.gradle` file and then use them to declare
tasks.
You can enable the tasks' logging output using Gradle's `--info` or `--debug` command-line options.
### The `JarFilter` task
The `JarFilter` task removes unwanted elements from `class` files, namely:
- Deleting both Java methods/fields and Kotlin functions/properties/type aliases.
- Stubbing out methods by replacing the byte-code of their implementations.
- Removing annotations from classes/methods/fields.
It supports the following configuration options:
```gradle
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
// Task(s) whose JAR outputs should be filtered.
jars jar
// The annotations assigned to each filtering role. For example:
annotations {
forDelete = [
"org.testing.DeleteMe"
]
forStub = [
"org.testing.StubMeOut"
]
forRemove = [
"org.testing.RemoveMe"
]
}
// Location for filtered JARs. Defaults to "$buildDir/filtered-libs".
outputDir file(...)
// Whether the timestamps on the JARs' entries should be preserved "as is"
// or set to a platform-independent constant value (1st February 1980).
preserveTimestamps = {true|false}
// The maximum number of times (>= 1) to pass the JAR through the filter.
maxPasses = 5
// Writes more information about each pass of the filter.
verbose = {true|false}
}
```
You can specify as many annotations for each role as you like. The only constraint is that a given
annotation cannot be assigned to more than one role.
### The `MetaFixer` task
The `MetaFixer` task updates the `@kotlin.Metadata` annotations by removing references to any functions,
constructors, properties or nested classes that no longer exist in the byte-code. This is primarily to
"repair" Kotlin library code that has been processed by ProGuard.
Kotlin type aliases exist only inside `@Metadata` and so are unaffected by this task. Similarly, the
constructors for Kotlin's annotation classes don't exist in the byte-code either because Java annotations
are interfaces really. The `MetaFixer` task will therefore ignore annotations' constructors too.
It supports these configuration options:
```gradle
import net.corda.gradle.jarfilter.MetaFixerTask
task metafix(type: MetaFixerTask) {
// Task(s) whose JAR outputs should be fixed.
jars jar
// Location for fixed JARs. Defaults to "$buildDir/metafixed-libs"
outputDir file(...)
// Tag to be appended to the JAR name. Defaults to "-metafixed".
suffix = "..."
// Whether the timestamps on the JARs' entries should be preserved "as is"
// or set to a platform-independent constant value (1st February 1980).
preserveTimestamps = {true|false}
}
```
## Implementation Details
### Code Coverage
You can generate a JaCoCo code coverage report for the unit tests using:
```bash
$ cd buildSrc
$ ../gradlew jarfilter:jacocoTestReport
```
### Kotlin Metadata
The Kotlin compiler encodes information about each class inside its `@kotlin.Metadata` annotation.
```kotlin
import kotlin.annotation.AnnotationRetention.*
@Retention(RUNTIME)
annotation class Metadata {
val k: Int = 1
val d1: Array<String> = []
val d2: Array<String> = []
// ...
}
```
This is an internal feature of Kotlin which is read by Kotlin Reflection. There is no public API
for writing this information, and the content format of arrays `d1` and `d2` depends upon the
"class kind" `k`. For the kinds that we are interested in, `d1` contains a buffer of ProtoBuf
data and `d2` contains an array of `String` identifiers which the ProtoBuf data refers to by index.
Although ProtoBuf generates functions for both reading and writing the data buffer, the
Kotlin Reflection artifact only contains the functions for reading. This is almost certainly
because the writing functionality has been removed from the `kotlin-reflect` JAR using
ProGuard. However, the complete set of generated ProtoBuf classes is still available in the
`kotlin-compiler-embeddable` JAR. The `jarfilter:kotlin-metadata` module uses ProGuard to
extracts these classes into a new `kotlin-metdata` JAR, discarding any classes that the
ProtoBuf ones do not need and obfuscating any other ones that they do.
The custom `kotlin-metadata` object was originally created as a workaround for
[KT-18621](https://youtrack.jetbrains.com/issue/KT-18621). However, reducing the number of unwanted
classes on the classpath anyway can only be a Good Thing<sup>(TM)</sup>.
At runtime, `JarFilter` decompiles the ProtoBuf buffer into POJOs, deletes the elements that
no longer exist in the byte-code and then recompiles the POJOs into a new ProtoBuf buffer. The
`@Metadata` annotation is then rewritten using this new buffer for `d1` and the _original_ `String`
identifiers for `d2`. While some of these identifiers are very likely no longer used after this,
removing them would also require re-indexing the ProtoBuf data. It is therefore simpler just to
leave them as harmless cruft in the byte-code's constant pool.
The majority of `JarFilter`'s unit tests use Kotlin and Java reflection and so should not be
brittle as Kotlin evolves because `kotlin-reflect` is public API. Also, Kotlin's requirement that
it remain backwards-compatible with itself should imply that the ProtoBuf logic shouldn't change
(much). However, the ProtoBuf classes are still internal to Kotlin and so it _is_ possible that they
will occasionally move between packages. This has already happened for Kotlin 1.2.3x -> 1.2.4x, but
I am hoping this means that they will not move again for a while.
### JARs vs ZIPs
The `JarFilter` and `MetaFixer` tasks _deliberately_ use `ZipFile` and `ZipOutputStream` rather
than `JarInputStream` and `JarOutputStream` when reading and writing their JAR files. This is to
ensure that the original `META-INF/MANIFEST.MF` files are passed through unaltered. Note also that
there is no `ZipInputStream.getComment()` method, and so we need to use `ZipFile` in order to
preserve any JAR comments.
Neither `JarFilter` nor `MetaFixer` should change the order of the entries inside the JAR files.

View File

@ -0,0 +1,46 @@
plugins {
id 'java-gradle-plugin'
id 'jacoco'
}
apply plugin: 'kotlin'
repositories {
mavenLocal()
mavenCentral()
jcenter()
}
gradlePlugin {
plugins {
jarFilterPlugin {
id = 'net.corda.plugins.jar-filter'
implementationClass = 'net.corda.gradle.jarfilter.JarFilterPlugin'
}
}
}
configurations {
jacocoRuntime
}
processTestResources {
filesMatching('**/build.gradle') {
expand(['kotlin_version': kotlin_version])
}
filesMatching('gradle.properties') {
expand(['jacocoAgent': configurations.jacocoRuntime.asPath.replace('\\', '/'),
'buildDir': buildDir])
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation project(':jarfilter:kotlin-metadata')
implementation "org.ow2.asm:asm:$asm_version"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit"
testImplementation "org.jetbrains.kotlin:kotlin-reflect"
testImplementation "org.assertj:assertj-core:$assertj_version"
testImplementation "junit:junit:$junit_version"
testImplementation project(':jarfilter:unwanteds')
jacocoRuntime "org.jacoco:org.jacoco.agent:${jacoco.toolVersion}:runtime"
}

View File

@ -0,0 +1,81 @@
plugins {
id 'base'
}
description "Kotlin's metadata-handling classes"
repositories {
mavenLocal()
jcenter()
}
configurations {
proguard
runtime
configurations.default.extendsFrom runtime
}
dependencies {
proguard "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version"
proguard "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
runtime "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}
def javaHome = System.getProperty('java.home')
def originalJar = configurations.proguard.files.find { it.name.startsWith("kotlin-compiler-embeddable") }
import proguard.gradle.ProGuardTask
task metadata(type: ProGuardTask) {
injars originalJar, filter: '!META-INF/native/**'
outjars "$buildDir/libs/${project.name}-${kotlin_version}.jar"
libraryjars "$javaHome/lib/rt.jar"
libraryjars "$javaHome/../lib/tools.jar"
configurations.proguard.forEach {
if (originalJar != it) {
libraryjars it.path, filter: '!META-INF/versions/**'
}
}
keepattributes '*'
dontoptimize
printseeds
verbose
dontwarn 'com.sun.jna.**'
dontwarn 'org.jetbrains.annotations.**'
dontwarn 'org.jetbrains.kotlin.com.intellij.**'
dontwarn 'org.jetbrains.kotlin.com.google.j2objc.annotations.**'
dontwarn 'org.jetbrains.kotlin.com.google.errorprone.annotations.**'
keep 'class org.jetbrains.kotlin.load.java.JvmAnnotationNames { *; }'
keep 'class org.jetbrains.kotlin.metadata.** { *; }', includedescriptorclasses: true
keep 'class org.jetbrains.kotlin.protobuf.** { *; }', includedescriptorclasses: true
}
def metadataJar = metadata.outputs.files.singleFile
task validate(type: ProGuardTask) {
injars metadataJar
libraryjars "$javaHome/lib/rt.jar"
configurations.runtime.forEach {
libraryjars it.path, filter: '!META-INF/versions/**'
}
keepattributes '*'
dontpreverify
dontobfuscate
dontoptimize
verbose
dontwarn 'org.jetbrains.kotlin.com.google.errorprone.annotations.**'
keep 'class *'
}
artifacts {
'default' file: metadataJar, name: project.name, type: 'jar', extension: 'jar', builtBy: metadata
}
defaultTasks "metadata"
assemble.dependsOn metadata
metadata.finalizedBy validate

View File

@ -0,0 +1,353 @@
package net.corda.gradle.jarfilter
import org.gradle.api.InvalidUserDataException
import org.gradle.api.logging.Logger
import org.objectweb.asm.*
import org.objectweb.asm.Opcodes.*
/**
* ASM [ClassVisitor] for the JarFilter task that deletes unwanted class elements.
* The unwanted elements have been annotated in advance. Elements that reference
* unwanted elements are also removed to keep the byte-code consistent. Finally,
* the deleted elements are passed to the [MetadataTransformer] so that they can
* be removed from the [kotlin.Metadata] annotation.
*
* This Visitor is applied to the byte-code repeatedly until it has removed
* everything that is no longer wanted.
*/
class ClassTransformer private constructor (
visitor: ClassVisitor,
logger: Logger,
kotlinMetadata: MutableMap<String, List<String>>,
private val removeAnnotations: Set<String>,
private val deleteAnnotations: Set<String>,
private val stubAnnotations: Set<String>,
private val unwantedClasses: MutableSet<String>,
private val unwantedFields: MutableSet<FieldElement>,
private val deletedMethods: MutableSet<MethodElement>,
private val stubbedMethods: MutableSet<MethodElement>
) : KotlinAwareVisitor(ASM6, visitor, logger, kotlinMetadata), Repeatable<ClassTransformer> {
constructor(
visitor: ClassVisitor,
logger: Logger,
removeAnnotations: Set<String>,
deleteAnnotations: Set<String>,
stubAnnotations: Set<String>,
unwantedClasses: MutableSet<String>
) : this(
visitor = visitor,
logger = logger,
kotlinMetadata = mutableMapOf(),
removeAnnotations = removeAnnotations,
deleteAnnotations = deleteAnnotations,
stubAnnotations = stubAnnotations,
unwantedClasses = unwantedClasses,
unwantedFields = mutableSetOf(),
deletedMethods = mutableSetOf(),
stubbedMethods = mutableSetOf()
)
private var _className: String = "(unknown)"
val className: String get() = _className
val isUnwantedClass: Boolean get() = isUnwantedClass(className)
override val hasUnwantedElements: Boolean
get() = unwantedFields.isNotEmpty()
|| deletedMethods.isNotEmpty()
|| stubbedMethods.isNotEmpty()
|| super.hasUnwantedElements
private fun isUnwantedClass(name: String): Boolean = unwantedClasses.contains(name)
private fun hasDeletedSyntheticMethod(name: String): Boolean = deletedMethods.any { method ->
name.startsWith("$className\$${method.visibleName}\$")
}
override fun recreate(visitor: ClassVisitor) = ClassTransformer(
visitor = visitor,
logger = logger,
kotlinMetadata = kotlinMetadata,
removeAnnotations = removeAnnotations,
deleteAnnotations = deleteAnnotations,
stubAnnotations = stubAnnotations,
unwantedClasses = unwantedClasses,
unwantedFields = unwantedFields,
deletedMethods = deletedMethods,
stubbedMethods = stubbedMethods
)
override fun visit(version: Int, access: Int, clsName: String, signature: String?, superName: String?, interfaces: Array<String>?) {
_className = clsName
logger.info("Class {}", clsName)
super.visit(version, access, clsName, signature, superName, interfaces)
}
override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? {
if (removeAnnotations.contains(descriptor)) {
logger.info("- Removing annotation {}", descriptor)
return null
} else if (deleteAnnotations.contains(descriptor)) {
if (unwantedClasses.add(className)) {
logger.info("- Identified class {} as unwanted", className)
}
}
return super.visitAnnotation(descriptor, visible)
}
override fun visitField(access: Int, fieldName: String, descriptor: String, signature: String?, value: Any?): FieldVisitor? {
val field = FieldElement(fieldName, descriptor)
logger.debug("--- field ---> {}", field)
if (unwantedFields.contains(field)) {
logger.info("- Deleted field {},{}", field.name, field.descriptor)
unwantedFields.remove(field)
return null
}
val fv = super.visitField(access, fieldName, descriptor, signature, value) ?: return null
return UnwantedFieldAdapter(fv, field)
}
override fun visitMethod(access: Int, methodName: String, descriptor: String, signature: String?, exceptions: Array<String>?): MethodVisitor? {
val method = MethodElement(methodName, descriptor, access)
logger.debug("--- method ---> {}", method)
if (deletedMethods.contains(method)) {
logger.info("- Deleted method {}{}", method.name, method.descriptor)
deletedMethods.remove(method)
return null
}
/*
* Write the byte-code for the method's prototype, then check whether
* we need to replace the method's body with our "stub" code.
*/
val mv = super.visitMethod(access, methodName, descriptor, signature, exceptions) ?: return null
if (stubbedMethods.contains(method)) {
logger.info("- Stubbed out method {}{}", method.name, method.descriptor)
stubbedMethods.remove(method)
return if (method.isVoidFunction) VoidStubMethodAdapter(mv) else ThrowingStubMethodAdapter(mv)
}
return UnwantedMethodAdapter(mv, method)
}
override fun visitInnerClass(clsName: String, outerName: String?, innerName: String?, access: Int) {
logger.debug("--- inner class {} [outer: {}, inner: {}]", clsName, outerName, innerName)
if (isUnwantedClass || hasDeletedSyntheticMethod(clsName)) {
if (unwantedClasses.add(clsName)) {
logger.info("- Deleted inner class {}", clsName)
}
} else if (isUnwantedClass(clsName)) {
logger.info("- Deleted reference to inner class: {}", clsName)
} else {
super.visitInnerClass(clsName, outerName, innerName, access)
}
}
override fun visitOuterClass(outerName: String, methodName: String?, methodDescriptor: String?) {
logger.debug("--- outer class {} [enclosing method {},{}]", outerName, methodName, methodDescriptor)
if (isUnwantedClass(outerName)) {
logger.info("- Deleted reference to outer class {}", outerName)
} else {
super.visitOuterClass(outerName, methodName, methodDescriptor)
}
}
override fun visitEnd() {
if (isUnwantedClass) {
/*
* Optimisation: Don't rewrite the Kotlin @Metadata
* annotation if we're going to delete this class.
*/
kotlinMetadata.clear()
}
super.visitEnd()
/*
* Some elements were created based on unreliable information,
* such as Kotlin @Metadata annotations. We cannot rely on
* these actually existing in the bytecode, and so we expire
* them after a fixed number of passes.
*/
deletedMethods.removeIf(MethodElement::isExpired)
unwantedFields.removeIf(FieldElement::isExpired)
}
/**
* Removes the deleted methods and fields from the Kotlin Class metadata.
*/
override fun transformClassMetadata(d1: List<String>, d2: List<String>): List<String> {
val partitioned = deletedMethods.groupBy(MethodElement::isConstructor)
val prefix = "$className$"
return ClassMetadataTransformer(
logger = logger,
deletedFields = unwantedFields,
deletedFunctions = partitioned[false] ?: emptyList(),
deletedConstructors = partitioned[true] ?: emptyList(),
deletedNestedClasses = unwantedClasses.filter { it.startsWith(prefix) }.map { it.drop(prefix.length) },
deletedClasses = unwantedClasses,
handleExtraMethod = ::delete,
d1 = d1,
d2 = d2)
.transform()
}
/**
* Removes the deleted methods and fields from the Kotlin Package metadata.
*/
override fun transformPackageMetadata(d1: List<String>, d2: List<String>): List<String> {
return PackageMetadataTransformer(
logger = logger,
deletedFields = unwantedFields,
deletedFunctions = deletedMethods,
handleExtraMethod = ::delete,
d1 = d1,
d2 = d2)
.transform()
}
/**
* Callback function to mark extra methods for deletion.
* This will override a request for stubbing.
*/
private fun delete(method: MethodElement) {
if (deletedMethods.add(method) && stubbedMethods.remove(method)) {
logger.warn("-- method {}{} will be deleted instead of stubbed out",
method.name, method.descriptor)
}
}
/**
* Analyses the field to decide whether it should be deleted.
*/
private inner class UnwantedFieldAdapter(fv: FieldVisitor, private val field: FieldElement) : FieldVisitor(api, fv) {
override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? {
if (removeAnnotations.contains(descriptor)) {
logger.info("- Removing annotation {} from field {},{}", descriptor, field.name, field.descriptor)
return null
} else if (deleteAnnotations.contains(descriptor)) {
if (unwantedFields.add(field)) {
logger.info("- Identified field {},{} as unwanted", field.name, field.descriptor)
}
}
return super.visitAnnotation(descriptor, visible)
}
}
/**
* Analyses the method to decide whether it should be deleted.
*/
private inner class UnwantedMethodAdapter(mv: MethodVisitor, private val method: MethodElement) : MethodVisitor(api, mv) {
override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? {
if (removeAnnotations.contains(descriptor)) {
logger.info("- Removing annotation {} from method {}{}", descriptor, method.name, method.descriptor)
return null
} else if (deleteAnnotations.contains(descriptor)) {
if (deletedMethods.add(method)) {
logger.info("- Identified method {}{} for deletion", method.name, method.descriptor)
}
if (method.isKotlinSynthetic("annotations")) {
val extensionType = method.descriptor.extensionType
if (unwantedFields.add(FieldElement(name = method.visibleName, extension = extensionType))) {
logger.info("-- also identified property or typealias {},{} for deletion", method.visibleName, extensionType)
}
}
} else if (stubAnnotations.contains(descriptor) && (method.access and (ACC_ABSTRACT or ACC_SYNTHETIC)) == 0) {
if (stubbedMethods.add(method)) {
logger.info("- Identified method {}{} for stubbing out", method.name, method.descriptor)
}
}
return super.visitAnnotation(descriptor, visible)
}
override fun visitMethodInsn(opcode: Int, ownerName: String, methodName: String, descriptor: String, isInterface: Boolean) {
if ((isUnwantedClass(ownerName) || (ownerName == className && deletedMethods.contains(MethodElement(methodName, descriptor))))
&& !stubbedMethods.contains(method)) {
if (deletedMethods.add(method)) {
logger.info("- Unwanted invocation of method {},{}{} from method {}{}", ownerName, methodName, descriptor, method.name, method.descriptor)
}
}
super.visitMethodInsn(opcode, ownerName, methodName, descriptor, isInterface)
}
override fun visitFieldInsn(opcode: Int, ownerName: String, fieldName: String, descriptor: String) {
if ((isUnwantedClass(ownerName) || (ownerName == className && unwantedFields.contains(FieldElement(fieldName, descriptor))))
&& !stubbedMethods.contains(method)) {
if (method.isConstructor) {
when (opcode) {
GETFIELD, GETSTATIC -> {
when (descriptor) {
"I", "S", "B", "C", "Z" -> visitIntInsn(BIPUSH, 0)
"J" -> visitInsn(LCONST_0)
"F" -> visitInsn(FCONST_0)
"D" -> visitInsn(DCONST_0)
else -> visitInsn(ACONST_NULL)
}
}
PUTFIELD, PUTSTATIC -> {
when (descriptor) {
"J", "D" -> visitInsn(POP2)
else -> visitInsn(POP)
}
}
else -> throw InvalidUserDataException("Unexpected opcode $opcode")
}
logger.info("- Unwanted reference to field {},{},{} REMOVED from constructor {}{}",
ownerName, fieldName, descriptor, method.name, method.descriptor)
return
} else if (deletedMethods.add(method)) {
logger.info("- Unwanted reference to field {},{},{} from method {}{}",
ownerName, fieldName, descriptor, method.name, method.descriptor)
}
}
super.visitFieldInsn(opcode, ownerName, fieldName, descriptor)
}
}
/**
* Write "stub" byte-code for this method, preserving its other annotations.
* The method's original byte-code is discarded.
*/
private abstract inner class StubbingMethodAdapter(mv: MethodVisitor) : MethodVisitor(api, mv) {
override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? {
return if (stubAnnotations.contains(descriptor)) null else mv.visitAnnotation(descriptor, visible)
}
protected abstract fun writeStubCode()
final override fun visitCode() {
with (mv) {
visitCode()
writeStubCode()
visitMaxs(-1, -1) // Trigger computation of the max values.
visitEnd()
}
// Prevent this visitor from writing any more byte-code.
mv = null
}
}
/**
* Write a method that throws [UnsupportedOperationException] with message "Method has been deleted".
*/
private inner class ThrowingStubMethodAdapter(mv: MethodVisitor) : StubbingMethodAdapter(mv) {
override fun writeStubCode() {
with (mv) {
val throwEx = Label()
visitLabel(throwEx)
visitLineNumber(0, throwEx)
visitTypeInsn(NEW, "java/lang/UnsupportedOperationException")
visitInsn(DUP)
visitLdcInsn("Method has been deleted")
visitMethodInsn(INVOKESPECIAL, "java/lang/UnsupportedOperationException", "<init>", "(Ljava/lang/String;)V", false)
visitInsn(ATHROW)
}
}
}
/**
* Write an empty method. Can only be applied to methods that return void.
*/
private inner class VoidStubMethodAdapter(mv: MethodVisitor) : StubbingMethodAdapter(mv) {
override fun writeStubCode() {
mv.visitInsn(RETURN)
}
}
}

View File

@ -0,0 +1,110 @@
@file:JvmName("Elements")
package net.corda.gradle.jarfilter
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.deserialization.NameResolver
import org.jetbrains.kotlin.metadata.deserialization.TypeTable
import org.jetbrains.kotlin.metadata.deserialization.returnType
import org.jetbrains.kotlin.metadata.jvm.JvmProtoBuf
import org.jetbrains.kotlin.metadata.jvm.deserialization.ClassMapperLite
import org.objectweb.asm.Opcodes.ACC_SYNTHETIC
import java.util.*
private const val DUMMY_PASSES = 1
internal abstract class Element(val name: String, val descriptor: String) {
private var lifetime: Int = DUMMY_PASSES
open val isExpired: Boolean get() = --lifetime < 0
}
internal class MethodElement(name: String, descriptor: String, val access: Int = 0) : Element(name, descriptor) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as MethodElement
return other.name == name && other.descriptor == descriptor
}
override fun hashCode(): Int = Objects.hash(name, descriptor)
override fun toString(): String = "MethodElement[name=$name, descriptor=$descriptor, access=$access]"
override val isExpired: Boolean get() = access == 0 && super.isExpired
val isConstructor: Boolean get() = isObjectConstructor || isClassConstructor
val isClassConstructor: Boolean get() = name == "<clinit>"
val isObjectConstructor: Boolean get() = name == "<init>"
val isVoidFunction: Boolean get() = !isConstructor && descriptor.endsWith(")V")
private val suffix: String
val visibleName: String
init {
val idx = name.indexOf('$')
visibleName = if (idx == -1) name else name.substring(0, idx)
suffix = if (idx == -1) "" else name.drop(idx + 1)
}
fun isKotlinSynthetic(vararg tags: String): Boolean = (access and ACC_SYNTHETIC) != 0 && tags.contains(suffix)
}
/**
* A class cannot have two fields with the same name but different types. However,
* it can define extension functions and properties.
*/
internal class FieldElement(name: String, descriptor: String = "?", val extension: String = "()") : Element(name, descriptor) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as FieldElement
return other.name == name && other.extension == extension
}
override fun hashCode(): Int = Objects.hash(name, extension)
override fun toString(): String = "FieldElement[name=$name, descriptor=$descriptor, extension=$extension]"
override val isExpired: Boolean get() = descriptor == "?" && super.isExpired
}
val String.extensionType: String get() = substring(0, 1 + indexOf(')'))
/**
* Convert Kotlin getter/setter method data to [MethodElement] objects.
*/
internal fun JvmProtoBuf.JvmPropertySignature.toGetter(nameResolver: NameResolver): MethodElement? {
return if (hasGetter()) { getter?.toMethodElement(nameResolver) } else { null }
}
internal fun JvmProtoBuf.JvmPropertySignature.toSetter(nameResolver: NameResolver): MethodElement? {
return if (hasSetter()) { setter?.toMethodElement(nameResolver) } else { null }
}
internal fun JvmProtoBuf.JvmMethodSignature.toMethodElement(nameResolver: NameResolver)
= MethodElement(nameResolver.getString(name), nameResolver.getString(desc))
/**
* This logic is based heavily on [JvmProtoBufUtil.getJvmFieldSignature].
*/
internal fun JvmProtoBuf.JvmPropertySignature.toFieldElement(property: ProtoBuf.Property, nameResolver: NameResolver, typeTable: TypeTable): FieldElement {
var nameId = property.name
var descId = -1
if (hasField()) {
if (field.hasName()) {
nameId = field.name
}
if (field.hasDesc()) {
descId = field.desc
}
}
val descriptor = if (descId == -1) {
val returnType = property.returnType(typeTable)
if (returnType.hasClassName()) {
ClassMapperLite.mapClass(nameResolver.getQualifiedClassName(returnType.className))
} else {
"?"
}
} else {
nameResolver.getString(descId)
}
return FieldElement(nameResolver.getString(nameId), descriptor)
}

View File

@ -0,0 +1,14 @@
@file:Suppress("UNUSED")
package net.corda.gradle.jarfilter
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* This plugin definition is only needed by the tests.
*/
class JarFilterPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.logger.info("Applying JarFilter plugin")
}
}

View File

@ -0,0 +1,254 @@
package net.corda.gradle.jarfilter
import groovy.lang.Closure
import org.gradle.api.DefaultTask
import org.gradle.api.InvalidUserDataException
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.*
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.ClassWriter.COMPUTE_MAXS
import java.io.Closeable
import java.io.File
import java.io.IOException
import java.nio.file.*
import java.nio.file.StandardCopyOption.*
import java.util.zip.Deflater.BEST_COMPRESSION
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import kotlin.math.max
@Suppress("Unused", "MemberVisibilityCanBePrivate")
open class JarFilterTask : DefaultTask() {
private companion object {
private const val DEFAULT_MAX_PASSES = 5
}
private val _jars: ConfigurableFileCollection = project.files()
@get:SkipWhenEmpty
@get:InputFiles
val jars: FileCollection get() = _jars
fun setJars(inputs: Any?) {
val files = inputs ?: return
_jars.setFrom(files)
}
fun jars(inputs: Any?) = setJars(inputs)
@get:Input
protected var forDelete: Set<String> = emptySet()
@get:Input
protected var forStub: Set<String> = emptySet()
@get:Input
protected var forRemove: Set<String> = emptySet()
fun annotations(assign: Closure<List<String>>) {
assign.call()
}
@get:Console
var verbose: Boolean = false
@get:Input
var maxPasses: Int = DEFAULT_MAX_PASSES
set(value) {
field = max(value, 1)
}
@get:Input
var preserveTimestamps: Boolean = true
private var _outputDir = project.buildDir.resolve("filtered-libs")
@get:Internal
val outputDir: File get() = _outputDir
fun setOutputDir(d: File?) {
val dir = d ?: return
_outputDir = dir
}
fun outputDir(dir: File?) = setOutputDir(dir)
@get:OutputFiles
val filtered: FileCollection get() = project.files(jars.files.map(this::toFiltered))
private fun toFiltered(source: File) = File(outputDir, source.name.replace(JAR_PATTERN, "-filtered\$1"))
@TaskAction
fun filterJars() {
logger.info("JarFiltering:")
if (forDelete.isNotEmpty()) {
logger.info("- Elements annotated with one of '{}' will be deleted", forDelete.joinToString())
}
if (forStub.isNotEmpty()) {
logger.info("- Methods annotated with one of '{}' will be stubbed out", forStub.joinToString())
}
if (forRemove.isNotEmpty()) {
logger.info("- Annotations '{}' will be removed entirely", forRemove.joinToString())
}
checkDistinctAnnotations()
try {
jars.forEach { jar ->
logger.info("Filtering {}", jar)
Filter(jar).run()
}
} catch (e: Exception) {
rethrowAsUncheckedException(e)
}
}
private fun checkDistinctAnnotations() {
logger.info("Checking that all annotations are distinct.")
val annotations = forRemove.toHashSet().apply {
addAll(forDelete)
addAll(forStub)
removeAll(forRemove)
}
forDelete.forEach {
if (!annotations.remove(it)) {
failWith("Annotation '$it' also appears in JarFilter 'forDelete' section")
}
}
forStub.forEach {
if (!annotations.remove(it)) {
failWith("Annotation '$it' also appears in JarFilter 'forStub' section")
}
}
if (!annotations.isEmpty()) {
failWith("SHOULDN'T HAPPEN - Martian annotations! '${annotations.joinToString()}'")
}
}
private fun failWith(message: String): Nothing = throw InvalidUserDataException(message)
private fun verbose(format: String, vararg objects: Any) {
if (verbose) {
logger.info(format, *objects)
}
}
private inner class Filter(inFile: File) {
private val unwantedClasses: MutableSet<String> = mutableSetOf()
private val source: Path = inFile.toPath()
private val target: Path = toFiltered(inFile).toPath()
init {
Files.deleteIfExists(target)
}
fun run() {
logger.info("Filtering to: {}", target)
var input = source
try {
var passes = 1
while (true) {
verbose("Pass {}", passes)
val isModified = Pass(input).use { it.run() }
if (!isModified) {
logger.info("No changes after latest pass - exiting.")
break
} else if (++passes > maxPasses) {
break
}
input = Files.move(
target, Files.createTempFile(target.parent, "filter-", ".tmp"), REPLACE_EXISTING)
verbose("New input JAR: {}", input)
}
} catch (e: Exception) {
logger.error("Error filtering '{}' elements from {}", ArrayList(forRemove).apply { addAll(forDelete); addAll(forStub) }, input)
throw e
}
}
private inner class Pass(input: Path): Closeable {
/**
* Use [ZipFile] instead of [java.util.jar.JarInputStream] because
* JarInputStream consumes MANIFEST.MF when it's the first or second entry.
*/
private val inJar = ZipFile(input.toFile())
private val outJar = ZipOutputStream(Files.newOutputStream(target))
private var isModified = false
@Throws(IOException::class)
override fun close() {
inJar.use {
outJar.close()
}
}
fun run(): Boolean {
outJar.setLevel(BEST_COMPRESSION)
outJar.setComment(inJar.comment)
for (entry in inJar.entries()) {
val entryData = inJar.getInputStream(entry)
if (entry.isDirectory || !entry.name.endsWith(".class")) {
// This entry's byte contents have not changed,
// but may still need to be recompressed.
outJar.putNextEntry(entry.copy().withFileTimestamps(preserveTimestamps))
entryData.copyTo(outJar)
} else {
val classData = transform(entryData.readBytes())
if (classData.isNotEmpty()) {
// This entry's byte contents have almost certainly
// changed, and will be stored compressed.
outJar.putNextEntry(entry.asCompressed().withFileTimestamps(preserveTimestamps))
outJar.write(classData)
}
}
}
return isModified
}
private fun transform(inBytes: ByteArray): ByteArray {
var reader = ClassReader(inBytes)
var writer = ClassWriter(COMPUTE_MAXS)
var transformer = ClassTransformer(
visitor = writer,
logger = logger,
removeAnnotations = toDescriptors(forRemove),
deleteAnnotations = toDescriptors(forDelete),
stubAnnotations = toDescriptors(forStub),
unwantedClasses = unwantedClasses
)
/*
* First pass: This might not find anything to remove!
*/
reader.accept(transformer, 0)
if (transformer.isUnwantedClass || transformer.hasUnwantedElements) {
isModified = true
do {
/*
* Rewrite the class without any of the unwanted elements.
* If we're deleting the class then make sure we identify all of
* its inner classes too, for the next filter pass to delete.
*/
reader = ClassReader(writer.toByteArray())
writer = ClassWriter(COMPUTE_MAXS)
transformer = transformer.recreate(writer)
reader.accept(transformer, 0)
} while (!transformer.isUnwantedClass && transformer.hasUnwantedElements)
}
return if (transformer.isUnwantedClass) {
// The entire class is unwanted, so don't write it out.
logger.info("Deleting class {}", transformer.className)
byteArrayOf()
} else {
writer.toByteArray()
}
}
}
}
}

View File

@ -0,0 +1,109 @@
package net.corda.gradle.jarfilter
import org.gradle.api.logging.Logger
import org.jetbrains.kotlin.load.java.JvmAnnotationNames.*
import org.objectweb.asm.AnnotationVisitor
import org.objectweb.asm.ClassVisitor
/**
* Kotlin support: Loads the ProtoBuf data from the [kotlin.Metadata] annotation,
* or writes new ProtoBuf data that was created during a previous pass.
*/
abstract class KotlinAwareVisitor(
api: Int,
visitor: ClassVisitor,
protected val logger: Logger,
protected val kotlinMetadata: MutableMap<String, List<String>>
) : ClassVisitor(api, visitor) {
private companion object {
/** See [org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader.Kind]. */
private const val KOTLIN_CLASS = 1
private const val KOTLIN_FILE = 2
private const val KOTLIN_SYNTHETIC = 3
private const val KOTLIN_MULTIFILE_PART = 5
}
private var classKind: Int = 0
open val hasUnwantedElements: Boolean get() = kotlinMetadata.isNotEmpty()
protected abstract fun transformClassMetadata(d1: List<String>, d2: List<String>): List<String>
protected abstract fun transformPackageMetadata(d1: List<String>, d2: List<String>): List<String>
override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? {
val av = super.visitAnnotation(descriptor, visible) ?: return null
return if (descriptor == METADATA_DESC) KotlinMetadataAdaptor(av) else av
}
override fun visitEnd() {
super.visitEnd()
if (kotlinMetadata.isNotEmpty()) {
logger.info("- Examining Kotlin @Metadata[k={}]", classKind)
val d1 = kotlinMetadata.remove(METADATA_DATA_FIELD_NAME)
val d2 = kotlinMetadata.remove(METADATA_STRINGS_FIELD_NAME)
if (d1 != null && d1.isNotEmpty() && d2 != null) {
transformMetadata(d1, d2).apply {
if (isNotEmpty()) {
kotlinMetadata[METADATA_DATA_FIELD_NAME] = this
kotlinMetadata[METADATA_STRINGS_FIELD_NAME] = d2
}
}
}
}
}
private fun transformMetadata(d1: List<String>, d2: List<String>): List<String> {
return when (classKind) {
KOTLIN_CLASS -> transformClassMetadata(d1, d2)
KOTLIN_FILE, KOTLIN_MULTIFILE_PART -> transformPackageMetadata(d1, d2)
KOTLIN_SYNTHETIC -> {
logger.info("-- synthetic class ignored")
emptyList()
}
else -> {
/*
* For class-kind=4 (i.e. "multi-file"), we currently
* expect d1=[list of multi-file-part classes], d2=null.
*/
logger.info("-- unsupported class-kind {}", classKind)
emptyList()
}
}
}
private inner class KotlinMetadataAdaptor(av: AnnotationVisitor): AnnotationVisitor(api, av) {
override fun visit(name: String?, value: Any?) {
if (name == KIND_FIELD_NAME) {
classKind = value as Int
}
super.visit(name, value)
}
override fun visitArray(name: String): AnnotationVisitor? {
val av = super.visitArray(name)
if (av != null) {
val data = kotlinMetadata.remove(name) ?: return ArrayAccumulator(av, name)
logger.debug("-- rewrote @Metadata.{}[{}]", name, data.size)
data.forEach { av.visit(null, it) }
av.visitEnd()
}
return null
}
private inner class ArrayAccumulator(av: AnnotationVisitor, private val name: String) : AnnotationVisitor(api, av) {
private val data: MutableList<String> = mutableListOf()
override fun visit(name: String?, value: Any?) {
super.visit(name, value)
data.add(value as String)
}
override fun visitEnd() {
super.visitEnd()
kotlinMetadata[name] = data
logger.debug("-- read @Metadata.{}[{}]", name, data.size)
}
}
}
}

View File

@ -0,0 +1,128 @@
package net.corda.gradle.jarfilter
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.FileCollection
import org.gradle.api.logging.Logger
import org.gradle.api.tasks.*
import java.io.Closeable
import java.io.File
import java.io.IOException
import java.nio.file.*
import java.util.zip.Deflater.BEST_COMPRESSION
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
@Suppress("Unused", "MemberVisibilityCanBePrivate")
open class MetaFixerTask : DefaultTask() {
private val _jars: ConfigurableFileCollection = project.files()
@get:SkipWhenEmpty
@get:InputFiles
val jars: FileCollection
get() = _jars
fun setJars(inputs: Any?) {
val files = inputs ?: return
_jars.setFrom(files)
}
fun jars(inputs: Any?) = setJars(inputs)
private var _outputDir = project.buildDir.resolve("metafixer-libs")
@get:Internal
val outputDir: File
get() = _outputDir
fun setOutputDir(d: File?) {
val dir = d ?: return
_outputDir = dir
}
fun outputDir(dir: File?) = setOutputDir(dir)
private var _suffix: String = "-metafixed"
@get:Input
val suffix: String get() = _suffix
fun setSuffix(input: String?) {
_suffix = input ?: return
}
fun suffix(suffix: String?) = setSuffix(suffix)
@get:Input
var preserveTimestamps: Boolean = true
@TaskAction
fun fixMetadata() {
logger.info("Fixing Kotlin @Metadata")
try {
jars.forEach { jar ->
logger.info("Reading from {}", jar)
MetaFix(jar).use { it.run() }
}
} catch (e: Exception) {
rethrowAsUncheckedException(e)
}
}
@get:OutputFiles
val metafixed: FileCollection get() = project.files(jars.files.map(this::toMetaFixed))
private fun toMetaFixed(source: File) = File(outputDir, source.name.replace(JAR_PATTERN, "$suffix\$1"))
private inner class MetaFix(inFile: File) : Closeable {
/**
* Use [ZipFile] instead of [java.util.jar.JarInputStream] because
* JarInputStream consumes MANIFEST.MF when it's the first or second entry.
*/
private val target: Path = toMetaFixed(inFile).toPath()
private val inJar = ZipFile(inFile)
private val outJar: ZipOutputStream
init {
// Default options for newOutputStream() are CREATE, TRUNCATE_EXISTING.
outJar = ZipOutputStream(Files.newOutputStream(target)).apply {
setLevel(BEST_COMPRESSION)
}
}
@Throws(IOException::class)
override fun close() {
inJar.use {
outJar.close()
}
}
fun run() {
logger.info("Writing to {}", target)
outJar.setComment(inJar.comment)
val classNames = inJar.entries().asSequence().namesEndingWith(".class")
for (entry in inJar.entries()) {
val entryData = inJar.getInputStream(entry)
if (entry.isDirectory || !entry.name.endsWith(".class")) {
// This entry's byte contents have not changed,
// but may still need to be recompressed.
outJar.putNextEntry(entry.copy().withFileTimestamps(preserveTimestamps))
entryData.copyTo(outJar)
} else {
// This entry's byte contents have almost certainly
// changed, and will be stored compressed.
val classData = entryData.readBytes().fixMetadata(logger, classNames)
outJar.putNextEntry(entry.asCompressed().withFileTimestamps(preserveTimestamps))
outJar.write(classData)
}
}
}
}
private fun Sequence<ZipEntry>.namesEndingWith(suffix: String): Set<String> {
return filter { it.name.endsWith(suffix) }.map { it.name.dropLast(suffix.length) }.toSet()
}
}
fun ByteArray.fixMetadata(logger: Logger, classNames: Set<String>): ByteArray
= execute({ writer -> MetaFixerVisitor(writer, logger, classNames) })

View File

@ -0,0 +1,258 @@
package net.corda.gradle.jarfilter
import org.gradle.api.logging.Logger
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.ProtoBuf.Class.Kind.*
import org.jetbrains.kotlin.metadata.deserialization.Flags.*
import org.jetbrains.kotlin.metadata.deserialization.NameResolver
import org.jetbrains.kotlin.metadata.deserialization.TypeTable
import org.jetbrains.kotlin.metadata.deserialization.getExtensionOrNull
import org.jetbrains.kotlin.metadata.jvm.JvmProtoBuf.*
import org.jetbrains.kotlin.metadata.jvm.deserialization.BitEncoding
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmNameResolver
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil.EXTENSION_REGISTRY
import org.jetbrains.kotlin.protobuf.ExtensionRegistryLite
import org.jetbrains.kotlin.protobuf.MessageLite
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
/**
* Base class for aligning the contents of [kotlin.Metadata] annotations
* with the contents of the host byte-code.
* This is used by [MetaFixerVisitor] for [MetaFixerTask].
*/
internal abstract class MetaFixerTransformer<out T : MessageLite>(
private val logger: Logger,
private val actualFields: Collection<FieldElement>,
private val actualMethods: Collection<String>,
private val actualNestedClasses: Collection<String>,
private val actualClasses: Collection<String>,
d1: List<String>,
d2: List<String>,
parser: (InputStream, ExtensionRegistryLite) -> T
) {
private val stringTableTypes: StringTableTypes
private val nameResolver: NameResolver
protected val message: T
protected abstract val typeTable: TypeTable
protected open val classKind: ProtoBuf.Class.Kind? = null
protected abstract val properties: MutableList<ProtoBuf.Property>
protected abstract val functions: MutableList<ProtoBuf.Function>
protected abstract val constructors: MutableList<ProtoBuf.Constructor>
protected open val nestedClassNames: MutableList<Int> get() = throw UnsupportedOperationException("No nestedClassNames")
protected open val sealedSubclassNames: MutableList<Int> get() = throw UnsupportedOperationException("No sealedSubclassNames")
init {
val input = ByteArrayInputStream(BitEncoding.decodeBytes(d1.toTypedArray()))
stringTableTypes = StringTableTypes.parseDelimitedFrom(input, EXTENSION_REGISTRY)
nameResolver = JvmNameResolver(stringTableTypes, d2.toTypedArray())
message = parser(input, EXTENSION_REGISTRY)
}
abstract fun rebuild(): T
private fun filterNestedClasses(): Int {
if (classKind == null) return 0
var count = 0
var idx = 0
while (idx < nestedClassNames.size) {
val nestedClassName = nameResolver.getString(nestedClassNames[idx])
if (actualNestedClasses.contains(nestedClassName)) {
++idx
} else {
logger.info("-- removing nested class: {}", nestedClassName)
nestedClassNames.removeAt(idx)
++count
}
}
return count
}
private fun filterSealedSubclassNames(): Int {
if (classKind == null) return 0
var count = 0
var idx = 0
while (idx < sealedSubclassNames.size) {
val sealedSubclassName = nameResolver.getString(sealedSubclassNames[idx]).replace('.', '$')
if (actualClasses.contains(sealedSubclassName)) {
++idx
} else {
logger.info("-- removing sealed subclass: {}", sealedSubclassName)
sealedSubclassNames.removeAt(idx)
++count
}
}
return count
}
private fun filterFunctions(): Int {
var count = 0
var idx = 0
while (idx < functions.size) {
val signature = JvmProtoBufUtil.getJvmMethodSignature(functions[idx], nameResolver, typeTable)
if ((signature == null) || actualMethods.contains(signature)) {
++idx
} else {
logger.info("-- removing method: {}", signature)
functions.removeAt(idx)
++count
}
}
return count
}
private fun filterConstructors(): Int {
var count = 0
var idx = 0
while (idx < constructors.size) {
val signature = JvmProtoBufUtil.getJvmConstructorSignature(constructors[idx], nameResolver, typeTable)
if ((signature == null) || actualMethods.contains(signature)) {
++idx
} else {
logger.info("-- removing constructor: {}", signature)
constructors.removeAt(idx)
++count
}
}
return count
}
private fun filterProperties(): Int {
var count = 0
var idx = 0
removed@ while (idx < properties.size) {
val property = properties[idx]
val signature = property.getExtensionOrNull(propertySignature) ?: continue
val field = signature.toFieldElement(property, nameResolver, typeTable)
val getterMethod = signature.toGetter(nameResolver)
/**
* A property annotated with [JvmField] will use a field instead of a getter method.
* But properties without [JvmField] will also usually have a backing field. So we only
* remove a property that has either lost its getter method, or never had a getter method
* and has lost its field.
*
* Having said that, we cannot remove [JvmField] properties from a companion object class
* because these properties are implemented as static fields on the companion's host class.
*/
val isValidProperty = if (getterMethod == null) {
actualFields.contains(field) || classKind == COMPANION_OBJECT
} else {
actualMethods.contains(getterMethod.name + getterMethod.descriptor)
}
if (!isValidProperty) {
logger.info("-- removing property: {},{}", field.name, field.descriptor)
properties.removeAt(idx)
++count
continue@removed
}
++idx
}
return count
}
fun transform(): List<String> {
var count = filterProperties() + filterFunctions() + filterNestedClasses() + filterSealedSubclassNames()
if (classKind != ANNOTATION_CLASS) {
count += filterConstructors()
}
if (count == 0) {
return emptyList()
}
val bytes = ByteArrayOutputStream()
stringTableTypes.writeDelimitedTo(bytes)
rebuild().writeTo(bytes)
return BitEncoding.encodeBytes(bytes.toByteArray()).toList()
}
}
/**
* Aligns a [kotlin.Metadata] annotation containing a [ProtoBuf.Class] object
* in its [d1][kotlin.Metadata.d1] field with the byte-code of its host class.
*/
internal class ClassMetaFixerTransformer(
logger: Logger,
actualFields: Collection<FieldElement>,
actualMethods: Collection<String>,
actualNestedClasses: Collection<String>,
actualClasses: Collection<String>,
d1: List<String>,
d2: List<String>
) : MetaFixerTransformer<ProtoBuf.Class>(
logger,
actualFields,
actualMethods,
actualNestedClasses,
actualClasses,
d1,
d2,
ProtoBuf.Class::parseFrom
) {
override val typeTable = TypeTable(message.typeTable)
override val classKind: ProtoBuf.Class.Kind = CLASS_KIND.get(message.flags)
override val properties = mutableList(message.propertyList)
override val functions = mutableList(message.functionList)
override val constructors = mutableList(message.constructorList)
override val nestedClassNames = mutableList(message.nestedClassNameList)
override val sealedSubclassNames= mutableList(message.sealedSubclassFqNameList)
override fun rebuild(): ProtoBuf.Class = message.toBuilder().apply {
if (nestedClassNames.size != nestedClassNameCount) {
clearNestedClassName().addAllNestedClassName(nestedClassNames)
}
if (sealedSubclassNames.size != sealedSubclassFqNameCount) {
clearSealedSubclassFqName().addAllSealedSubclassFqName(sealedSubclassNames)
}
if (constructors.size != constructorCount) {
clearConstructor().addAllConstructor(constructors)
}
if (functions.size != functionCount) {
clearFunction().addAllFunction(functions)
}
if (properties.size != propertyCount) {
clearProperty().addAllProperty(properties)
}
}.build()
}
/**
* Aligns a [kotlin.Metadata] annotation containing a [ProtoBuf.Package] object
* in its [d1][kotlin.Metadata.d1] field with the byte-code of its host class.
*/
internal class PackageMetaFixerTransformer(
logger: Logger,
actualFields: Collection<FieldElement>,
actualMethods: Collection<String>,
d1: List<String>,
d2: List<String>
) : MetaFixerTransformer<ProtoBuf.Package>(
logger,
actualFields,
actualMethods,
emptyList(),
emptyList(),
d1,
d2,
ProtoBuf.Package::parseFrom
) {
override val typeTable = TypeTable(message.typeTable)
override val properties = mutableList(message.propertyList)
override val functions = mutableList(message.functionList)
override val constructors = mutableListOf<ProtoBuf.Constructor>()
override fun rebuild(): ProtoBuf.Package = message.toBuilder().apply {
if (functions.size != functionCount) {
clearFunction().addAllFunction(functions)
}
if (properties.size != propertyCount) {
clearProperty().addAllProperty(properties)
}
}.build()
}

View File

@ -0,0 +1,76 @@
package net.corda.gradle.jarfilter
import org.gradle.api.logging.Logger
import org.objectweb.asm.*
import org.objectweb.asm.Opcodes.*
/**
* ASM [ClassVisitor] for the MetaFixer task. This visitor inventories every function,
* property and inner class within the byte-code and then passes this information to
* the [MetaFixerTransformer].
*/
class MetaFixerVisitor private constructor(
visitor: ClassVisitor,
logger: Logger,
kotlinMetadata: MutableMap<String, List<String>>,
private val classNames: Set<String>,
private val fields: MutableSet<FieldElement>,
private val methods: MutableSet<String>,
private val nestedClasses: MutableSet<String>
) : KotlinAwareVisitor(ASM6, visitor, logger, kotlinMetadata), Repeatable<MetaFixerVisitor> {
constructor(visitor: ClassVisitor, logger: Logger, classNames: Set<String>)
: this(visitor, logger, mutableMapOf(), classNames, mutableSetOf(), mutableSetOf(), mutableSetOf())
override fun recreate(visitor: ClassVisitor) = MetaFixerVisitor(visitor, logger, kotlinMetadata, classNames, fields, methods, nestedClasses)
private var className: String = "(unknown)"
override fun visit(version: Int, access: Int, clsName: String, signature: String?, superName: String?, interfaces: Array<String>?) {
className = clsName
logger.info("Class {}", clsName)
super.visit(version, access, clsName, signature, superName, interfaces)
}
override fun visitField(access: Int, fieldName: String, descriptor: String, signature: String?, value: Any?): FieldVisitor? {
if (fields.add(FieldElement(fieldName, descriptor))) {
logger.info("- field {},{}", fieldName, descriptor)
}
return super.visitField(access, fieldName, descriptor, signature, value)
}
override fun visitMethod(access: Int, methodName: String, descriptor: String, signature: String?, exceptions: Array<String>?): MethodVisitor? {
if (methods.add(methodName + descriptor)) {
logger.info("- method {}{}", methodName, descriptor)
}
return super.visitMethod(access, methodName, descriptor, signature, exceptions)
}
override fun visitInnerClass(clsName: String, outerName: String?, innerName: String?, access: Int) {
if (outerName == className && innerName != null && nestedClasses.add(innerName)) {
logger.info("- inner class {}", clsName)
}
return super.visitInnerClass(clsName, outerName, innerName, access)
}
override fun transformClassMetadata(d1: List<String>, d2: List<String>): List<String> {
return ClassMetaFixerTransformer(
logger = logger,
actualFields = fields,
actualMethods = methods,
actualNestedClasses = nestedClasses,
actualClasses = classNames,
d1 = d1,
d2 = d2)
.transform()
}
override fun transformPackageMetadata(d1: List<String>, d2: List<String>): List<String> {
return PackageMetaFixerTransformer(
logger = logger,
actualFields = fields,
actualMethods = methods,
d1 = d1,
d2 = d2)
.transform()
}
}

View File

@ -0,0 +1,307 @@
package net.corda.gradle.jarfilter
import org.gradle.api.logging.Logger
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.deserialization.Flags.*
import org.jetbrains.kotlin.metadata.deserialization.NameResolver
import org.jetbrains.kotlin.metadata.deserialization.TypeTable
import org.jetbrains.kotlin.metadata.deserialization.getExtensionOrNull
import org.jetbrains.kotlin.metadata.jvm.JvmProtoBuf.*
import org.jetbrains.kotlin.metadata.jvm.deserialization.BitEncoding
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmNameResolver
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil.EXTENSION_REGISTRY
import org.jetbrains.kotlin.protobuf.ExtensionRegistryLite
import org.jetbrains.kotlin.protobuf.MessageLite
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
/**
* Base class for removing unwanted elements from [kotlin.Metadata] annotations.
* This is used by [ClassTransformer] for [JarFilterTask].
*/
internal abstract class MetadataTransformer<out T : MessageLite>(
private val logger: Logger,
private val deletedFields: Collection<FieldElement>,
private val deletedFunctions: Collection<MethodElement>,
private val deletedConstructors: Collection<MethodElement>,
private val deletedNestedClasses: Collection<String>,
private val deletedClasses: Collection<String>,
private val handleExtraMethod: (MethodElement) -> Unit,
d1: List<String>,
d2: List<String>,
parser: (InputStream, ExtensionRegistryLite) -> T
) {
private val stringTableTypes: StringTableTypes
protected val nameResolver: NameResolver
protected val message: T
protected abstract val typeTable: TypeTable
protected open val className: String get() = throw UnsupportedOperationException("No className")
protected open val nestedClassNames: MutableList<Int> get() = throw UnsupportedOperationException("No nestedClassNames")
protected open val sealedSubclassNames: MutableList<Int> get() = throw UnsupportedOperationException("No sealedSubclassNames")
protected abstract val properties: MutableList<ProtoBuf.Property>
protected abstract val functions: MutableList<ProtoBuf.Function>
protected open val constructors: MutableList<ProtoBuf.Constructor> get() = throw UnsupportedOperationException("No constructors")
protected abstract val typeAliases: MutableList<ProtoBuf.TypeAlias>
init {
val input = ByteArrayInputStream(BitEncoding.decodeBytes(d1.toTypedArray()))
stringTableTypes = StringTableTypes.parseDelimitedFrom(input, EXTENSION_REGISTRY)
nameResolver = JvmNameResolver(stringTableTypes, d2.toTypedArray())
message = parser(input, EXTENSION_REGISTRY)
}
abstract fun rebuild(): T
fun transform(): List<String> {
val count = (
filterProperties()
+ filterFunctions()
+ filterConstructors()
+ filterNestedClasses()
+ filterTypeAliases()
+ filterSealedSubclasses()
)
if (count == 0) {
return emptyList()
}
val bytes = ByteArrayOutputStream()
stringTableTypes.writeDelimitedTo(bytes)
rebuild().writeTo(bytes)
return BitEncoding.encodeBytes(bytes.toByteArray()).toList()
}
private fun filterNestedClasses(): Int {
if (deletedNestedClasses.isEmpty()) return 0
var count = 0
var idx = 0
while (idx < nestedClassNames.size) {
val nestedClassName = nameResolver.getString(nestedClassNames[idx])
if (deletedNestedClasses.contains(nestedClassName)) {
logger.info("-- removing nested class: {}", nestedClassName)
nestedClassNames.removeAt(idx)
++count
} else {
++idx
}
}
return count
}
private fun filterConstructors(): Int = deletedConstructors.count(::filterConstructor)
private fun filterConstructor(deleted: MethodElement): Boolean {
for (idx in 0 until constructors.size) {
val constructor = constructors[idx]
val signature = JvmProtoBufUtil.getJvmConstructorSignature(constructor, nameResolver, typeTable)
if (signature == deleted.name + deleted.descriptor) {
if (IS_SECONDARY.get(constructor.flags)) {
logger.info("-- removing constructor: {}{}", deleted.name, deleted.descriptor)
} else {
logger.warn("Removing primary constructor: {}{}", className, deleted.descriptor)
}
constructors.removeAt(idx)
return true
}
}
return false
}
private fun filterFunctions(): Int = deletedFunctions.count(::filterFunction)
private fun filterFunction(deleted: MethodElement): Boolean {
for (idx in 0 until functions.size) {
val function = functions[idx]
if (nameResolver.getString(function.name) == deleted.name) {
val signature = JvmProtoBufUtil.getJvmMethodSignature(function, nameResolver, typeTable)
if (signature == deleted.name + deleted.descriptor) {
logger.info("-- removing function: {}{}", deleted.name, deleted.descriptor)
functions.removeAt(idx)
return true
}
}
}
return false
}
private fun filterProperties(): Int = deletedFields.count(::filterProperty)
private fun filterProperty(deleted: FieldElement): Boolean {
for (idx in 0 until properties.size) {
val property = properties[idx]
val signature = property.getExtensionOrNull(propertySignature) ?: continue
val field = signature.toFieldElement(property, nameResolver, typeTable)
if (field.name.toVisible() == deleted.name) {
// Check that this property's getter has the correct descriptor.
// If it doesn't then we have the wrong property here.
val getter = signature.toGetter(nameResolver)
if (getter != null) {
if (!getter.descriptor.startsWith(deleted.extension)) {
continue
}
deleteExtra(getter)
}
signature.toSetter(nameResolver)?.apply(::deleteExtra)
logger.info("-- removing property: {},{}", field.name, field.descriptor)
properties.removeAt(idx)
return true
}
}
return false
}
private fun deleteExtra(func: MethodElement) {
if (!deletedFunctions.contains(func)) {
logger.info("-- identified extra method {}{} for deletion", func.name, func.descriptor)
handleExtraMethod(func)
filterFunction(func)
}
}
private fun filterTypeAliases(): Int {
if (deletedFields.isEmpty()) return 0
var count = 0
var idx = 0
while (idx < typeAliases.size) {
val aliasName = nameResolver.getString(typeAliases[idx].name)
if (deletedFields.any { it.name == aliasName && it.extension == "()" }) {
logger.info("-- removing typealias: {}", aliasName)
typeAliases.removeAt(idx)
++count
} else {
++idx
}
}
return count
}
private fun filterSealedSubclasses(): Int {
if (deletedClasses.isEmpty()) return 0
var count = 0
var idx = 0
while (idx < sealedSubclassNames.size) {
val subclassName = nameResolver.getString(sealedSubclassNames[idx]).replace('.', '$')
if (deletedClasses.contains(subclassName)) {
logger.info("-- removing sealed subclass: {}", subclassName)
sealedSubclassNames.removeAt(idx)
++count
} else {
++idx
}
}
return count
}
/**
* Removes any Kotlin suffix, e.g. "$delegate" or "$annotations".
*/
private fun String.toVisible(): String {
val idx = indexOf('$')
return if (idx == -1) this else substring(0, idx)
}
}
/**
* Removes elements from a [kotlin.Metadata] annotation that contains
* a [ProtoBuf.Class] object in its [d1][kotlin.Metadata.d1] field.
*/
internal class ClassMetadataTransformer(
logger: Logger,
deletedFields: Collection<FieldElement>,
deletedFunctions: Collection<MethodElement>,
deletedConstructors: Collection<MethodElement>,
deletedNestedClasses: Collection<String>,
deletedClasses: Collection<String>,
handleExtraMethod: (MethodElement) -> Unit,
d1: List<String>,
d2: List<String>
) : MetadataTransformer<ProtoBuf.Class>(
logger,
deletedFields,
deletedFunctions,
deletedConstructors,
deletedNestedClasses,
deletedClasses,
handleExtraMethod,
d1,
d2,
ProtoBuf.Class::parseFrom
) {
override val typeTable = TypeTable(message.typeTable)
override val className = nameResolver.getString(message.fqName)
override val nestedClassNames = mutableList(message.nestedClassNameList)
override val sealedSubclassNames = mutableList(message.sealedSubclassFqNameList)
override val properties = mutableList(message.propertyList)
override val functions = mutableList(message.functionList)
override val constructors = mutableList(message.constructorList)
override val typeAliases = mutableList(message.typeAliasList)
override fun rebuild(): ProtoBuf.Class = message.toBuilder().apply {
if (nestedClassNames.size != nestedClassNameCount) {
clearNestedClassName().addAllNestedClassName(nestedClassNames)
}
if (constructors.size != constructorCount) {
clearConstructor().addAllConstructor(constructors)
}
if (functions.size != functionCount) {
clearFunction().addAllFunction(functions)
}
if (properties.size != propertyCount) {
clearProperty().addAllProperty(properties)
}
if (typeAliases.size != typeAliasCount) {
clearTypeAlias().addAllTypeAlias(typeAliases)
}
if (sealedSubclassNames.size != sealedSubclassFqNameCount) {
clearSealedSubclassFqName().addAllSealedSubclassFqName(sealedSubclassNames)
}
}.build()
}
/**
* Removes elements from a [kotlin.Metadata] annotation that contains
* a [ProtoBuf.Package] object in its [d1][kotlin.Metadata.d1] field.
*/
internal class PackageMetadataTransformer(
logger: Logger,
deletedFields: Collection<FieldElement>,
deletedFunctions: Collection<MethodElement>,
handleExtraMethod: (MethodElement) -> Unit,
d1: List<String>,
d2: List<String>
) : MetadataTransformer<ProtoBuf.Package>(
logger,
deletedFields,
deletedFunctions,
emptyList(),
emptyList(),
emptyList(),
handleExtraMethod,
d1,
d2,
ProtoBuf.Package::parseFrom
) {
override val typeTable = TypeTable(message.typeTable)
override val properties = mutableList(message.propertyList)
override val functions = mutableList(message.functionList)
override val typeAliases = mutableList(message.typeAliasList)
override fun rebuild(): ProtoBuf.Package = message.toBuilder().apply {
if (functions.size != functionCount) {
clearFunction().addAllFunction(functions)
}
if (properties.size != propertyCount) {
clearProperty().addAllProperty(properties)
}
if (typeAliases.size != typeAliasCount) {
clearTypeAlias().addAllTypeAlias(typeAliases)
}
}.build()
}

View File

@ -0,0 +1,8 @@
package net.corda.gradle.jarfilter
import org.objectweb.asm.ClassVisitor
interface Repeatable<T : ClassVisitor> {
fun recreate(visitor: ClassVisitor): T
val hasUnwantedElements: Boolean
}

View File

@ -0,0 +1,89 @@
@file:JvmName("Utils")
package net.corda.gradle.jarfilter
import org.gradle.api.GradleException
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import java.nio.file.attribute.FileTime
import java.util.*
import java.util.Calendar.FEBRUARY
import java.util.zip.ZipEntry
import java.util.zip.ZipEntry.DEFLATED
import java.util.zip.ZipEntry.STORED
import kotlin.math.max
import kotlin.text.RegexOption.*
internal val JAR_PATTERN = "(\\.jar)$".toRegex(IGNORE_CASE)
// Use the same constant file timestamp as Gradle.
private val CONSTANT_TIME: FileTime = FileTime.fromMillis(
GregorianCalendar(1980, FEBRUARY, 1).apply { timeZone = TimeZone.getTimeZone("UTC") }.timeInMillis
)
internal fun rethrowAsUncheckedException(e: Exception): Nothing
= throw (e as? RuntimeException) ?: GradleException(e.message ?: "", e)
/**
* Recreates a [ZipEntry] object. The entry's byte contents
* will be compressed automatically, and its CRC, size and
* compressed size fields populated.
*/
internal fun ZipEntry.asCompressed(): ZipEntry {
return ZipEntry(name).also { entry ->
entry.lastModifiedTime = lastModifiedTime
lastAccessTime?.also { at -> entry.lastAccessTime = at }
creationTime?.also { ct -> entry.creationTime = ct }
entry.comment = comment
entry.method = DEFLATED
entry.extra = extra
}
}
internal fun ZipEntry.copy(): ZipEntry {
return if (method == STORED) ZipEntry(this) else asCompressed()
}
internal fun ZipEntry.withFileTimestamps(preserveTimestamps: Boolean): ZipEntry {
if (!preserveTimestamps) {
lastModifiedTime = CONSTANT_TIME
lastAccessTime?.apply { lastAccessTime = CONSTANT_TIME }
creationTime?.apply { creationTime = CONSTANT_TIME }
}
return this
}
internal fun <T : Any> mutableList(c: Collection<T>): MutableList<T> = ArrayList(c)
/**
* Converts Java class names to Java descriptors.
*/
internal fun toDescriptors(classNames: Iterable<String>): Set<String> {
return classNames.map(String::descriptor).toSet()
}
internal val String.toPathFormat: String get() = replace('.', '/')
internal val String.descriptor: String get() = "L$toPathFormat;"
/**
* Performs the given number of passes of the repeatable visitor over the byte-code.
* Used by [MetaFixerVisitor], but also by some of the test visitors.
*/
internal fun <T> ByteArray.execute(visitor: (ClassVisitor) -> T, flags: Int = 0, passes: Int = 2): ByteArray
where T : ClassVisitor,
T : Repeatable<T> {
var reader = ClassReader(this)
var writer = ClassWriter(flags)
val transformer = visitor(writer)
var count = max(passes, 1)
reader.accept(transformer, 0)
while (transformer.hasUnwantedElements && --count > 0) {
reader = ClassReader(writer.toByteArray())
writer = ClassWriter(flags)
reader.accept(transformer.recreate(writer), 0)
}
return writer.toByteArray()
}

View File

@ -0,0 +1,69 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.matcher.*
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsNot.not
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import java.lang.reflect.Modifier.*
import kotlin.reflect.full.declaredFunctions
import kotlin.test.assertFailsWith
class AbstractFunctionTest {
companion object {
private const val FUNCTION_CLASS = "net.corda.gradle.AbstractFunctions"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "abstract-function")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteAbstractFunction() {
val longFunction = isFunction("toDelete", Long::class, Long::class)
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getMethod("toDelete", Long::class.java).also { method ->
assertEquals(ABSTRACT, method.modifiers and ABSTRACT)
}
assertThat("toDelete(J) not found", kotlin.declaredFunctions, hasItem(longFunction))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getMethod("toDelete", Long::class.java) }
assertThat("toDelete(J) still exists", kotlin.declaredFunctions, not(hasItem(longFunction)))
}
}
}
@Test
fun cannotStubAbstractFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getMethod("toStubOut", Long::class.java).also { method ->
assertEquals(ABSTRACT, method.modifiers and ABSTRACT)
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getMethod("toStubOut", Long::class.java).also { method ->
assertEquals(ABSTRACT, method.modifiers and ABSTRACT)
}
}
}
}
}

View File

@ -0,0 +1,188 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.matcher.*
import net.corda.gradle.unwanted.*
import org.assertj.core.api.Assertions.*
import org.hamcrest.core.IsCollectionContaining.*
import org.hamcrest.core.IsNot.*
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.declaredMemberProperties
import kotlin.test.assertFailsWith
class DeleteAndStubTests {
companion object {
private const val VAR_PROPERTY_CLASS = "net.corda.gradle.HasVarPropertyForDeleteAndStub"
private const val VAL_PROPERTY_CLASS = "net.corda.gradle.HasValPropertyForDeleteAndStub"
private const val DELETED_FUN_CLASS = "net.corda.gradle.DeletedFunctionInsideStubbed"
private const val DELETED_VAR_CLASS = "net.corda.gradle.DeletedVarInsideStubbed"
private const val DELETED_VAL_CLASS = "net.corda.gradle.DeletedValInsideStubbed"
private const val DELETED_PKG_CLASS = "net.corda.gradle.DeletePackageWithStubbed"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-and-stub")
private val stringVal = isProperty("stringVal", String::class)
private val longVar = isProperty("longVar", Long::class)
private val getStringVal = isMethod("getStringVal", String::class.java)
private val getLongVar = isMethod("getLongVar", Long::class.java)
private val setLongVar = isMethod("setLongVar", Void.TYPE, Long::class.java)
private val stringData = isFunction("stringData", String::class)
private val unwantedFun = isFunction("unwantedFun", String::class, String::class)
private val unwantedVar = isProperty("unwantedVar", String::class)
private val unwantedVal = isProperty("unwantedVal", String::class)
private val stringDataJava = isMethod("stringData", String::class.java)
private val getUnwantedVal = isMethod("getUnwantedVal", String::class.java)
private val getUnwantedVar = isMethod("getUnwantedVar", String::class.java)
private val setUnwantedVar = isMethod("setUnwantedVar", Void.TYPE, String::class.java)
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteValProperty() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasStringVal>(VAL_PROPERTY_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.stringVal)
}
assertThat("stringVal not found", kotlin.declaredMemberProperties, hasItem(stringVal))
assertThat("getStringVal() not found", kotlin.javaDeclaredMethods, hasItem(getStringVal))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasStringVal>(VAL_PROPERTY_CLASS).apply {
assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE))
assertThat("stringVal still exists", kotlin.declaredMemberProperties, not(hasItem(stringVal)))
assertThat("getStringVal() still exists", kotlin.javaDeclaredMethods, not(hasItem(getStringVal)))
}
}
}
@Test
fun deleteVarProperty() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasLongVar>(VAR_PROPERTY_CLASS).apply {
getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also { obj ->
assertEquals(BIG_NUMBER, obj.longVar)
}
assertThat("longVar not found", kotlin.declaredMemberProperties, hasItem(longVar))
assertThat("getLongVar() not found", kotlin.javaDeclaredMethods, hasItem(getLongVar))
assertThat("setLongVar() not found", kotlin.javaDeclaredMethods, hasItem(setLongVar))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasLongVar>(VAR_PROPERTY_CLASS).apply {
assertNotNull(getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER))
assertThat("longVar still exists", kotlin.declaredMemberProperties, not(hasItem(longVar)))
assertThat("getLongVar() still exists", kotlin.javaDeclaredMethods, not(hasItem(getLongVar)))
assertThat("setLongVar() still exists", kotlin.javaDeclaredMethods, not(hasItem(setLongVar)))
}
}
}
@Test
fun deletedFunctionInsideStubbed() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasString>(DELETED_FUN_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj ->
assertEquals(DEFAULT_MESSAGE, obj.stringData())
assertEquals(MESSAGE, (obj as HasUnwantedFun).unwantedFun(MESSAGE))
}
assertThat("unwantedFun not found", kotlin.declaredMemberFunctions, hasItem(unwantedFun))
assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasString>(DELETED_FUN_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj ->
assertFailsWith<UnsupportedOperationException> { obj.stringData() }.also { ex ->
assertThat(ex).hasMessage("Method has been deleted")
}
assertFailsWith<AbstractMethodError> { (obj as HasUnwantedFun).unwantedFun(MESSAGE) }
}
assertThat("unwantedFun still exists", kotlin.declaredMemberFunctions, not(hasItem(unwantedFun)))
assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData))
}
}
}
@Test
fun deletedVarInsideStubbed() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasString>(DELETED_VAR_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj ->
assertEquals(DEFAULT_MESSAGE, obj.stringData())
(obj as HasUnwantedVar).also {
assertEquals(DEFAULT_MESSAGE, it.unwantedVar)
it.unwantedVar = MESSAGE
assertEquals(MESSAGE, it.unwantedVar)
}
}
assertThat("unwantedVar not found", kotlin.declaredMemberProperties, hasItem(unwantedVar))
assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasString>(DELETED_VAR_CLASS).apply {
assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE))
assertThat("unwantedVar still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVar)))
assertThat("getUnwantedVar() still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVar)))
assertThat("setUnwantedVar() still exists", kotlin.javaDeclaredMethods, not(hasItem(setUnwantedVar)))
assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData))
assertThat("stringData() not found", kotlin.javaDeclaredMethods, hasItem(stringDataJava))
}
}
}
@Test
fun deletedValInsideStubbed() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasString>(DELETED_VAL_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.stringData())
assertEquals(MESSAGE, (obj as HasUnwantedVal).unwantedVal)
}
assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal))
assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasString>(DELETED_VAL_CLASS).apply {
assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE))
assertThat("unwantedVal still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVal)))
assertThat("getUnwantedVal() still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal)))
assertThat("stringData() not found", kotlin.declaredMemberFunctions, hasItem(stringData))
assertThat("stringData() not found", kotlin.javaDeclaredMethods, hasItem(stringDataJava))
}
}
}
@Test
fun deletePackageWithStubbed() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(DELETED_PKG_CLASS).apply {
getDeclaredMethod("stubbed", String::class.java).also { method ->
assertEquals("[$MESSAGE]", method.invoke(null, MESSAGE))
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
assertFailsWith<ClassNotFoundException> { cl.load<Any>(DELETED_PKG_CLASS) }
}
}
}

View File

@ -0,0 +1,165 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.matcher.*
import net.corda.gradle.unwanted.HasAll
import net.corda.gradle.unwanted.HasInt
import net.corda.gradle.unwanted.HasLong
import net.corda.gradle.unwanted.HasString
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsNot.not
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.jvm.kotlin
import kotlin.reflect.full.primaryConstructor
import kotlin.test.assertFailsWith
class DeleteConstructorTest {
companion object {
private const val STRING_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryStringConstructorToDelete"
private const val LONG_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryLongConstructorToDelete"
private const val INT_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryIntConstructorToDelete"
private const val SECONDARY_CONSTRUCTOR_CLASS = "net.corda.gradle.HasConstructorToDelete"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-constructor")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteConstructorWithLongParameter() {
val longConstructor = isConstructor(SECONDARY_CONSTRUCTOR_CLASS, hasParam(Long::class))
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasLong>(SECONDARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also {
assertEquals(BIG_NUMBER, it.longData())
}
assertThat("<init>(J) not found", kotlin.constructors, hasItem(longConstructor))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasLong>(SECONDARY_CONSTRUCTOR_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredConstructor(Long::class.java) }
assertThat("<init>(J) still exists", kotlin.constructors, not(hasItem(longConstructor)))
assertNotNull("primary constructor missing", kotlin.primaryConstructor)
}
}
}
@Test
fun deleteConstructorWithStringParameter() {
val stringConstructor = isConstructor(SECONDARY_CONSTRUCTOR_CLASS, hasParam(String::class))
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasString>(SECONDARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also {
assertEquals(MESSAGE, it.stringData())
}
assertThat("<init>(String) not found", kotlin.constructors, hasItem(stringConstructor))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasString>(SECONDARY_CONSTRUCTOR_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredConstructor(String::class.java) }
assertThat("<init>(String) still exists", kotlin.constructors, not(hasItem(stringConstructor)))
assertNotNull("primary constructor missing", kotlin.primaryConstructor)
}
}
}
@Test
fun showUnannotatedConstructorIsUnaffected() {
val intConstructor = isConstructor(SECONDARY_CONSTRUCTOR_CLASS, hasParam(Int::class))
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasAll>(SECONDARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Int::class.java).newInstance(NUMBER).also {
assertEquals(NUMBER, it.intData())
assertEquals(NUMBER.toLong(), it.longData())
assertEquals("<nothing>", it.stringData())
}
assertThat("<init>(Int) not found", kotlin.constructors, hasItem(intConstructor))
assertNotNull("primary constructor missing", kotlin.primaryConstructor)
}
}
}
@Test
fun deletePrimaryConstructorWithStringParameter() {
val stringConstructor = isConstructor(STRING_PRIMARY_CONSTRUCTOR_CLASS, hasParam(String::class))
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasString>(STRING_PRIMARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also {
assertEquals(MESSAGE, it.stringData())
}
assertThat("<init>(String) not found", kotlin.constructors, hasItem(stringConstructor))
assertThat("primary constructor missing", kotlin.primaryConstructor!!, stringConstructor)
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasString>(STRING_PRIMARY_CONSTRUCTOR_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredConstructor(String::class.java) }
assertThat("<init>(String) still exists", kotlin.constructors, not(hasItem(stringConstructor)))
assertNull("primary constructor still exists", kotlin.primaryConstructor)
}
}
}
@Test
fun deletePrimaryConstructorWithLongParameter() {
val longConstructor = isConstructor(LONG_PRIMARY_CONSTRUCTOR_CLASS, hasParam(Long::class))
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasLong>(LONG_PRIMARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also {
assertEquals(BIG_NUMBER, it.longData())
}
assertThat("<init>(J) not found", kotlin.constructors, hasItem(longConstructor))
assertThat("primary constructor missing", kotlin.primaryConstructor!!, longConstructor)
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasLong>(LONG_PRIMARY_CONSTRUCTOR_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredConstructor(Long::class.java) }
assertThat("<init>(J) still exists", kotlin.constructors, not(hasItem(longConstructor)))
assertNull("primary constructor still exists", kotlin.primaryConstructor)
}
}
}
@Test
fun deletePrimaryConstructorWithIntParameter() {
val intConstructor = isConstructor(INT_PRIMARY_CONSTRUCTOR_CLASS, hasParam(Int::class))
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasInt>(INT_PRIMARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Int::class.java).newInstance(NUMBER).also {
assertEquals(NUMBER, it.intData())
}
assertThat("<init>(I) not found", kotlin.constructors, hasItem(intConstructor))
assertThat("primary constructor missing", kotlin.primaryConstructor!!, intConstructor)
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasInt>(INT_PRIMARY_CONSTRUCTOR_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredConstructor(Int::class.java) }
assertThat("<init>(I) still exists", kotlin.constructors, not(hasItem(intConstructor)))
assertNull("primary constructor still exists", kotlin.primaryConstructor)
}
}
}
}

View File

@ -0,0 +1,52 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.matcher.isMethod
import net.corda.gradle.jarfilter.matcher.isProperty
import net.corda.gradle.jarfilter.matcher.javaDeclaredMethods
import net.corda.gradle.unwanted.HasUnwantedVal
import org.hamcrest.core.IsCollectionContaining.*
import org.hamcrest.core.IsNot.*
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.reflect.full.declaredMemberExtensionProperties
import kotlin.reflect.full.declaredMemberProperties
class DeleteExtensionValPropertyTest {
companion object {
private const val PROPERTY_CLASS = "net.corda.gradle.HasValExtension"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-extension-val")
private val unwantedVal = isProperty("unwantedVal", String::class)
private val getUnwantedVal = isMethod("getUnwantedVal", String::class.java, List::class.java)
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteExtensionProperty() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVal>(PROPERTY_CLASS).apply {
assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal))
assertThat("getUnwantedVal not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVal))
assertThat("List.unwantedVal not found", kotlin.declaredMemberExtensionProperties, hasItem(unwantedVal))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVal>(PROPERTY_CLASS).apply {
assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal))
assertThat("getUnwantedVal still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal)))
assertThat("List.unwantedVal still exists", kotlin.declaredMemberExtensionProperties, not(hasItem(unwantedVal)))
}
}
}
}

View File

@ -0,0 +1,99 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.matcher.*
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsNot.not
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.reflect.full.declaredMemberProperties
import kotlin.test.assertFailsWith
class DeleteFieldTest {
companion object {
private const val STRING_FIELD_CLASS = "net.corda.gradle.HasStringFieldToDelete"
private const val INTEGER_FIELD_CLASS = "net.corda.gradle.HasIntFieldToDelete"
private const val LONG_FIELD_CLASS = "net.corda.gradle.HasLongFieldToDelete"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-field")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteStringField() {
val stringField = isProperty("stringField", String::class)
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(STRING_FIELD_CLASS).apply {
val obj: Any = getDeclaredConstructor(String::class.java).newInstance(MESSAGE)
getDeclaredField("stringField").also { field ->
assertEquals(MESSAGE, field.get(obj))
}
assertThat("stringField not found", kotlin.declaredMemberProperties, hasItem(stringField))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(STRING_FIELD_CLASS).apply {
assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE))
assertFailsWith<NoSuchFieldException> { getDeclaredField("stringField") }
assertThat("stringField still exists", kotlin.declaredMemberProperties, not(hasItem(stringField)))
}
}
}
@Test
fun deleteLongField() {
val longField = isProperty("longField", Long::class)
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(LONG_FIELD_CLASS).apply {
val obj: Any = getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER)
getDeclaredField("longField").also { field ->
assertEquals(BIG_NUMBER, field.get(obj))
}
assertThat("longField not found", kotlin.declaredMemberProperties, hasItem(longField))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(LONG_FIELD_CLASS).apply {
assertNotNull(getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER))
assertFailsWith<NoSuchFieldException> { getDeclaredField("longField") }
assertThat("longField still exists", kotlin.declaredMemberProperties, not(hasItem(longField)))
}
}
}
@Test
fun deleteIntegerField() {
val intField = isProperty("intField", Int::class)
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(INTEGER_FIELD_CLASS).apply {
val obj: Any = getDeclaredConstructor(Int::class.java).newInstance(NUMBER)
getDeclaredField("intField").also { field ->
assertEquals(NUMBER, field.get(obj))
}
assertThat("intField not found", kotlin.declaredMemberProperties, hasItem(intField))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(INTEGER_FIELD_CLASS).apply {
assertNotNull(getDeclaredConstructor(Int::class.java).newInstance(NUMBER))
assertFailsWith<NoSuchFieldException> { getDeclaredField("intField") }
assertThat("intField still exists", kotlin.declaredMemberProperties, not(hasItem(intField)))
}
}
}
}

View File

@ -0,0 +1,81 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.matcher.*
import net.corda.gradle.unwanted.HasString
import net.corda.gradle.unwanted.HasUnwantedFun
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsNot.not
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.jvm.kotlin
import kotlin.reflect.full.declaredFunctions
import kotlin.test.assertFailsWith
class DeleteFunctionTest {
companion object {
private const val FUNCTION_CLASS = "net.corda.gradle.HasFunctionToDelete"
private const val INDIRECT_CLASS = "net.corda.gradle.HasIndirectFunctionToDelete"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-function")
private val unwantedFun = isFunction("unwantedFun", String::class, String::class)
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedFun>(FUNCTION_CLASS).apply {
newInstance().also {
assertEquals(MESSAGE, it.unwantedFun(MESSAGE))
}
assertThat("unwantedFun(String) not found", kotlin.declaredFunctions, hasItem(unwantedFun))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedFun>(FUNCTION_CLASS).apply {
newInstance().also {
assertFailsWith<AbstractMethodError> { it.unwantedFun(MESSAGE) }
}
assertThat("unwantedFun(String) still exists", kotlin.declaredFunctions, not(hasItem(unwantedFun)))
}
}
}
@Test
fun deleteIndirectFunction() {
val stringData = isFunction("stringData", String::class)
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedFun>(INDIRECT_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also {
assertEquals(MESSAGE, it.unwantedFun(MESSAGE))
assertEquals(MESSAGE, (it as HasString).stringData())
}
assertThat("unwantedFun(String) not found", kotlin.declaredFunctions, hasItem(unwantedFun))
assertThat("stringData() not found", kotlin.declaredFunctions, hasItem(stringData))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedFun>(INDIRECT_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also {
assertFailsWith<AbstractMethodError> { it.unwantedFun(MESSAGE) }
assertFailsWith<AbstractMethodError> { (it as HasString).stringData() }
}
assertThat("unwantedFun(String) still exists", kotlin.declaredFunctions, not(hasItem(unwantedFun)))
assertThat("stringData still exists", kotlin.declaredFunctions, not(hasItem(stringData)))
}
}
}
}

View File

@ -0,0 +1,71 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.matcher.*
import net.corda.gradle.unwanted.HasUnwantedVal
import org.assertj.core.api.Assertions.*
import org.hamcrest.core.IsCollectionContaining.*
import org.hamcrest.core.IsNot.*
import org.junit.Assert.*
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.reflect.full.declaredMemberProperties
import kotlin.test.assertFailsWith
class DeleteLazyTest {
companion object {
private const val LAZY_VAL_CLASS = "net.corda.gradle.HasLazyVal"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-lazy")
private val unwantedVal = isProperty("unwantedVal", String::class)
private val getUnwantedVal = isMethod("getUnwantedVal", String::class.java)
private lateinit var sourceClasses: List<String>
private lateinit var filteredClasses: List<String>
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
@BeforeClass
@JvmStatic
fun setup() {
sourceClasses = testProject.sourceJar.getClassNames(LAZY_VAL_CLASS)
filteredClasses = testProject.filteredJar.getClassNames(LAZY_VAL_CLASS)
}
}
@Test
fun deletedClasses() {
assertThat(sourceClasses).contains(LAZY_VAL_CLASS)
assertThat(filteredClasses).containsExactly(LAZY_VAL_CLASS)
}
@Test
fun deleteLazyVal() {
assertThat(sourceClasses).anyMatch { it.contains("\$unwantedVal\$") }
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVal>(LAZY_VAL_CLASS).apply {
getConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.unwantedVal)
}
assertThat("getUnwantedVal not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVal))
assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVal>(LAZY_VAL_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getConstructor(String::class.java) }
assertThat("getUnwantedVal still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal)))
assertThat("unwantedVal still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVal)))
}
}
}
}

View File

@ -0,0 +1,90 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertFailsWith
class DeleteMultiFileTest {
companion object {
private const val MULTIFILE_CLASS = "net.corda.gradle.HasMultiData"
private const val STRING_METHOD = "stringToDelete"
private const val LONG_METHOD = "longToDelete"
private const val INT_METHOD = "intToDelete"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-multifile")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteStringFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(MULTIFILE_CLASS).apply {
getMethod(STRING_METHOD, String::class.java).also { method ->
method.invoke(null, MESSAGE).also { result ->
assertThat(result)
.isInstanceOf(String::class.java)
.isEqualTo(MESSAGE)
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(MULTIFILE_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getMethod(STRING_METHOD, String::class.java) }
}
}
}
@Test
fun deleteLongFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(MULTIFILE_CLASS).apply {
getMethod(LONG_METHOD, Long::class.java).also { method ->
method.invoke(null, BIG_NUMBER).also { result ->
assertThat(result)
.isInstanceOf(Long::class.javaObjectType)
.isEqualTo(BIG_NUMBER)
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(MULTIFILE_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getMethod(LONG_METHOD, Long::class.java) }
}
}
}
@Test
fun deleteIntFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(MULTIFILE_CLASS).apply {
getMethod(INT_METHOD, Int::class.java).also { method ->
method.invoke(null, NUMBER).also { result ->
assertThat(result)
.isInstanceOf(Int::class.javaObjectType)
.isEqualTo(NUMBER)
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(MULTIFILE_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getMethod(INT_METHOD, Int::class.java) }
}
}
}
}

View File

@ -0,0 +1,90 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.asm.classMetadata
import net.corda.gradle.jarfilter.matcher.isClass
import org.assertj.core.api.Assertions.*
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsNot.not
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertFailsWith
class DeleteNestedClassTest {
companion object {
private const val HOST_CLASS = "net.corda.gradle.HasNestedClasses"
private const val KEPT_CLASS = "$HOST_CLASS\$OneToKeep"
private const val DELETED_CLASS = "$HOST_CLASS\$OneToThrowAway"
private const val SEALED_CLASS = "net.corda.gradle.SealedClass"
private const val WANTED_SUBCLASS = "$SEALED_CLASS\$Wanted"
private const val UNWANTED_SUBCLASS = "$SEALED_CLASS\$Unwanted"
private val keptClass = isClass(KEPT_CLASS)
private val deletedClass = isClass(DELETED_CLASS)
private val wantedSubclass = isClass(WANTED_SUBCLASS)
private val unwantedSubclass = isClass(UNWANTED_SUBCLASS)
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-nested-class")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteNestedClass() {
classLoaderFor(testProject.sourceJar).use { cl ->
val deleted = cl.load<Any>(DELETED_CLASS)
val kept = cl.load<Any>(KEPT_CLASS)
cl.load<Any>(HOST_CLASS).apply {
assertThat(declaredClasses).containsExactlyInAnyOrder(deleted, kept)
assertThat("OneToThrowAway class is missing", kotlin.nestedClasses, hasItem(deletedClass))
assertThat("OneToKeep class is missing", kotlin.nestedClasses, hasItem(keptClass))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
assertFailsWith<ClassNotFoundException> { cl.load<Any>(DELETED_CLASS) }
val kept = cl.load<Any>(KEPT_CLASS)
cl.load<Any>(HOST_CLASS).apply {
assertThat(declaredClasses).containsExactly(kept)
assertThat("OneToThrowAway class still exists", kotlin.nestedClasses, not(hasItem(deletedClass)))
assertThat("OneToKeep class is missing", kotlin.nestedClasses, hasItem(keptClass))
}
}
}
@Test
fun deleteFromSealedClass() {
classLoaderFor(testProject.sourceJar).use { cl ->
val unwanted = cl.load<Any>(UNWANTED_SUBCLASS)
val wanted = cl.load<Any>(WANTED_SUBCLASS)
cl.load<Any>(SEALED_CLASS).apply {
assertTrue(kotlin.isSealed)
assertThat(declaredClasses).containsExactlyInAnyOrder(wanted, unwanted)
assertThat("Wanted class is missing", kotlin.nestedClasses, hasItem(wantedSubclass))
assertThat("Unwanted class is missing", kotlin.nestedClasses, hasItem(unwantedSubclass))
assertThat(classMetadata.sealedSubclasses).containsExactlyInAnyOrder(WANTED_SUBCLASS, UNWANTED_SUBCLASS)
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
assertFailsWith<ClassNotFoundException> { cl.load<Any>(UNWANTED_SUBCLASS) }
val wanted = cl.load<Any>(WANTED_SUBCLASS)
cl.load<Any>(SEALED_CLASS).apply {
assertTrue(kotlin.isSealed)
assertThat(declaredClasses).containsExactly(wanted)
assertThat("Unwanted class still exists", kotlin.nestedClasses, not(hasItem(unwantedSubclass)))
assertThat("Wanted class is missing", kotlin.nestedClasses, hasItem(wantedSubclass))
assertThat(classMetadata.sealedSubclasses).containsExactly(WANTED_SUBCLASS)
}
}
}
}

View File

@ -0,0 +1,89 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.HasUnwantedFun
import org.assertj.core.api.Assertions.*
import org.junit.Assert.*
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertFailsWith
class DeleteObjectTest {
companion object {
private const val OBJECT_CLASS = "net.corda.gradle.HasObjects"
private const val UNWANTED_OBJ_METHOD = "getUnwantedObj"
private const val UNWANTED_OBJ_FIELD = "unwantedObj"
private const val UNWANTED_FUN_METHOD = "unwantedFun"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-object")
private lateinit var sourceClasses: List<String>
private lateinit var filteredClasses: List<String>
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
@BeforeClass
@JvmStatic
fun setup() {
sourceClasses = testProject.sourceJar.getClassNames(OBJECT_CLASS)
filteredClasses = testProject.filteredJar.getClassNames(OBJECT_CLASS)
}
}
@Test
fun deletedClasses() {
assertThat(sourceClasses).contains(OBJECT_CLASS)
assertThat(filteredClasses).containsExactly(OBJECT_CLASS)
}
@Test
fun deleteObject() {
assertThat(sourceClasses).anyMatch { it.contains("\$unwantedObj\$") }
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(OBJECT_CLASS).apply {
getDeclaredMethod(UNWANTED_OBJ_METHOD).also { method ->
(method.invoke(null) as HasUnwantedFun).also { obj ->
assertEquals(MESSAGE, obj.unwantedFun(MESSAGE))
}
}
getDeclaredField(UNWANTED_OBJ_FIELD).also { field ->
assertFalse(field.isAccessible)
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(OBJECT_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod(UNWANTED_OBJ_METHOD) }
assertFailsWith<NoSuchFieldException> { getDeclaredField(UNWANTED_OBJ_FIELD) }
}
}
}
@Test
fun deleteFunctionWithObject() {
assertThat(sourceClasses).anyMatch { it.contains("\$unwantedFun\$") }
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(OBJECT_CLASS).apply {
getDeclaredMethod(UNWANTED_FUN_METHOD).also { method ->
assertEquals("<default-value>", method.invoke(null))
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(OBJECT_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod(UNWANTED_FUN_METHOD) }
}
}
}
}

View File

@ -0,0 +1,56 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.asm.classMetadata
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertFailsWith
/**
* Sealed classes can have non-nested subclasses, so long as those subclasses
* are declared in the same file as the sealed class. Check that the metadata
* is still updated correctly in this case.
*/
class DeleteSealedSubclassTest {
companion object {
private const val SEALED_CLASS = "net.corda.gradle.SealedBaseClass"
private const val WANTED_SUBCLASS = "net.corda.gradle.WantedSubclass"
private const val UNWANTED_SUBCLASS = "net.corda.gradle.UnwantedSubclass"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-sealed-subclass")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteUnwantedSubclass() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(WANTED_SUBCLASS)
cl.load<Any>(UNWANTED_SUBCLASS)
cl.load<Any>(SEALED_CLASS).apply {
assertTrue(kotlin.isSealed)
assertThat(classMetadata.sealedSubclasses)
.containsExactlyInAnyOrder(WANTED_SUBCLASS, UNWANTED_SUBCLASS)
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(WANTED_SUBCLASS)
assertFailsWith<ClassNotFoundException> { cl.load<Any>(UNWANTED_SUBCLASS) }
cl.load<Any>(SEALED_CLASS).apply {
assertTrue(kotlin.isSealed)
assertThat(classMetadata.sealedSubclasses)
.containsExactly(WANTED_SUBCLASS)
}
}
}
}

View File

@ -0,0 +1,74 @@
package net.corda.gradle.jarfilter
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class DeleteStaticFieldTest {
companion object {
private const val PROPERTY_CLASS = "net.corda.gradle.StaticFieldsToDelete"
private const val DEFAULT_BIG_NUMBER: Long = 123456789L
private const val DEFAULT_NUMBER: Int = 123456
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-static-field")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteStringField() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredField("stringField")
assertEquals(DEFAULT_MESSAGE, getter.get(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchFieldException> { getDeclaredField("stringField") }
}
}
}
@Test
fun deleteLongField() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredField("longField")
assertEquals(DEFAULT_BIG_NUMBER, getter.get(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchFieldException> { getDeclaredField("longField") }
}
}
}
@Test
fun deleteIntField() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredField("intField")
assertEquals(DEFAULT_NUMBER, getter.get(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchFieldException> { getDeclaredField("intField") }
}
}
}
}

View File

@ -0,0 +1,87 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertFailsWith
class DeleteStaticFunctionTest {
companion object {
private const val FUNCTION_CLASS = "net.corda.gradle.StaticFunctionsToDelete"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-static-function")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteStringFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getMethod("unwantedStringToDelete", String::class.java).also { method ->
method.invoke(null, MESSAGE).also { result ->
assertThat(result)
.isInstanceOf(String::class.java)
.isEqualTo(MESSAGE)
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getMethod("unwantedStringToDelete", String::class.java) }
}
}
}
@Test
fun deleteLongFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getMethod("unwantedLongToDelete", Long::class.java).also { method ->
method.invoke(null, BIG_NUMBER).also { result ->
assertThat(result)
.isInstanceOf(Long::class.javaObjectType)
.isEqualTo(BIG_NUMBER)
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getMethod("unwantedLongToDelete", Long::class.java) }
}
}
}
@Test
fun deleteIntFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getMethod("unwantedIntToDelete", Int::class.java).also { method ->
method.invoke(null, NUMBER).also { result ->
assertThat(result)
.isInstanceOf(Int::class.javaObjectType)
.isEqualTo(NUMBER)
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getMethod("unwantedIntToDelete", Int::class.java) }
}
}
}
}

View File

@ -0,0 +1,91 @@
package net.corda.gradle.jarfilter
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class DeleteStaticValPropertyTest {
companion object {
private const val PROPERTY_CLASS = "net.corda.gradle.StaticValToDelete"
private const val DEFAULT_BIG_NUMBER: Long = 123456789L
private const val DEFAULT_NUMBER: Int = 123456
private object LocalBlob
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-static-val")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteStringVal() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredMethod("getStringVal")
assertEquals(DEFAULT_MESSAGE, getter.invoke(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("getStringVal") }
}
}
}
@Test
fun deleteLongVal() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredMethod("getLongVal")
assertEquals(DEFAULT_BIG_NUMBER, getter.invoke(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("getLongVal") }
}
}
}
@Test
fun deleteIntVal() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredMethod("getIntVal")
assertEquals(DEFAULT_NUMBER, getter.invoke(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("getIntVal") }
}
}
}
@Test
fun deleteMemberVal() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredMethod("getMemberVal", Any::class.java)
assertEquals(LocalBlob, getter.invoke(null, LocalBlob))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("getMemberVal", Any::class.java) }
}
}
}
}

View File

@ -0,0 +1,106 @@
package net.corda.gradle.jarfilter
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertFailsWith
class DeleteStaticVarPropertyTest {
companion object {
private const val PROPERTY_CLASS = "net.corda.gradle.StaticVarToDelete"
private const val DEFAULT_BIG_NUMBER: Long = 123456789L
private const val DEFAULT_NUMBER: Int = 123456
private object LocalBlob
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-static-var")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteStringVar() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredMethod("getStringVar")
val setter = getDeclaredMethod("setStringVar", String::class.java)
assertEquals(DEFAULT_MESSAGE, getter.invoke(null))
setter.invoke(null, MESSAGE)
assertEquals(MESSAGE, getter.invoke(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("getStringVar") }
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("setStringVar", String::class.java) }
}
}
}
@Test
fun deleteLongVar() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredMethod("getLongVar")
val setter = getDeclaredMethod("setLongVar", Long::class.java)
assertEquals(DEFAULT_BIG_NUMBER, getter.invoke(null))
setter.invoke(null, BIG_NUMBER)
assertEquals(BIG_NUMBER, getter.invoke(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("getLongVar") }
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("setLongVar", Long::class.java) }
}
}
}
@Test
fun deleteIntVar() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredMethod("getIntVar")
val setter = getDeclaredMethod("setIntVar", Int::class.java)
assertEquals(DEFAULT_NUMBER, getter.invoke(null))
setter.invoke(null, NUMBER)
assertEquals(NUMBER, getter.invoke(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("getIntVar") }
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("setIntVar", Int::class.java) }
}
}
}
@Test
fun deleteMemberVar() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
val getter = getDeclaredMethod("getMemberVar", Any::class.java)
val setter = getDeclaredMethod("setMemberVar", Any::class.java, Any::class.java)
assertEquals(LocalBlob, getter.invoke(null, LocalBlob))
setter.invoke(null, LocalBlob, LocalBlob)
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(PROPERTY_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("getMemberVar", Any::class.java) }
assertFailsWith<NoSuchMethodException> { getDeclaredMethod("setMemberVar", Any::class.java, Any::class.java) }
}
}
}
}

View File

@ -0,0 +1,48 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.asm.fileMetadata
import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
class DeleteTypeAliasFromFileTest {
companion object {
private const val TYPEALIAS_CLASS = "net.corda.gradle.FileWithTypeAlias"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-file-typealias")
private lateinit var sourceClasses: List<String>
private lateinit var filteredClasses: List<String>
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
@BeforeClass
@JvmStatic
fun setup() {
sourceClasses = testProject.sourceJar.getClassNames(TYPEALIAS_CLASS)
filteredClasses = testProject.filteredJar.getClassNames(TYPEALIAS_CLASS)
}
}
@Test
fun deleteTypeAlias() {
classLoaderFor(testProject.sourceJar).use { cl ->
val metadata = cl.load<Any>(TYPEALIAS_CLASS).fileMetadata
assertThat(metadata.typeAliasNames)
.containsExactlyInAnyOrder("FileWantedType", "FileUnwantedType")
}
classLoaderFor(testProject.filteredJar).use { cl ->
val metadata = cl.load<Any>(TYPEALIAS_CLASS).fileMetadata
assertThat(metadata.typeAliasNames)
.containsExactly("FileWantedType")
}
}
}

View File

@ -0,0 +1,102 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.matcher.*
import net.corda.gradle.unwanted.HasUnwantedVal
import org.hamcrest.core.IsCollectionContaining.*
import org.hamcrest.core.IsNot.*
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.reflect.full.declaredMemberProperties
import kotlin.test.assertFailsWith
class DeleteValPropertyTest {
companion object {
private const val PROPERTY_CLASS = "net.corda.gradle.HasValPropertyForDelete"
private const val GETTER_CLASS = "net.corda.gradle.HasValGetterForDelete"
private const val JVM_FIELD_CLASS = "net.corda.gradle.HasValJvmFieldForDelete"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-val-property")
private val unwantedVal = isProperty("unwantedVal", String::class)
private val getUnwantedVal = isMethod("getUnwantedVal", String::class.java)
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteProperty() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVal>(PROPERTY_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.unwantedVal)
}
assertFalse(getDeclaredField("unwantedVal").isAccessible)
assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal))
assertThat("getUnwantedVal not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVal))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVal>(PROPERTY_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertFailsWith<AbstractMethodError> { obj.unwantedVal }
}
assertFailsWith<NoSuchFieldException> { getDeclaredField("unwantedVal") }
assertThat("unwantedVal still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVal)))
assertThat("getUnwantedVal still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal)))
}
}
}
@Test
fun deleteGetter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVal>(GETTER_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.unwantedVal)
}
assertFalse(getDeclaredField("unwantedVal").isAccessible)
assertThat("getUnwantedVal not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVal))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVal>(GETTER_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertFailsWith<AbstractMethodError> { obj.unwantedVal }
}
assertFalse(getDeclaredField("unwantedVal").isAccessible)
assertThat("getUnwantedVal still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVal)))
}
}
}
@Test
fun deleteJvmField() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(JVM_FIELD_CLASS).apply {
val obj = getDeclaredConstructor(String::class.java).newInstance(MESSAGE)
getDeclaredField("unwantedVal").also { field ->
assertEquals(MESSAGE, field.get(obj))
}
assertThat("unwantedVal not found", kotlin.declaredMemberProperties, hasItem(unwantedVal))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(JVM_FIELD_CLASS).apply {
assertNotNull(getDeclaredConstructor(String::class.java).newInstance(MESSAGE))
assertFailsWith<NoSuchFieldException> { getDeclaredField("unwantedVal") }
assertThat("unwantedVal still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVal)))
}
}
}
}

View File

@ -0,0 +1,141 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.matcher.*
import net.corda.gradle.unwanted.HasUnwantedVar
import org.hamcrest.core.IsCollectionContaining.*
import org.hamcrest.core.IsNot.*
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.reflect.full.declaredMemberProperties
import kotlin.test.assertFailsWith
class DeleteVarPropertyTest {
companion object {
private const val PROPERTY_CLASS = "net.corda.gradle.HasUnwantedVarPropertyForDelete"
private const val GETTER_CLASS = "net.corda.gradle.HasUnwantedGetForDelete"
private const val SETTER_CLASS = "net.corda.gradle.HasUnwantedSetForDelete"
private const val JVM_FIELD_CLASS = "net.corda.gradle.HasVarJvmFieldForDelete"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "delete-var-property")
private val unwantedVar = isProperty("unwantedVar", String::class)
private val getUnwantedVar = isMethod("getUnwantedVar", String::class.java)
private val setUnwantedVar = isMethod("setUnwantedVar", Void.TYPE, String::class.java)
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteProperty() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVar>(PROPERTY_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj ->
assertEquals(DEFAULT_MESSAGE, obj.unwantedVar)
obj.unwantedVar = MESSAGE
assertEquals(MESSAGE, obj.unwantedVar)
}
assertFalse(getDeclaredField("unwantedVar").isAccessible)
assertThat("unwantedVar not found", kotlin.declaredMemberProperties, hasItem(unwantedVar))
assertThat("getUnwantedVar not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVar))
assertThat("setUnwantedVar not found", kotlin.javaDeclaredMethods, hasItem(setUnwantedVar))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVar>(PROPERTY_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj ->
assertFailsWith<AbstractMethodError> { obj.unwantedVar }
assertFailsWith<AbstractMethodError> { obj.unwantedVar = MESSAGE }
}
assertFailsWith<NoSuchFieldException> { getDeclaredField("unwantedVar") }
assertThat("unwantedVar still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVar)))
assertThat("getUnwantedVar still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVar)))
assertThat("setUnwantedVar still exists", kotlin.javaDeclaredMethods, not(hasItem(setUnwantedVar)))
}
}
}
@Test
fun deleteGetter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVar>(GETTER_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.unwantedVar)
}
assertFalse(getDeclaredField("unwantedVar").isAccessible)
assertThat("getUnwantedVar not found", kotlin.javaDeclaredMethods, hasItem(getUnwantedVar))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVar>(GETTER_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertFailsWith<AbstractMethodError> { obj.unwantedVar }
}
assertFalse(getDeclaredField("unwantedVar").isAccessible)
assertThat("getUnwantedVar still exists", kotlin.javaDeclaredMethods, not(hasItem(getUnwantedVar)))
}
}
}
@Test
fun deleteSetter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVar>(SETTER_CLASS).apply {
getConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj ->
assertEquals(DEFAULT_MESSAGE, obj.unwantedVar)
obj.unwantedVar = MESSAGE
assertEquals(MESSAGE, obj.unwantedVar)
}
getDeclaredField("unwantedVar").also { field ->
assertFalse(field.isAccessible)
}
assertThat("setUnwantedVar not found", kotlin.javaDeclaredMethods, hasItem(setUnwantedVar))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVar>(SETTER_CLASS).apply {
getConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj ->
assertEquals(DEFAULT_MESSAGE, obj.unwantedVar)
assertFailsWith<AbstractMethodError> { obj.unwantedVar = MESSAGE }
}
getDeclaredField("unwantedVar").also { field ->
assertFalse(field.isAccessible)
}
assertThat("setUnwantedVar still exists", kotlin.javaDeclaredMethods, not(hasItem(setUnwantedVar)))
}
}
}
@Test
fun deleteJvmField() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(JVM_FIELD_CLASS).apply {
val obj: Any = getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE)
getDeclaredField("unwantedVar").also { field ->
assertEquals(DEFAULT_MESSAGE, field.get(obj))
field.set(obj, MESSAGE)
assertEquals(MESSAGE, field.get(obj))
}
assertThat("unwantedVar not found", kotlin.declaredMemberProperties, hasItem(unwantedVar))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(JVM_FIELD_CLASS).apply {
assertNotNull(getDeclaredConstructor(String::class.java).newInstance(DEFAULT_MESSAGE))
assertFailsWith<NoSuchFieldException> { getDeclaredField("unwantedVar") }
assertThat("unwantedVar still exists", kotlin.declaredMemberProperties, not(hasItem(unwantedVar)))
}
}
}
}

View File

@ -0,0 +1,104 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.asm.bytecode
import net.corda.gradle.jarfilter.asm.resourceName
import org.assertj.core.api.Assertions.*
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.nio.file.Files
import java.nio.file.Path
import java.util.jar.Attributes.Name.MANIFEST_VERSION
import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import java.util.zip.CRC32
import java.util.zip.Deflater.NO_COMPRESSION
import java.util.zip.ZipEntry
import java.util.zip.ZipEntry.*
/**
* Creates a dummy jar containing the following:
* - META-INF/MANIFEST.MF
* - A compressed class file
* - A compressed binary non-class file
* - An uncompressed text file
* - A directory entry
*
* The compression level is set to NO_COMPRESSION
* in order to force the Gradle task to compress
* the entries properly.
*/
class DummyJar(
private val projectDir: TemporaryFolder,
private val testClass: Class<*>,
private val name: String
) : TestRule {
private companion object {
private const val DATA_SIZE = 512
private fun uncompressed(name: String, data: ByteArray) = ZipEntry(name).apply {
method = STORED
compressedSize = data.size.toLong()
size = data.size.toLong()
crc = CRC32().let { crc ->
crc.update(data)
crc.value
}
}
private fun compressed(name: String) = ZipEntry(name).apply { method = DEFLATED }
private fun directoryOf(type: Class<*>)
= directory(type.`package`.name.toPathFormat + '/')
private fun directory(name: String) = ZipEntry(name).apply {
method = STORED
compressedSize = 0
size = 0
crc = 0
}
}
private lateinit var _path: Path
val path: Path get() = _path
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
val manifest = Manifest().apply {
mainAttributes.also { main ->
main[MANIFEST_VERSION] = "1.0"
}
}
_path = projectDir.pathOf("$name.jar")
JarOutputStream(Files.newOutputStream(_path), manifest).use { jar ->
jar.setComment(testClass.name)
jar.setLevel(NO_COMPRESSION)
// One directory entry (stored)
jar.putNextEntry(directoryOf(testClass))
// One compressed class file
jar.putNextEntry(compressed(testClass.resourceName))
jar.write(testClass.bytecode)
// One compressed non-class file
jar.putNextEntry(compressed("binary.dat"))
jar.write(arrayOfJunk(DATA_SIZE))
// One uncompressed text file
val text = """
Jar: ${_path.toAbsolutePath()}
Class: ${testClass.name}
""".toByteArray()
jar.putNextEntry(uncompressed("comment.txt", text))
jar.write(text)
}
assertThat(_path).isRegularFile()
base.evaluate()
}
}
}
}

View File

@ -0,0 +1,8 @@
@file:JvmName("EmptyPackage")
@file:Suppress("UNUSED")
package net.corda.gradle.jarfilter
/*
* We need to put something in here so that Kotlin will create a class file.
*/
const val PLACEHOLDER = 0

View File

@ -0,0 +1,32 @@
package net.corda.gradle.jarfilter
import org.junit.Assert.*
import org.junit.Test
class FieldElementTest {
private companion object {
private const val DESCRIPTOR = "Ljava.lang.String;"
}
@Test
fun testFieldsMatchByNameOnly() {
val elt = FieldElement(name = "fieldName", descriptor = DESCRIPTOR)
assertEquals(FieldElement(name = "fieldName"), elt)
}
@Test
fun testFieldWithDescriptorDoesNotExpire() {
val elt = FieldElement(name = "fieldName", descriptor = DESCRIPTOR)
assertFalse(elt.isExpired)
assertFalse(elt.isExpired)
assertFalse(elt.isExpired)
}
@Test
fun testFieldWithoutDescriptorDoesExpire() {
val elt = FieldElement(name = "fieldName")
assertFalse(elt.isExpired)
assertTrue(elt.isExpired)
assertTrue(elt.isExpired)
}
}

View File

@ -0,0 +1,212 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.annotations.Deletable
import net.corda.gradle.jarfilter.asm.bytecode
import net.corda.gradle.jarfilter.asm.toClass
import net.corda.gradle.jarfilter.matcher.isProperty
import org.gradle.api.logging.Logger
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsEqual.equalTo
import org.hamcrest.core.IsNot.not
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertThat
import org.junit.Test
import org.objectweb.asm.ClassWriter.COMPUTE_MAXS
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.jvm.jvmName
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
/**
* Demonstrate that we can still instantiate objects, even after we've deleted
* one of their properties. (Check we haven't blown the constructor away!)
*/
class FieldRemovalTest {
companion object {
private val logger: Logger = StdOutLogging(FieldRemovalTest::class)
private const val SHORT_NUMBER = 999.toShort()
private const val BYTE_NUMBER = 99.toByte()
private const val BIG_FLOATING_POINT = 9999999.9999
private const val FLOATING_POINT = 9999.99f
private val objectField = isProperty(equalTo("objectField"), equalTo("T"))
private val longField = isProperty("longField", Long::class)
private val intField = isProperty("intField", Int::class)
private val shortField = isProperty("shortField", Short::class)
private val byteField = isProperty("byteField", Byte::class)
private val charField = isProperty("charField", Char::class)
private val booleanField = isProperty("booleanField", Boolean::class)
private val doubleField = isProperty("doubleField", Double::class)
private val floatField = isProperty("floatField", Float::class)
private val arrayField = isProperty("arrayField", ByteArray::class)
}
private inline fun <reified T: R, reified R: Any> transform(): Class<out R> = transform(T::class.java, R::class.java)
private fun <T: R, R: Any> transform(type: Class<in T>, asType: Class<out R>): Class<out R> {
val bytecode = type.bytecode.execute({ writer ->
ClassTransformer(
visitor = writer,
logger = logger,
removeAnnotations = emptySet(),
deleteAnnotations = setOf(Deletable::class.jvmName.descriptor),
stubAnnotations = emptySet(),
unwantedClasses = mutableSetOf()
)
}, COMPUTE_MAXS)
return bytecode.toClass(type, asType)
}
@Test
fun removeObject() {
val sourceField = SampleGenericField(MESSAGE)
assertEquals(MESSAGE, sourceField.objectField)
assertThat("objectField not found", sourceField::class.declaredMemberProperties, hasItem(objectField))
val targetField = transform<SampleGenericField<String>, HasGenericField<String>>()
.getDeclaredConstructor(Any::class.java).newInstance(MESSAGE)
assertFailsWith<AbstractMethodError> { targetField.objectField }
assertFailsWith<AbstractMethodError> { targetField.objectField = "New Value" }
assertThat("objectField still exists", targetField::class.declaredMemberProperties, not(hasItem(objectField)))
}
@Test
fun removeLong() {
val sourceField = SampleLongField(BIG_NUMBER)
assertEquals(BIG_NUMBER, sourceField.longField)
assertThat("longField not found", sourceField::class.declaredMemberProperties, hasItem(longField))
val targetField = transform<SampleLongField, HasLongField>()
.getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER)
assertFailsWith<AbstractMethodError> { targetField.longField }
assertFailsWith<AbstractMethodError> { targetField.longField = 10L }
assertThat("longField still exists", targetField::class.declaredMemberProperties, not(hasItem(longField)))
}
@Test
fun removeInt() {
val sourceField = SampleIntField(NUMBER)
assertEquals(NUMBER, sourceField.intField)
assertThat("intField not found", sourceField::class.declaredMemberProperties, hasItem(intField))
val targetField = transform<SampleIntField, HasIntField>()
.getDeclaredConstructor(Int::class.java).newInstance(NUMBER)
assertFailsWith<AbstractMethodError> { targetField.intField }
assertFailsWith<AbstractMethodError> { targetField.intField = 100 }
assertThat("intField still exists", targetField::class.declaredMemberProperties, not(hasItem(intField)))
}
@Test
fun removeShort() {
val sourceField = SampleShortField(SHORT_NUMBER)
assertEquals(SHORT_NUMBER, sourceField.shortField)
assertThat("shortField not found", sourceField::class.declaredMemberProperties, hasItem(shortField))
val targetField = transform<SampleShortField, HasShortField>()
.getDeclaredConstructor(Short::class.java).newInstance(SHORT_NUMBER)
assertFailsWith<AbstractMethodError> { targetField.shortField }
assertFailsWith<AbstractMethodError> { targetField.shortField = 15 }
assertThat("shortField still exists", targetField::class.declaredMemberProperties, not(hasItem(shortField)))
}
@Test
fun removeByte() {
val sourceField = SampleByteField(BYTE_NUMBER)
assertEquals(BYTE_NUMBER, sourceField.byteField)
assertThat("byteField not found", sourceField::class.declaredMemberProperties, hasItem(byteField))
val targetField = transform<SampleByteField, HasByteField>()
.getDeclaredConstructor(Byte::class.java).newInstance(BYTE_NUMBER)
assertFailsWith<AbstractMethodError> { targetField.byteField }
assertFailsWith<AbstractMethodError> { targetField.byteField = 16 }
assertThat("byteField still exists", targetField::class.declaredMemberProperties, not(hasItem(byteField)))
}
@Test
fun removeBoolean() {
val sourceField = SampleBooleanField(true)
assertTrue(sourceField.booleanField)
assertThat("booleanField not found", sourceField::class.declaredMemberProperties, hasItem(booleanField))
val targetField = transform<SampleBooleanField, HasBooleanField>()
.getDeclaredConstructor(Boolean::class.java).newInstance(true)
assertFailsWith<AbstractMethodError> { targetField.booleanField }
assertFailsWith<AbstractMethodError> { targetField.booleanField = false }
assertThat("booleanField still exists", targetField::class.declaredMemberProperties, not(hasItem(booleanField)))
}
@Test
fun removeChar() {
val sourceField = SampleCharField('?')
assertEquals('?', sourceField.charField)
assertThat("charField not found", sourceField::class.declaredMemberProperties, hasItem(charField))
val targetField = transform<SampleCharField, HasCharField>()
.getDeclaredConstructor(Char::class.java).newInstance('?')
assertFailsWith<AbstractMethodError> { targetField.charField }
assertFailsWith<AbstractMethodError> { targetField.charField = 'A' }
assertThat("charField still exists", targetField::class.declaredMemberProperties, not(hasItem(charField)))
}
@Test
fun removeDouble() {
val sourceField = SampleDoubleField(BIG_FLOATING_POINT)
assertEquals(BIG_FLOATING_POINT, sourceField.doubleField)
assertThat("doubleField not found", sourceField::class.declaredMemberProperties, hasItem(doubleField))
val targetField = transform<SampleDoubleField, HasDoubleField>()
.getDeclaredConstructor(Double::class.java).newInstance(BIG_FLOATING_POINT)
assertFailsWith<AbstractMethodError> { targetField.doubleField }
assertFailsWith<AbstractMethodError> { targetField.doubleField = 12345.678 }
assertThat("doubleField still exists", targetField::class.declaredMemberProperties, not(hasItem(doubleField)))
}
@Test
fun removeFloat() {
val sourceField = SampleFloatField(FLOATING_POINT)
assertEquals(FLOATING_POINT, sourceField.floatField)
assertThat("floatField not found", sourceField::class.declaredMemberProperties, hasItem(floatField))
val targetField = transform<SampleFloatField, HasFloatField>()
.getDeclaredConstructor(Float::class.java).newInstance(FLOATING_POINT)
assertFailsWith<AbstractMethodError> { targetField.floatField }
assertFailsWith<AbstractMethodError> { targetField.floatField = 123.45f }
assertThat("floatField still exists", targetField::class.declaredMemberProperties, not(hasItem(floatField)))
}
@Test
fun removeArray() {
val sourceField = SampleArrayField(byteArrayOf())
assertArrayEquals(byteArrayOf(), sourceField.arrayField)
assertThat("arrayField not found", sourceField::class.declaredMemberProperties, hasItem(arrayField))
val targetField = transform<SampleArrayField, HasArrayField>()
.getDeclaredConstructor(ByteArray::class.java).newInstance(byteArrayOf())
assertFailsWith<AbstractMethodError> { targetField.arrayField }
assertFailsWith<AbstractMethodError> { targetField.arrayField = byteArrayOf(0x35, 0x73) }
assertThat("arrayField still exists", targetField::class.declaredMemberProperties, not(hasItem(arrayField)))
}
}
interface HasGenericField<T> { var objectField: T }
interface HasLongField { var longField: Long }
interface HasIntField { var intField: Int }
interface HasShortField { var shortField: Short }
interface HasByteField { var byteField: Byte }
interface HasBooleanField { var booleanField: Boolean }
interface HasCharField { var charField: Char }
interface HasFloatField { var floatField: Float }
interface HasDoubleField { var doubleField: Double }
interface HasArrayField { var arrayField: ByteArray }
internal class SampleGenericField<T>(@Deletable override var objectField: T) : HasGenericField<T>
internal class SampleLongField(@Deletable override var longField: Long) : HasLongField
internal class SampleIntField(@Deletable override var intField: Int) : HasIntField
internal class SampleShortField(@Deletable override var shortField: Short) : HasShortField
internal class SampleByteField(@Deletable override var byteField: Byte) : HasByteField
internal class SampleBooleanField(@Deletable override var booleanField: Boolean) : HasBooleanField
internal class SampleCharField(@Deletable override var charField: Char) : HasCharField
internal class SampleFloatField(@Deletable override var floatField: Float) : HasFloatField
internal class SampleDoubleField(@Deletable override var doubleField: Double) : HasDoubleField
internal class SampleArrayField(@Deletable override var arrayField: ByteArray) : HasArrayField

View File

@ -0,0 +1,61 @@
package net.corda.gradle.jarfilter
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import java.lang.reflect.Modifier.*
import kotlin.test.assertFailsWith
class InterfaceFunctionTest {
companion object {
private const val FUNCTION_CLASS = "net.corda.gradle.InterfaceFunctions"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "interface-function")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteInterfaceFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getMethod("toDelete", Long::class.java).also { method ->
assertEquals(ABSTRACT, method.modifiers and ABSTRACT)
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
assertFailsWith<NoSuchMethodException> { getMethod("toDelete", Long::class.java) }
}
}
}
@Test
fun cannotStubInterfaceFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getMethod("toStubOut", Long::class.java).also { method ->
assertEquals(ABSTRACT, method.modifiers and ABSTRACT)
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getMethod("toStubOut", Long::class.java).also { method ->
assertEquals(ABSTRACT, method.modifiers and ABSTRACT)
}
}
}
}
}

View File

@ -0,0 +1,272 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.*
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.BuildTask
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class JarFilterConfigurationTest {
private companion object {
private const val AMBIGUOUS = "net.corda.gradle.jarfilter.Ambiguous"
private const val DELETE = "net.corda.gradle.jarfilter.DeleteMe"
private const val REMOVE = "net.corda.gradle.jarfilter.RemoveMe"
private const val STUB = "net.corda.gradle.jarfilter.StubMeOut"
}
@Rule
@JvmField
val testProjectDir = TemporaryFolder()
private lateinit var output: String
@Before
fun setup() {
testProjectDir.installResource("gradle.properties")
}
@Test
fun checkNoJarMeansNoSource() {
val result = gradleProject("""
plugins {
id 'java'
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
annotations {
forDelete = ["$DELETE"]
}
}
""").build()
output = result.output
println(output)
val jarFilter = result.forTask("jarFilter")
assertEquals(NO_SOURCE, jarFilter.outcome)
}
@Test
fun checkWithMissingJar() {
val result = gradleProject("""
plugins {
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars = file('does-not-exist.jar')
}
""").buildAndFail()
output = result.output
println(output)
assertThat(output).containsSubsequence(
"Caused by: org.gradle.api.GradleException:",
"Caused by: java.io.FileNotFoundException:"
)
val jarFilter = result.forTask("jarFilter")
assertEquals(FAILED, jarFilter.outcome)
}
@Test
fun checkSameAnnotationForRemoveAndDelete() {
val result = gradleProject("""
plugins {
id 'java'
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars = jar
annotations {
forDelete = ["$AMBIGUOUS"]
forRemove = ["$AMBIGUOUS"]
}
}
""").buildAndFail()
output = result.output
println(output)
assertThat(output).containsSequence(
"Caused by: org.gradle.api.InvalidUserDataException: Annotation 'net.corda.gradle.jarfilter.Ambiguous' also appears in JarFilter 'forDelete' section"
)
val jarFilter = result.forTask("jarFilter")
assertEquals(FAILED, jarFilter.outcome)
}
@Test
fun checkSameAnnotationForRemoveAndStub() {
val result = gradleProject("""
plugins {
id 'java'
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars = jar
annotations {
forStub = ["$AMBIGUOUS"]
forRemove = ["$AMBIGUOUS"]
}
}
""").buildAndFail()
output = result.output
println(output)
assertThat(output).containsSequence(
"Caused by: org.gradle.api.InvalidUserDataException: Annotation 'net.corda.gradle.jarfilter.Ambiguous' also appears in JarFilter 'forStub' section"
)
val jarFilter = result.forTask("jarFilter")
assertEquals(FAILED, jarFilter.outcome)
}
@Test
fun checkSameAnnotationForStubAndDelete() {
val result = gradleProject("""
plugins {
id 'java'
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars = jar
annotations {
forStub = ["$AMBIGUOUS"]
forDelete = ["$AMBIGUOUS"]
}
}
""").buildAndFail()
output = result.output
println(output)
assertThat(output).containsSequence(
"Caused by: org.gradle.api.InvalidUserDataException: Annotation 'net.corda.gradle.jarfilter.Ambiguous' also appears in JarFilter 'forStub' section"
)
val jarFilter = result.forTask("jarFilter")
assertEquals(FAILED, jarFilter.outcome)
}
@Test
fun checkSameAnnotationForStubAndDeleteAndRemove() {
val result = gradleProject("""
plugins {
id 'java'
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars = jar
annotations {
forStub = ["$AMBIGUOUS"]
forDelete = ["$AMBIGUOUS"]
forRemove = ["$AMBIGUOUS"]
}
}
""").buildAndFail()
output = result.output
println(output)
assertThat(output).containsSequence(
"Caused by: org.gradle.api.InvalidUserDataException: Annotation 'net.corda.gradle.jarfilter.Ambiguous' also appears in JarFilter 'forDelete' section"
)
val jarFilter = result.forTask("jarFilter")
assertEquals(FAILED, jarFilter.outcome)
}
@Test
fun checkRepeatedAnnotationForDelete() {
val result = gradleProject("""
plugins {
id 'java'
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars = jar
annotations {
forDelete = ["$DELETE", "$DELETE"]
}
}
""").build()
output = result.output
println(output)
val jarFilter = result.forTask("jarFilter")
assertEquals(SUCCESS, jarFilter.outcome)
}
@Test
fun checkRepeatedAnnotationForStub() {
val result = gradleProject("""
plugins {
id 'java'
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars = jar
annotations {
forStub = ["$STUB", "$STUB"]
}
}
""").build()
output = result.output
println(output)
val jarFilter = result.forTask("jarFilter")
assertEquals(SUCCESS, jarFilter.outcome)
}
@Test
fun checkRepeatedAnnotationForRemove() {
val result = gradleProject("""
plugins {
id 'java'
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars = jar
annotations {
forRemove = ["$REMOVE", "$REMOVE"]
}
}
""").build()
output = result.output
println(output)
val jarFilter = result.forTask("jarFilter")
assertEquals(SUCCESS, jarFilter.outcome)
}
private fun gradleProject(script: String): GradleRunner {
testProjectDir.newFile("build.gradle").writeText(script)
return GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments(getBasicArgsForTasks("jarFilter", "--stacktrace"))
.withPluginClasspath()
}
private fun BuildResult.forTask(name: String): BuildTask {
return task(":$name") ?: throw AssertionError("No outcome for $name task")
}
}

View File

@ -0,0 +1,56 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.*
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Assert.*
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.io.FileNotFoundException
import java.nio.file.Path
class JarFilterProject(private val projectDir: TemporaryFolder, private val name: String) : TestRule {
private var _sourceJar: Path? = null
val sourceJar: Path get() = _sourceJar ?: throw FileNotFoundException("Input not found")
private var _filteredJar: Path? = null
val filteredJar: Path get() = _filteredJar ?: throw FileNotFoundException("Output not found")
private var _output: String = ""
val output: String get() = _output
override fun apply(statement: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
projectDir.installResources(
"$name/build.gradle",
"repositories.gradle",
"gradle.properties",
"settings.gradle"
)
val result = GradleRunner.create()
.withProjectDir(projectDir.root)
.withArguments(getGradleArgsForTasks("jarFilter"))
.withPluginClasspath()
.build()
_output = result.output
println(output)
val jarFilter = result.task(":jarFilter")
?: throw AssertionError("No outcome for jarFilter task")
assertEquals(SUCCESS, jarFilter.outcome)
_sourceJar = projectDir.pathOf("build", "libs", "$name.jar")
assertThat(sourceJar).isRegularFile()
_filteredJar = projectDir.pathOf("build", "filtered-libs", "$name-filtered.jar")
assertThat(filteredJar).isRegularFile()
statement.evaluate()
}
}
}
}

View File

@ -0,0 +1,107 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.*
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import org.junit.runners.model.Statement
import java.nio.file.Path
import java.nio.file.attribute.FileTime
import java.util.*
import java.util.Calendar.FEBRUARY
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
class JarFilterTimestampTest {
companion object {
private val testProjectDir = TemporaryFolder()
private val sourceJar = DummyJar(testProjectDir, JarFilterTimestampTest::class.java, "timestamps")
private val CONSTANT_TIME: FileTime = FileTime.fromMillis(
GregorianCalendar(1980, FEBRUARY, 1).apply {
timeZone = TimeZone.getTimeZone("UTC")
}.timeInMillis
)
private lateinit var filteredJar: Path
private lateinit var output: String
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(sourceJar)
.around(createTestProject())
private fun createTestProject() = TestRule { base, _ ->
object : Statement() {
override fun evaluate() {
testProjectDir.installResource("gradle.properties")
testProjectDir.newFile("build.gradle").writeText("""
plugins {
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars file("${sourceJar.path.toUri()}")
preserveTimestamps = false
}
""")
val result = GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments(getGradleArgsForTasks("jarFilter"))
.withPluginClasspath()
.build()
output = result.output
println(output)
val metafix = result.task(":jarFilter")
?: throw AssertionError("No outcome for jarFilter task")
assertEquals(SUCCESS, metafix.outcome)
filteredJar = testProjectDir.pathOf("build", "filtered-libs", "timestamps-filtered.jar")
assertThat(filteredJar).isRegularFile()
base.evaluate()
}
}
}
private val ZipEntry.methodName: String get() = if (method == ZipEntry.STORED) "Stored" else "Deflated"
}
@Test
fun fileTimestampsAreRemoved() {
var directoryCount = 0
var classCount = 0
var otherCount = 0
ZipFile(filteredJar.toFile()).use { jar ->
for (entry in jar.entries()) {
println("Entry: ${entry.name}")
println("- ${entry.methodName} (${entry.size} size / ${entry.compressedSize} compressed) bytes")
assertThat(entry.lastModifiedTime).isEqualTo(CONSTANT_TIME)
assertThat(entry.lastAccessTime).isNull()
assertThat(entry.creationTime).isNull()
if (entry.isDirectory) {
++directoryCount
} else if (entry.name.endsWith(".class")) {
++classCount
} else {
++otherCount
}
}
}
assertThat(directoryCount).isGreaterThan(0)
assertThat(classCount).isGreaterThan(0)
assertThat(otherCount).isGreaterThan(0)
}
}

View File

@ -0,0 +1,47 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.asm.bytecode
import net.corda.gradle.jarfilter.asm.toClass
import net.corda.gradle.jarfilter.matcher.isConstructor
import org.gradle.api.logging.Logger
import org.hamcrest.core.IsCollectionContaining.*
import org.junit.Assert.*
import org.junit.Test
class MetaFixAnnotationTest {
companion object {
private val logger: Logger = StdOutLogging(MetaFixAnnotationTest::class)
private val defaultCon = isConstructor(
returnType = SimpleAnnotation::class
)
private val valueCon = isConstructor(
returnType = AnnotationWithValue::class,
parameters = *arrayOf(String::class)
)
}
@Test
fun testSimpleAnnotation() {
val sourceClass = SimpleAnnotation::class.java
assertThat("<init>() not found", sourceClass.kotlin.constructors, hasItem(defaultCon))
// Rewrite the metadata according to the contents of the bytecode.
val fixedClass = sourceClass.bytecode.fixMetadata(logger, pathsOf(SimpleAnnotation::class))
.toClass<SimpleAnnotation, Any>()
assertThat("<init>() not found", fixedClass.kotlin.constructors, hasItem(defaultCon))
}
@Test
fun testAnnotationWithValue() {
val sourceClass = AnnotationWithValue::class.java
assertThat("<init>(String) not found", sourceClass.kotlin.constructors, hasItem(valueCon))
// Rewrite the metadata according to the contents of the bytecode.
val fixedClass = sourceClass.bytecode.fixMetadata(logger, pathsOf(AnnotationWithValue::class))
.toClass<AnnotationWithValue, Any>()
assertThat("<init>(String) not found", fixedClass.kotlin.constructors, hasItem(valueCon))
}
}
annotation class AnnotationWithValue(val str: String)
annotation class SimpleAnnotation

View File

@ -0,0 +1,79 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.*
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.BuildTask
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class MetaFixConfigurationTests {
@Rule
@JvmField
val testProjectDir = TemporaryFolder()
private lateinit var output: String
@Before
fun setup() {
testProjectDir.installResource("gradle.properties")
}
@Test
fun checkNoJarMeansNoSource() {
val result = gradleProject("""
plugins {
id 'java'
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.MetaFixerTask
task metafix(type: MetaFixerTask)
""").build()
output = result.output
println(output)
val metafix = result.forTask("metafix")
assertEquals(NO_SOURCE, metafix.outcome)
}
@Test
fun checkWithMissingJar() {
val result = gradleProject("""
plugins {
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.MetaFixerTask
task metafix(type: MetaFixerTask) {
jars = file('does-not-exist.jar')
}
""").buildAndFail()
output = result.output
println(output)
assertThat(output).containsSubsequence(
"Caused by: org.gradle.api.GradleException:",
"Caused by: java.io.FileNotFoundException:"
)
val metafix = result.forTask("metafix")
assertEquals(FAILED, metafix.outcome)
}
private fun gradleProject(script: String): GradleRunner {
testProjectDir.newFile("build.gradle").writeText(script)
return GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments(getBasicArgsForTasks("metafix", "--stacktrace"))
.withPluginClasspath()
}
private fun BuildResult.forTask(name: String): BuildTask {
return task(":$name") ?: throw AssertionError("No outcome for $name task")
}
}

View File

@ -0,0 +1,55 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.*
import net.corda.gradle.jarfilter.asm.*
import net.corda.gradle.jarfilter.matcher.*
import org.gradle.api.logging.Logger
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsNot.not
import org.junit.Assert.*
import org.junit.Test
import kotlin.jvm.kotlin
class MetaFixConstructorTest {
companion object {
private val logger: Logger = StdOutLogging(MetaFixConstructorTest::class)
private val unwantedCon = isConstructor(
returnType = WithConstructor::class,
parameters = *arrayOf(Int::class, Long::class)
)
private val wantedCon = isConstructor(
returnType = WithConstructor::class,
parameters = *arrayOf(Long::class)
)
}
@Test
fun testConstructorRemovedFromMetadata() {
val bytecode = recodeMetadataFor<WithConstructor, MetadataTemplate>()
val sourceClass = bytecode.toClass<WithConstructor, HasLong>()
// Check that the unwanted constructor has been successfully
// added to the metadata, and that the class is valid.
val sourceObj = sourceClass.getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER)
assertEquals(BIG_NUMBER, sourceObj.longData())
assertThat("<init>(Int,Long) not found", sourceClass.kotlin.constructors, hasItem(unwantedCon))
assertThat("<init>(Long) not found", sourceClass.kotlin.constructors, hasItem(wantedCon))
// Rewrite the metadata according to the contents of the bytecode.
val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithConstructor::class)).toClass<WithConstructor, HasLong>()
val fixedObj = fixedClass.getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER)
assertEquals(BIG_NUMBER, fixedObj.longData())
assertThat("<init>(Int,Long) still exists", fixedClass.kotlin.constructors, not(hasItem(unwantedCon)))
assertThat("<init>(Long) not found", fixedClass.kotlin.constructors, hasItem(wantedCon))
}
class MetadataTemplate(private val longData: Long) : HasLong {
@Suppress("UNUSED_PARAMETER", "UNUSED")
constructor(intData: Int, longData: Long) : this(longData)
override fun longData(): Long = longData
}
}
class WithConstructor(private val longData: Long) : HasLong {
override fun longData(): Long = longData
}

View File

@ -0,0 +1,53 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.*
import net.corda.gradle.jarfilter.asm.*
import net.corda.gradle.jarfilter.matcher.*
import org.gradle.api.logging.Logger
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsNot.not
import org.junit.Assert.*
import org.junit.Test
import kotlin.jvm.kotlin
import kotlin.reflect.full.declaredFunctions
class MetaFixFunctionTest {
companion object {
private val logger: Logger = StdOutLogging(MetaFixFunctionTest::class)
private val longData = isFunction("longData", Long::class)
private val unwantedFun = isFunction(
name = "unwantedFun",
returnType = String::class,
parameters = *arrayOf(String::class)
)
}
@Test
fun testFunctionRemovedFromMetadata() {
val bytecode = recodeMetadataFor<WithFunction, MetadataTemplate>()
val sourceClass = bytecode.toClass<WithFunction, HasLong>()
// Check that the unwanted function has been successfully
// added to the metadata, and that the class is valid.
val sourceObj = sourceClass.newInstance()
assertEquals(BIG_NUMBER, sourceObj.longData())
assertThat("unwantedFun(String) not found", sourceClass.kotlin.declaredFunctions, hasItem(unwantedFun))
assertThat("longData not found", sourceClass.kotlin.declaredFunctions, hasItem(longData))
// Rewrite the metadata according to the contents of the bytecode.
val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithFunction::class)).toClass<WithFunction, HasLong>()
val fixedObj = fixedClass.newInstance()
assertEquals(BIG_NUMBER, fixedObj.longData())
assertThat("unwantedFun(String) still exists", fixedClass.kotlin.declaredFunctions, not(hasItem(unwantedFun)))
assertThat("longData not found", fixedClass.kotlin.declaredFunctions, hasItem(longData))
}
class MetadataTemplate : HasLong {
override fun longData(): Long = 0
@Suppress("UNUSED") fun unwantedFun(str: String): String = "UNWANTED[$str]"
}
}
class WithFunction : HasLong {
override fun longData(): Long = BIG_NUMBER
}

View File

@ -0,0 +1,57 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.asm.*
import org.assertj.core.api.Assertions.*
import org.gradle.api.logging.Logger
import org.junit.Test
import kotlin.reflect.jvm.jvmName
/**
* Kotlin reflection will attempt to validate the nested classes stored in the [kotlin.Metadata]
* annotation rather than just reporting what is there, which means that it can tell us nothing
* about what the MetaFixer task has done.
*/
class MetaFixNestedClassTest {
companion object {
private val logger: Logger = StdOutLogging(MetaFixNestedClassTest::class)
private val WANTED_CLASS: String = WithNestedClass.Wanted::class.jvmName
private val UNWANTED_CLASS: String = "${WithNestedClass::class.jvmName}\$Unwanted"
}
@Test
fun testNestedClassRemovedFromMetadata() {
val bytecode = recodeMetadataFor<WithNestedClass, MetadataTemplate>()
val sourceClass = bytecode.toClass<WithNestedClass, Any>()
assertThat(sourceClass.classMetadata.nestedClasses).containsExactlyInAnyOrder(WANTED_CLASS, UNWANTED_CLASS)
// Rewrite the metadata according to the contents of the bytecode.
val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithNestedClass::class, WithNestedClass.Wanted::class))
.toClass<WithNestedClass, Any>()
assertThat(fixedClass.classMetadata.nestedClasses).containsExactly(WANTED_CLASS)
}
@Test
fun testAllNestedClassesRemovedFromMetadata() {
val bytecode = recodeMetadataFor<WithoutNestedClass, MetadataTemplate>()
val sourceClass = bytecode.toClass<WithoutNestedClass, Any>()
assertThat(sourceClass.classMetadata.nestedClasses)
.containsExactlyInAnyOrder("${WithoutNestedClass::class.jvmName}\$Wanted", "${WithoutNestedClass::class.jvmName}\$Unwanted")
// Rewrite the metadata according to the contents of the bytecode.
val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithoutNestedClass::class))
.toClass<WithoutNestedClass, Any>()
assertThat(fixedClass.classMetadata.nestedClasses).isEmpty()
}
@Suppress("UNUSED")
class MetadataTemplate {
class Wanted
class Unwanted
}
}
class WithNestedClass {
class Wanted
}
class WithoutNestedClass

View File

@ -0,0 +1,66 @@
@file:JvmName("PackageTemplate")
@file:Suppress("UNUSED")
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.asm.*
import net.corda.gradle.jarfilter.matcher.*
import org.gradle.api.logging.Logger
import org.junit.BeforeClass
import org.junit.Test
import kotlin.jvm.kotlin
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.declaredMembers
import kotlin.test.assertFailsWith
/**
* These tests cannot actually "test" anything until Kotlin reflection
* supports package metadata. Until then, we can only execute the code
* paths to ensure they don't throw any exceptions.
*/
class MetaFixPackageTest {
companion object {
private const val TEMPLATE_CLASS = "net.corda.gradle.jarfilter.PackageTemplate"
private const val EMPTY_CLASS = "net.corda.gradle.jarfilter.EmptyPackage"
private val logger: Logger = StdOutLogging(MetaFixPackageTest::class)
private val staticVal = isProperty("templateVal", Long::class)
private val staticVar = isProperty("templateVar", Int::class)
private val staticFun = isFunction("templateFun", String::class)
private lateinit var sourceClass: Class<out Any>
private lateinit var fixedClass: Class<out Any>
@BeforeClass
@JvmStatic
fun setup() {
val emptyClass = Class.forName(EMPTY_CLASS)
val bytecode = emptyClass.metadataAs(Class.forName(TEMPLATE_CLASS))
sourceClass = bytecode.toClass(emptyClass, Any::class.java)
fixedClass = bytecode.fixMetadata(logger, setOf(EMPTY_CLASS)).toClass(sourceClass, Any::class.java)
}
}
@Test
fun testPackageFunction() {
assertFailsWith<UnsupportedOperationException> { sourceClass.kotlin.declaredFunctions }
//assertThat("templateFun() not found", sourceClass.kotlin.declaredFunctions, hasItem(staticFun))
//assertThat("templateFun() still exists", fixedClass.kotlin.declaredFunctions, not(hasItem(staticFun)))
}
@Test
fun testPackageVal() {
assertFailsWith<UnsupportedOperationException> { sourceClass.kotlin.declaredMembers }
//assertThat("templateVal not found", sourceClass.kotlin.declaredMembers, hasItem(staticVal))
//assertThat("templateVal still exists", fixedClass.kotlin.declaredMembers, not(hasItem(staticVal)))
}
@Test
fun testPackageVar() {
assertFailsWith<UnsupportedOperationException> { sourceClass.kotlin.declaredMembers }
//assertThat("templateVar not found", sourceClass.kotlin.declaredMembers, hasItem(staticVar))
//assertThat("templateVar still exists", fixedClass.kotlin.declaredMembers, not(hasItem(staticVar)))
}
}
internal fun templateFun(): String = MESSAGE
internal const val templateVal: Long = BIG_NUMBER
internal var templateVar: Int = NUMBER

View File

@ -0,0 +1,57 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.*
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Assert.*
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.io.FileNotFoundException
import java.nio.file.Path
@Suppress("UNUSED")
class MetaFixProject(private val projectDir: TemporaryFolder, private val name: String) : TestRule {
private var _sourceJar: Path? = null
val sourceJar: Path get() = _sourceJar ?: throw FileNotFoundException("Input not found")
private var _metafixedJar: Path? = null
val metafixedJar: Path get() = _metafixedJar ?: throw FileNotFoundException("Output not found")
private var _output: String = ""
val output: String get() = _output
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
projectDir.installResources(
"$name/build.gradle",
"repositories.gradle",
"gradle.properties",
"settings.gradle"
)
val result = GradleRunner.create()
.withProjectDir(projectDir.root)
.withArguments(getGradleArgsForTasks("metafix"))
.withPluginClasspath()
.build()
_output = result.output
println(output)
val metafix = result.task(":metafix")
?: throw AssertionError("No outcome for metafix task")
assertEquals(SUCCESS, metafix.outcome)
_sourceJar = projectDir.pathOf("build", "libs", "$name.jar")
assertThat(sourceJar).isRegularFile()
_metafixedJar = projectDir.pathOf("build", "metafixer-libs", "$name-metafixed.jar")
assertThat(metafixedJar).isRegularFile()
base.evaluate()
}
}
}
}

View File

@ -0,0 +1,37 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.asm.*
import org.assertj.core.api.Assertions.*
import org.gradle.api.logging.Logger
import org.junit.Test
import kotlin.reflect.jvm.jvmName
class MetaFixSealedClassTest {
companion object {
private val logger: Logger = StdOutLogging(MetaFixSealedClassTest::class)
private val UNWANTED_CLASS: String = "${MetaSealedClass::class.jvmName}\$Unwanted"
private val WANTED_CLASS: String = MetaSealedClass.Wanted::class.jvmName
}
@Test
fun testSealedSubclassRemovedFromMetadata() {
val bytecode = recodeMetadataFor<MetaSealedClass, MetadataTemplate>()
val sourceClass = bytecode.toClass<MetaSealedClass, Any>()
assertThat(sourceClass.classMetadata.sealedSubclasses).containsExactlyInAnyOrder(UNWANTED_CLASS, WANTED_CLASS)
// Rewrite the metadata according to the contents of the bytecode.
val fixedClass = bytecode.fixMetadata(logger, pathsOf(MetaSealedClass::class, MetaSealedClass.Wanted::class))
.toClass<MetaSealedClass, Any>()
assertThat(fixedClass.classMetadata.sealedSubclasses).containsExactly(WANTED_CLASS)
}
@Suppress("UNUSED")
sealed class MetadataTemplate {
class Wanted : MetadataTemplate()
class Unwanted : MetadataTemplate()
}
}
sealed class MetaSealedClass {
class Wanted : MetaSealedClass()
}

View File

@ -0,0 +1,108 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.*
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import org.junit.runners.model.Statement
import java.nio.file.Path
import java.nio.file.attribute.FileTime
import java.util.*
import java.util.Calendar.FEBRUARY
import java.util.zip.ZipEntry
import java.util.zip.ZipEntry.*
import java.util.zip.ZipFile
class MetaFixTimestampTest {
companion object {
private val testProjectDir = TemporaryFolder()
private val sourceJar = DummyJar(testProjectDir, MetaFixTimestampTest::class.java, "timestamps")
private val CONSTANT_TIME: FileTime = FileTime.fromMillis(
GregorianCalendar(1980, FEBRUARY, 1).apply {
timeZone = TimeZone.getTimeZone("UTC")
}.timeInMillis
)
private lateinit var metafixedJar: Path
private lateinit var output: String
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(sourceJar)
.around(createTestProject())
private fun createTestProject() = TestRule { base, _ ->
object : Statement() {
override fun evaluate() {
testProjectDir.installResource("gradle.properties")
testProjectDir.newFile("build.gradle").writeText("""
plugins {
id 'net.corda.plugins.jar-filter'
}
import net.corda.gradle.jarfilter.MetaFixerTask
task metafix(type: MetaFixerTask) {
jars file("${sourceJar.path.toUri()}")
preserveTimestamps = false
}
""")
val result = GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments(getGradleArgsForTasks("metafix"))
.withPluginClasspath()
.build()
output = result.output
println(output)
val metafix = result.task(":metafix")
?: throw AssertionError("No outcome for metafix task")
assertEquals(SUCCESS, metafix.outcome)
metafixedJar = testProjectDir.pathOf("build", "metafixer-libs", "timestamps-metafixed.jar")
assertThat(metafixedJar).isRegularFile()
base.evaluate()
}
}
}
private val ZipEntry.methodName: String get() = if (method == STORED) "Stored" else "Deflated"
}
@Test
fun fileTimestampsAreRemoved() {
var directoryCount = 0
var classCount = 0
var otherCount = 0
ZipFile(metafixedJar.toFile()).use { jar ->
for (entry in jar.entries()) {
println("Entry: ${entry.name}")
println("- ${entry.methodName} (${entry.size} size / ${entry.compressedSize} compressed) bytes")
assertThat(entry.lastModifiedTime).isEqualTo(CONSTANT_TIME)
assertThat(entry.lastAccessTime).isNull()
assertThat(entry.creationTime).isNull()
if (entry.isDirectory) {
++directoryCount
} else if (entry.name.endsWith(".class")) {
++classCount
} else {
++otherCount
}
}
}
assertThat(directoryCount).isGreaterThan(0)
assertThat(classCount).isGreaterThan(0)
assertThat(otherCount).isGreaterThan(0)
}
}

View File

@ -0,0 +1,49 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.*
import net.corda.gradle.jarfilter.asm.*
import net.corda.gradle.jarfilter.matcher.*
import org.gradle.api.logging.Logger
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsNot.not
import org.junit.Assert.*
import org.junit.Test
import kotlin.jvm.kotlin
import kotlin.reflect.full.declaredMemberProperties
class MetaFixValPropertyTest {
companion object {
private val logger: Logger = StdOutLogging(MetaFixValPropertyTest::class)
private val unwantedVal = isProperty("unwantedVal", String::class)
private val intVal = isProperty("intVal", Int::class)
}
@Test
fun testPropertyRemovedFromMetadata() {
val bytecode = recodeMetadataFor<WithValProperty, MetadataTemplate>()
val sourceClass = bytecode.toClass<WithValProperty, HasIntVal>()
// Check that the unwanted property has been successfully
// added to the metadata, and that the class is valid.
val sourceObj = sourceClass.newInstance()
assertEquals(NUMBER, sourceObj.intVal)
assertThat("unwantedVal not found", sourceClass.kotlin.declaredMemberProperties, hasItem(unwantedVal))
assertThat("intVal not found", sourceClass.kotlin.declaredMemberProperties, hasItem(intVal))
// Rewrite the metadata according to the contents of the bytecode.
val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithValProperty::class)).toClass<WithValProperty, HasIntVal>()
val fixedObj = fixedClass.newInstance()
assertEquals(NUMBER, fixedObj.intVal)
assertThat("unwantedVal still exists", fixedClass.kotlin.declaredMemberProperties, not(hasItem(unwantedVal)))
assertThat("intVal not found", fixedClass.kotlin.declaredMemberProperties, hasItem(intVal))
}
class MetadataTemplate : HasIntVal {
override val intVal: Int = 0
@Suppress("UNUSED") val unwantedVal: String = "UNWANTED"
}
}
class WithValProperty : HasIntVal {
override val intVal: Int = NUMBER
}

View File

@ -0,0 +1,49 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.*
import net.corda.gradle.jarfilter.asm.*
import net.corda.gradle.jarfilter.matcher.*
import org.gradle.api.logging.Logger
import org.hamcrest.core.IsCollectionContaining.hasItem
import org.hamcrest.core.IsNot.not
import org.junit.Assert.*
import org.junit.Test
import kotlin.jvm.kotlin
import kotlin.reflect.full.declaredMemberProperties
class MetaFixVarPropertyTest {
companion object {
private val logger: Logger = StdOutLogging(MetaFixVarPropertyTest::class)
private val unwantedVar = isProperty("unwantedVar", String::class)
private val intVar = isProperty("intVar", Int::class)
}
@Test
fun testPropertyRemovedFromMetadata() {
val bytecode = recodeMetadataFor<WithVarProperty, MetadataTemplate>()
val sourceClass = bytecode.toClass<WithVarProperty, HasIntVar>()
// Check that the unwanted property has been successfully
// added to the metadata, and that the class is valid.
val sourceObj = sourceClass.newInstance()
assertEquals(NUMBER, sourceObj.intVar)
assertThat("unwantedVar not found", sourceClass.kotlin.declaredMemberProperties, hasItem(unwantedVar))
assertThat("intVar not found", sourceClass.kotlin.declaredMemberProperties, hasItem(intVar))
// Rewrite the metadata according to the contents of the bytecode.
val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithVarProperty::class)).toClass<WithVarProperty, HasIntVar>()
val fixedObj = fixedClass.newInstance()
assertEquals(NUMBER, fixedObj.intVar)
assertThat("unwantedVar still exists", fixedClass.kotlin.declaredMemberProperties, not(hasItem(unwantedVar)))
assertThat("intVar not found", fixedClass.kotlin.declaredMemberProperties, hasItem(intVar))
}
class MetadataTemplate : HasIntVar {
override var intVar: Int = 0
@Suppress("UNUSED") var unwantedVar: String = "UNWANTED"
}
}
class WithVarProperty : HasIntVar {
override var intVar: Int = NUMBER
}

View File

@ -0,0 +1,88 @@
package net.corda.gradle.jarfilter
import org.junit.Assert.*
import org.junit.Test
import org.objectweb.asm.Opcodes.*
class MethodElementTest {
private companion object {
private const val DESCRIPTOR = "()Ljava.lang.String;"
}
@Test
fun testMethodsMatchByNameAndDescriptor() {
val elt = MethodElement(
name = "getThing",
descriptor = DESCRIPTOR,
access = ACC_PUBLIC or ACC_ABSTRACT or ACC_FINAL
)
assertEquals(MethodElement(name="getThing", descriptor=DESCRIPTOR), elt)
assertNotEquals(MethodElement(name="getOther", descriptor=DESCRIPTOR), elt)
assertNotEquals(MethodElement(name="getThing", descriptor="()J"), elt)
}
@Test
fun testBasicMethodVisibleName() {
val elt = MethodElement(
name = "getThing",
descriptor = DESCRIPTOR,
access = ACC_PUBLIC
)
assertEquals("getThing", elt.visibleName)
}
@Test
fun testMethodVisibleNameWithSuffix() {
val elt = MethodElement(
name = "getThing\$extra",
descriptor = DESCRIPTOR,
access = ACC_PUBLIC
)
assertEquals("getThing", elt.visibleName)
}
@Test
fun testSyntheticMethodSuffix() {
val elt = MethodElement(
name = "getThing\$extra",
descriptor = DESCRIPTOR,
access = ACC_PUBLIC or ACC_SYNTHETIC
)
assertTrue(elt.isKotlinSynthetic("extra"))
assertFalse(elt.isKotlinSynthetic("something"))
assertTrue(elt.isKotlinSynthetic("extra", "something"))
}
@Test
fun testPublicMethodSuffix() {
val elt = MethodElement(
name = "getThing\$extra",
descriptor = DESCRIPTOR,
access = ACC_PUBLIC
)
assertFalse(elt.isKotlinSynthetic("extra"))
}
@Test
fun testMethodDoesNotExpire() {
val elt = MethodElement(
name = "getThing\$extra",
descriptor = DESCRIPTOR,
access = ACC_PUBLIC
)
assertFalse(elt.isExpired)
assertFalse(elt.isExpired)
assertFalse(elt.isExpired)
}
@Test
fun testArtificialMethodDoesExpire() {
val elt = MethodElement(
name = "getThing\$extra",
descriptor = DESCRIPTOR
)
assertFalse(elt.isExpired)
assertTrue(elt.isExpired)
assertTrue(elt.isExpired)
}
}

View File

@ -0,0 +1,176 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.HasUnwantedFun
import net.corda.gradle.unwanted.HasUnwantedVal
import net.corda.gradle.unwanted.HasUnwantedVar
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
class RemoveAnnotationsTest {
companion object {
private const val ANNOTATED_CLASS = "net.corda.gradle.HasUnwantedAnnotations"
private const val REMOVE_ME_CLASS = "net.corda.gradle.jarfilter.RemoveMe"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "remove-annotations")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteFromClass() {
classLoaderFor(testProject.sourceJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<Any>(ANNOTATED_CLASS).apply {
assertNotNull(getAnnotation(removeMe))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<Any>(ANNOTATED_CLASS).apply {
assertNull(getAnnotation(removeMe))
}
}
}
@Test
fun deleteFromDefaultConstructor() {
classLoaderFor(testProject.sourceJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<Any>(ANNOTATED_CLASS).apply {
getDeclaredConstructor().also { con ->
assertNotNull(con.getAnnotation(removeMe))
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<Any>(ANNOTATED_CLASS).apply {
getDeclaredConstructor().also { con ->
assertNull(con.getAnnotation(removeMe))
}
}
}
}
@Test
fun deleteFromPrimaryConstructor() {
classLoaderFor(testProject.sourceJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<Any>(ANNOTATED_CLASS).apply {
getDeclaredConstructor(Long::class.java, String::class.java).also { con ->
assertNotNull(con.getAnnotation(removeMe))
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<Any>(ANNOTATED_CLASS).apply {
getDeclaredConstructor(Long::class.java, String::class.java).also { con ->
assertNull(con.getAnnotation(removeMe))
}
}
}
}
@Test
fun deleteFromField() {
classLoaderFor(testProject.sourceJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<Any>(ANNOTATED_CLASS).apply {
getField("longField").also { field ->
assertNotNull(field.getAnnotation(removeMe))
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<Any>(ANNOTATED_CLASS).apply {
getField("longField").also { field ->
assertNull(field.getAnnotation(removeMe))
}
}
}
}
@Test
fun deleteFromMethod() {
classLoaderFor(testProject.sourceJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<HasUnwantedFun>(ANNOTATED_CLASS).apply {
getMethod("unwantedFun", String::class.java).also { method ->
assertNotNull(method.getAnnotation(removeMe))
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<HasUnwantedFun>(ANNOTATED_CLASS).apply {
getMethod("unwantedFun", String::class.java).also { method ->
assertNull(method.getAnnotation(removeMe))
}
}
}
}
@Test
fun deleteFromValProperty() {
classLoaderFor(testProject.sourceJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<HasUnwantedVal>(ANNOTATED_CLASS).apply {
getMethod("getUnwantedVal").also { method ->
assertNotNull(method.getAnnotation(removeMe))
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<HasUnwantedVal>(ANNOTATED_CLASS).apply {
getMethod("getUnwantedVal").also { method ->
assertNull(method.getAnnotation(removeMe))
}
}
}
}
@Test
fun deleteFromVarProperty() {
classLoaderFor(testProject.sourceJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<HasUnwantedVar>(ANNOTATED_CLASS).apply {
getMethod("getUnwantedVar").also { method ->
assertNotNull(method.getAnnotation(removeMe))
}
getMethod("setUnwantedVar", String::class.java).also { method ->
assertNotNull(method.getAnnotation(removeMe))
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
val removeMe = cl.load<Annotation>(REMOVE_ME_CLASS)
cl.load<HasUnwantedVar>(ANNOTATED_CLASS).apply {
getMethod("getUnwantedVar").also { method ->
assertNull(method.getAnnotation(removeMe))
}
getMethod("setUnwantedVar", String::class.java).also { method ->
assertNull(method.getAnnotation(removeMe))
}
}
}
}
}

View File

@ -0,0 +1,102 @@
@file:JvmName("StaticFields")
@file:Suppress("UNUSED")
package net.corda.gradle.jarfilter
import net.corda.gradle.jarfilter.annotations.Deletable
import net.corda.gradle.jarfilter.asm.bytecode
import net.corda.gradle.jarfilter.asm.toClass
import org.gradle.api.logging.Logger
import org.junit.Assert.*
import org.junit.BeforeClass
import org.junit.Test
import org.objectweb.asm.ClassWriter.COMPUTE_MAXS
import kotlin.reflect.jvm.jvmName
import kotlin.test.assertFailsWith
/**
* Static properties are all initialised in the same <clinit> block.
* Show that deleting some field references doesn't break the other
* properties' initialisation code.
*/
class StaticFieldRemovalTest {
companion object {
private val logger: Logger = StdOutLogging(StaticFieldRemovalTest::class)
private const val FIELD_CLASS = "net.corda.gradle.jarfilter.StaticFields"
private lateinit var sourceClass: Class<out Any>
private lateinit var targetClass: Class<out Any>
private fun <T : R, R : Any> transform(type: Class<in T>, asType: Class<out R>): Class<out R> {
val bytecode = type.bytecode.execute({ writer ->
ClassTransformer(
visitor = writer,
logger = logger,
removeAnnotations = emptySet(),
deleteAnnotations = setOf(Deletable::class.jvmName.descriptor),
stubAnnotations = emptySet(),
unwantedClasses = mutableSetOf()
)
}, COMPUTE_MAXS)
return bytecode.toClass(type, asType)
}
@JvmStatic
@BeforeClass
fun setup() {
sourceClass = Class.forName(FIELD_CLASS)
targetClass = transform(sourceClass, Any::class.java)
}
}
@Test
fun deleteStaticString() {
assertEquals("1", sourceClass.getDeclaredMethod("getStaticString").invoke(null))
assertFailsWith<NoSuchMethodException> { targetClass.getDeclaredMethod("getStaticString") }
}
@Test
fun deleteStaticLong() {
assertEquals(2L, sourceClass.getDeclaredMethod("getStaticLong").invoke(null))
assertFailsWith<NoSuchMethodException> { targetClass.getDeclaredMethod("getStaticLong") }
}
@Test
fun deleteStaticInt() {
assertEquals(3, sourceClass.getDeclaredMethod("getStaticInt").invoke(null))
assertFailsWith<NoSuchMethodException> { targetClass.getDeclaredMethod("getStaticInt") }
}
@Test
fun deleteStaticShort() {
assertEquals(4.toShort(), sourceClass.getDeclaredMethod("getStaticShort").invoke(null))
assertFailsWith<NoSuchMethodException> { targetClass.getDeclaredMethod("getStaticShort") }
}
@Test
fun deleteStaticByte() {
assertEquals(5.toByte(), sourceClass.getDeclaredMethod("getStaticByte").invoke(null))
assertFailsWith<NoSuchMethodException> { targetClass.getDeclaredMethod("getStaticByte") }
}
@Test
fun deleteStaticChar() {
assertEquals(6.toChar(), sourceClass.getDeclaredMethod("getStaticChar").invoke(null))
assertFailsWith<NoSuchMethodException> { targetClass.getDeclaredMethod("getStaticChar") }
}
@Test
fun checkSeedHasBeenIncremented() {
assertEquals(6, sourceClass.getDeclaredMethod("getStaticSeed").invoke(null))
assertEquals(6, targetClass.getDeclaredMethod("getStaticSeed").invoke(null))
}
}
private var seed: Int = 0
val staticSeed get() = seed
@Deletable val staticString: String = (++seed).toString()
@Deletable val staticLong: Long = (++seed).toLong()
@Deletable val staticInt: Int = ++seed
@Deletable val staticShort: Short = (++seed).toShort()
@Deletable val staticByte: Byte = (++seed).toByte()
@Deletable val staticChar: Char = (++seed).toChar()

View File

@ -0,0 +1,262 @@
package net.corda.gradle.jarfilter
import org.gradle.api.logging.LogLevel.*
import org.gradle.api.logging.LogLevel
import org.gradle.api.logging.Logger
import org.slf4j.Marker
import org.slf4j.helpers.MessageFormatter
import kotlin.reflect.KClass
class StdOutLogging(private val name: String, private val threshold: LogLevel = INFO) : Logger {
constructor(clazz: KClass<*>) : this(clazz.java.simpleName)
override fun getName(): String = name
override fun isErrorEnabled(): Boolean = isEnabled(ERROR)
override fun isErrorEnabled(marker: Marker): Boolean = isEnabled(ERROR)
override fun isWarnEnabled(): Boolean = isEnabled(WARN)
override fun isWarnEnabled(marker: Marker): Boolean = isEnabled(WARN)
override fun isInfoEnabled(): Boolean = isEnabled(INFO)
override fun isInfoEnabled(marker: Marker): Boolean = isEnabled(INFO)
override fun isDebugEnabled(): Boolean = isEnabled(DEBUG)
override fun isDebugEnabled(marker: Marker): Boolean = isEnabled(DEBUG)
override fun isTraceEnabled(): Boolean = isEnabled(DEBUG)
override fun isTraceEnabled(marker: Marker): Boolean = isEnabled(DEBUG)
override fun isQuietEnabled(): Boolean = isEnabled(QUIET)
override fun isLifecycleEnabled(): Boolean = isEnabled(LIFECYCLE)
override fun isEnabled(level: LogLevel): Boolean = threshold <= level
override fun warn(msg: String) = log(WARN, msg)
override fun warn(msg: String, obj: Any?) = log(WARN, msg, obj)
override fun warn(msg: String, vararg objects: Any?) = log(WARN, msg, *objects)
override fun warn(msg: String, obj1: Any?, obj2: Any?) = log(WARN, msg, obj1, obj2)
override fun warn(msg: String, ex: Throwable) = log(WARN, msg, ex)
override fun warn(marker: Marker, msg: String) {
if (isWarnEnabled(marker)) {
print(WARN, msg)
}
}
override fun warn(marker: Marker, msg: String, obj: Any?) {
if (isWarnEnabled(marker)) {
print(WARN, msg, obj)
}
}
override fun warn(marker: Marker, msg: String, obj1: Any?, obj2: Any?) {
if (isWarnEnabled(marker)) {
print(WARN, msg, obj1, obj2)
}
}
override fun warn(marker: Marker, msg: String, vararg objects: Any?) {
if (isWarnEnabled(marker)) {
printAny(WARN, msg, *objects)
}
}
override fun warn(marker: Marker, msg: String, ex: Throwable) {
if (isWarnEnabled(marker)) {
print(WARN, msg, ex)
}
}
override fun info(message: String, vararg objects: Any?) = log(INFO, message, *objects)
override fun info(message: String) = log(INFO, message)
override fun info(message: String, obj: Any?) = log(INFO, message, obj)
override fun info(message: String, obj1: Any?, obj2: Any?) = log(INFO, message, obj1, obj2)
override fun info(message: String, ex: Throwable) = log(INFO, message, ex)
override fun info(marker: Marker, msg: String) {
if (isInfoEnabled(marker)) {
print(INFO, msg)
}
}
override fun info(marker: Marker, msg: String, obj: Any?) {
if (isInfoEnabled(marker)) {
print(INFO, msg, obj)
}
}
override fun info(marker: Marker, msg: String, obj1: Any?, obj2: Any?) {
if (isInfoEnabled(marker)) {
print(INFO, msg, obj1, obj2)
}
}
override fun info(marker: Marker, msg: String, vararg objects: Any?) {
if (isInfoEnabled(marker)) {
printAny(INFO, msg, *objects)
}
}
override fun info(marker: Marker, msg: String, ex: Throwable) {
if (isInfoEnabled(marker)) {
print(INFO, msg, ex)
}
}
override fun error(message: String) = log(ERROR, message)
override fun error(message: String, obj: Any?) = log(ERROR, message, obj)
override fun error(message: String, obj1: Any?, obj2: Any?) = log(ERROR, message, obj1, obj2)
override fun error(message: String, vararg objects: Any?) = log(ERROR, message, *objects)
override fun error(message: String, ex: Throwable) = log(ERROR, message, ex)
override fun error(marker: Marker, msg: String) {
if (isErrorEnabled(marker)) {
print(ERROR, msg)
}
}
override fun error(marker: Marker, msg: String, obj: Any?) {
if (isErrorEnabled(marker)) {
print(ERROR, msg, obj)
}
}
override fun error(marker: Marker, msg: String, obj1: Any?, obj2: Any?) {
if (isErrorEnabled(marker)) {
print(ERROR, msg, obj1, obj2)
}
}
override fun error(marker: Marker, msg: String, vararg objects: Any?) {
if (isErrorEnabled(marker)) {
printAny(ERROR, msg, *objects)
}
}
override fun error(marker: Marker, msg: String, ex: Throwable) {
if (isErrorEnabled(marker)) {
print(ERROR, msg, ex)
}
}
override fun log(level: LogLevel, message: String) {
if (isEnabled(level)) {
print(level, message)
}
}
override fun log(level: LogLevel, message: String, vararg objects: Any?) {
if (isEnabled(level)) {
printAny(level, message, *objects)
}
}
override fun log(level: LogLevel, message: String, ex: Throwable) {
if (isEnabled(level)) {
print(level, message, ex)
}
}
override fun debug(message: String, vararg objects: Any?) = log(DEBUG, message, *objects)
override fun debug(message: String) = log(DEBUG, message)
override fun debug(message: String, obj: Any?) = log(DEBUG, message, obj)
override fun debug(message: String, obj1: Any?, obj2: Any?) = log(DEBUG, message, obj1, obj2)
override fun debug(message: String, ex: Throwable) = log(DEBUG, message, ex)
override fun debug(marker: Marker, msg: String) {
if (isDebugEnabled(marker)) {
print(DEBUG, msg)
}
}
override fun debug(marker: Marker, msg: String, obj: Any?) {
if (isDebugEnabled(marker)) {
print(DEBUG, msg, obj)
}
}
override fun debug(marker: Marker, msg: String, obj1: Any?, obj2: Any?) {
if (isDebugEnabled(marker)) {
print(DEBUG, msg, obj1, obj2)
}
}
override fun debug(marker: Marker, msg: String, vararg objects: Any?) {
if (isDebugEnabled(marker)) {
printAny(DEBUG, msg, *objects)
}
}
override fun debug(marker: Marker, msg: String, ex: Throwable) {
if (isDebugEnabled(marker)) {
print(DEBUG, msg, ex)
}
}
override fun lifecycle(message: String) = log(LIFECYCLE, message)
override fun lifecycle(message: String, vararg objects: Any?) = log(LIFECYCLE, message, *objects)
override fun lifecycle(message: String, ex: Throwable) = log(LIFECYCLE, message, ex)
override fun quiet(message: String) = log(QUIET, message)
override fun quiet(message: String, vararg objects: Any?) = log(QUIET, message, *objects)
override fun quiet(message: String, ex: Throwable) = log(QUIET, message, ex)
override fun trace(message: String) = debug(message)
override fun trace(message: String, obj: Any?) = debug(message, obj)
override fun trace(message: String, obj1: Any?, obj2: Any?) = debug(message, obj1, obj2)
override fun trace(message: String, vararg objects: Any?) = debug(message, *objects)
override fun trace(message: String, ex: Throwable) = debug(message, ex)
override fun trace(marker: Marker, msg: String) {
if (isTraceEnabled(marker)) {
print(DEBUG, msg)
}
}
override fun trace(marker: Marker, msg: String, obj: Any?) {
if (isTraceEnabled(marker)) {
print(DEBUG, msg, obj)
}
}
override fun trace(marker: Marker, msg: String, obj1: Any?, obj2: Any?) {
if (isTraceEnabled(marker)) {
print(DEBUG, msg, obj1, obj2)
}
}
override fun trace(marker: Marker, msg: String, vararg objects: Any?) {
if (isTraceEnabled(marker)) {
printAny(DEBUG, msg, *objects)
}
}
override fun trace(marker: Marker, msg: String, ex: Throwable) {
if (isTraceEnabled) {
print(DEBUG, msg, ex)
}
}
private fun print(level: LogLevel, message: String) {
println("$name - $level: $message")
}
private fun print(level: LogLevel, message: String, ex: Throwable) {
print(level, message)
ex.printStackTrace(System.out)
}
private fun print(level: LogLevel, message: String, obj: Any?) {
print(level, MessageFormatter.format(message, obj).message)
}
private fun print(level: LogLevel, message: String, obj1: Any?, obj2: Any?) {
print(level, MessageFormatter.format(message, obj1, obj2).message)
}
private fun printAny(level: LogLevel, message: String, vararg objects: Any?) {
print(level, MessageFormatter.arrayFormat(message, objects).message)
}
}

View File

@ -0,0 +1,160 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.HasAll
import net.corda.gradle.unwanted.HasInt
import net.corda.gradle.unwanted.HasLong
import net.corda.gradle.unwanted.HasString
import org.assertj.core.api.Assertions.*
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import java.lang.reflect.InvocationTargetException
import kotlin.test.assertFailsWith
class StubConstructorTest {
companion object {
private const val STRING_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryStringConstructorToStub"
private const val LONG_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryLongConstructorToStub"
private const val INT_PRIMARY_CONSTRUCTOR_CLASS = "net.corda.gradle.PrimaryIntConstructorToStub"
private const val SECONDARY_CONSTRUCTOR_CLASS = "net.corda.gradle.HasConstructorToStub"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "stub-constructor")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun stubConstructorWithLongParameter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasLong>(SECONDARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also { obj ->
assertEquals(BIG_NUMBER, obj.longData())
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasLong>(SECONDARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Long::class.java).also {
assertFailsWith<InvocationTargetException> { it.newInstance(BIG_NUMBER) }.targetException.also { ex ->
assertThat(ex)
.isInstanceOf(UnsupportedOperationException::class.java)
.hasMessage("Method has been deleted")
}
}
}
}
}
@Test
fun stubConstructorWithStringParameter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasString>(SECONDARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.stringData())
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasString>(SECONDARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(String::class.java).also {
assertFailsWith<InvocationTargetException> { it.newInstance(MESSAGE) }.targetException.also { ex ->
assertThat(ex)
.isInstanceOf(UnsupportedOperationException::class.java)
.hasMessage("Method has been deleted")
}
}
}
}
}
@Test
fun showUnannotatedConstructorIsUnaffected() {
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasAll>(SECONDARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Int::class.java).newInstance(NUMBER).also { obj ->
assertEquals(NUMBER, obj.intData())
assertEquals(NUMBER.toLong(), obj.longData())
assertEquals("<nothing>", obj.stringData())
}
}
}
}
@Test
fun stubPrimaryConstructorWithStringParameter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasString>(STRING_PRIMARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.stringData())
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasString>(STRING_PRIMARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(String::class.java).also {
assertFailsWith<InvocationTargetException> { it.newInstance(MESSAGE) }.targetException.also { ex ->
assertThat(ex)
.isInstanceOf(UnsupportedOperationException::class.java)
.hasMessage("Method has been deleted")
}
}
}
}
}
@Test
fun stubPrimaryConstructorWithLongParameter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasLong>(LONG_PRIMARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER).also { obj ->
assertEquals(BIG_NUMBER, obj.longData())
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasLong>(LONG_PRIMARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Long::class.java).also {
assertFailsWith<InvocationTargetException> { it.newInstance(BIG_NUMBER) }.targetException.also { ex ->
assertThat(ex)
.isInstanceOf(UnsupportedOperationException::class.java)
.hasMessage("Method has been deleted")
}
}
}
}
}
@Test
fun stubPrimaryConstructorWithIntParameter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasInt>(INT_PRIMARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Int::class.java).newInstance(NUMBER).also { obj ->
assertEquals(NUMBER, obj.intData())
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasInt>(INT_PRIMARY_CONSTRUCTOR_CLASS).apply {
getDeclaredConstructor(Int::class.java).apply {
val error = assertFailsWith<InvocationTargetException> { newInstance(NUMBER) }.targetException
assertThat(error)
.isInstanceOf(UnsupportedOperationException::class.java)
.hasMessage("Method has been deleted")
}
}
}
}
}

View File

@ -0,0 +1,74 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.HasUnwantedFun
import org.assertj.core.api.Assertions.*
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import javax.annotation.Resource
import kotlin.test.assertFailsWith
class StubFunctionOutTest {
companion object {
private const val FUNCTION_CLASS = "net.corda.gradle.HasFunctionToStub"
private const val STUB_ME_OUT_ANNOTATION = "net.corda.gradle.jarfilter.StubMeOut"
private const val PARAMETER_ANNOTATION = "net.corda.gradle.Parameter"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "stub-function")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun stubFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
val stubMeOut = cl.load<Annotation>(STUB_ME_OUT_ANNOTATION)
val parameter = cl.load<Annotation>(PARAMETER_ANNOTATION)
cl.load<HasUnwantedFun>(FUNCTION_CLASS).apply {
newInstance().also { obj ->
assertEquals(MESSAGE, obj.unwantedFun(MESSAGE))
}
getMethod("unwantedFun", String::class.java).also { method ->
assertTrue("StubMeOut annotation missing", method.isAnnotationPresent (stubMeOut))
assertTrue("Resource annotation missing", method.isAnnotationPresent(Resource::class.java))
method.parameterAnnotations.also { paramAnns ->
assertEquals(1, paramAnns.size)
assertThat(paramAnns[0])
.hasOnlyOneElementSatisfying { a -> a.javaClass.isInstance(parameter) }
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
val stubMeOut = cl.load<Annotation>(STUB_ME_OUT_ANNOTATION)
val parameter = cl.load<Annotation>(PARAMETER_ANNOTATION)
cl.load<HasUnwantedFun>(FUNCTION_CLASS).apply {
newInstance().also { obj ->
assertFailsWith<UnsupportedOperationException> { obj.unwantedFun(MESSAGE) }.also { ex ->
assertEquals("Method has been deleted", ex.message)
}
}
getMethod("unwantedFun", String::class.java).also { method ->
assertFalse("StubMeOut annotation present", method.isAnnotationPresent(stubMeOut))
assertTrue("Resource annotation missing", method.isAnnotationPresent(Resource::class.java))
method.parameterAnnotations.also { paramAnns ->
assertEquals(1, paramAnns.size)
assertThat(paramAnns[0])
.hasOnlyOneElementSatisfying { a -> a.javaClass.isInstance(parameter) }
}
}
}
}
}
}

View File

@ -0,0 +1,127 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import java.lang.reflect.InvocationTargetException
import kotlin.test.*
class StubStaticFunctionTest {
companion object {
private const val FUNCTION_CLASS = "net.corda.gradle.StaticFunctionsToStub"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "stub-static-function")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun stubStringFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getDeclaredMethod("unwantedStringToStub", String::class.java).also { method ->
method.invoke(null, MESSAGE).also { result ->
assertThat(result)
.isInstanceOf(String::class.java)
.isEqualTo(MESSAGE)
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getDeclaredMethod("unwantedStringToStub", String::class.java).also { method ->
assertFailsWith<InvocationTargetException> { method.invoke(null, MESSAGE) }.targetException.also { ex ->
assertThat(ex)
.isInstanceOf(UnsupportedOperationException::class.java)
.hasMessage("Method has been deleted")
}
}
}
}
}
@Test
fun stubLongFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getDeclaredMethod("unwantedLongToStub", Long::class.java).also { method ->
method.invoke(null, BIG_NUMBER).also { result ->
assertThat(result)
.isInstanceOf(Long::class.javaObjectType)
.isEqualTo(BIG_NUMBER)
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getDeclaredMethod("unwantedLongToStub", Long::class.java).also { method ->
assertFailsWith<InvocationTargetException> { method.invoke(null, BIG_NUMBER) }.targetException.also { ex ->
assertThat(ex)
.isInstanceOf(UnsupportedOperationException::class.java)
.hasMessage("Method has been deleted")
}
}
}
}
}
@Test
fun stubIntFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getDeclaredMethod("unwantedIntToStub", Int::class.java).also { method ->
method.invoke(null, NUMBER).also { result ->
assertThat(result)
.isInstanceOf(Int::class.javaObjectType)
.isEqualTo(NUMBER)
}
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
getDeclaredMethod("unwantedIntToStub", Int::class.java).also { method ->
assertFailsWith<InvocationTargetException> { method.invoke(null, NUMBER) }.targetException.also { ex ->
assertThat(ex)
.isInstanceOf(UnsupportedOperationException::class.java)
.hasMessage("Method has been deleted")
}
}
}
}
}
@Test
fun stubVoidFunction() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
val staticSeed = getDeclaredMethod("getStaticSeed")
assertEquals(0, staticSeed.invoke(null))
getDeclaredMethod("unwantedVoidToStub").invoke(null)
assertEquals(1, staticSeed.invoke(null))
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<Any>(FUNCTION_CLASS).apply {
val staticSeed = getDeclaredMethod("getStaticSeed")
assertEquals(0, staticSeed.invoke(null))
getDeclaredMethod("unwantedVoidToStub").invoke(null)
assertEquals(0, staticSeed.invoke(null))
}
}
}
}

View File

@ -0,0 +1,46 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.HasUnwantedVal
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertFailsWith
class StubValPropertyTest {
companion object {
private const val PROPERTY_CLASS = "net.corda.gradle.HasValPropertyForStub"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "stub-val-property")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteGetter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVal>(PROPERTY_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.unwantedVal)
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVal>(PROPERTY_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertFailsWith<UnsupportedOperationException> { obj.unwantedVal }.also { ex ->
assertEquals("Method has been deleted", ex.message)
}
}
}
}
}
}

View File

@ -0,0 +1,70 @@
package net.corda.gradle.jarfilter
import net.corda.gradle.unwanted.HasUnwantedVar
import org.junit.Assert.*
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestRule
import kotlin.test.assertFailsWith
class StubVarPropertyTest {
companion object {
private const val GETTER_CLASS = "net.corda.gradle.HasUnwantedGetForStub"
private const val SETTER_CLASS = "net.corda.gradle.HasUnwantedSetForStub"
private val testProjectDir = TemporaryFolder()
private val testProject = JarFilterProject(testProjectDir, "stub-var-property")
@ClassRule
@JvmField
val rules: TestRule = RuleChain
.outerRule(testProjectDir)
.around(testProject)
}
@Test
fun deleteGetter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVar>(GETTER_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertEquals(MESSAGE, obj.unwantedVar)
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVar>(GETTER_CLASS).apply {
getDeclaredConstructor(String::class.java).newInstance(MESSAGE).also { obj ->
assertFailsWith<UnsupportedOperationException> { obj.unwantedVar }.also { ex ->
assertEquals("Method has been deleted", ex.message)
}
}
}
}
}
@Test
fun deleteSetter() {
classLoaderFor(testProject.sourceJar).use { cl ->
cl.load<HasUnwantedVar>(SETTER_CLASS).apply {
getConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj ->
assertEquals(DEFAULT_MESSAGE, obj.unwantedVar)
obj.unwantedVar = MESSAGE
assertEquals(MESSAGE, obj.unwantedVar)
}
}
}
classLoaderFor(testProject.filteredJar).use { cl ->
cl.load<HasUnwantedVar>(SETTER_CLASS).apply {
getConstructor(String::class.java).newInstance(DEFAULT_MESSAGE).also { obj ->
assertEquals(DEFAULT_MESSAGE, obj.unwantedVar)
obj.unwantedVar = MESSAGE
assertEquals(DEFAULT_MESSAGE, obj.unwantedVar)
}
}
}
}
}

View File

@ -0,0 +1,82 @@
@file:JvmName("Utilities")
package net.corda.gradle.jarfilter
import org.junit.AssumptionViolatedException
import org.junit.rules.TemporaryFolder
import java.io.File
import java.io.IOException
import java.net.MalformedURLException
import java.net.URLClassLoader
import java.nio.file.StandardCopyOption.*
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.stream.Collectors.*
import java.util.zip.ZipFile
import kotlin.reflect.KClass
const val DEFAULT_MESSAGE = "<default-value>"
const val MESSAGE = "Goodbye, Cruel World!"
const val NUMBER = 111
const val BIG_NUMBER = 9999L
private val classLoader: ClassLoader = object {}.javaClass.classLoader
// The AssumptionViolatedException must be caught by the JUnit test runner,
// which means that it must not be thrown when this class loads.
private val testGradleUserHomeValue: String? = System.getProperty("test.gradle.user.home")
private val testGradleUserHome: String get() = testGradleUserHomeValue
?: throw AssumptionViolatedException("System property 'test.gradle.user.home' not set.")
fun getGradleArgsForTasks(vararg taskNames: String): MutableList<String> = getBasicArgsForTasks(*taskNames).apply { add("--info") }
fun getBasicArgsForTasks(vararg taskNames: String): MutableList<String> = mutableListOf(*taskNames, "-g", testGradleUserHome)
@Throws(IOException::class)
fun copyResourceTo(resourceName: String, target: Path) {
classLoader.getResourceAsStream(resourceName).use { source ->
Files.copy(source, target, REPLACE_EXISTING)
}
}
@Throws(IOException::class)
fun copyResourceTo(resourceName: String, target: File) = copyResourceTo(resourceName, target.toPath())
@Throws(IOException::class)
fun TemporaryFolder.installResources(vararg resourceNames: String) {
resourceNames.forEach { installResource(it) }
}
@Throws(IOException::class)
fun TemporaryFolder.installResource(resourceName: String): File = newFile(resourceName.fileName).let { file ->
copyResourceTo(resourceName, file)
file
}
private val String.fileName: String get() = substring(1 + lastIndexOf('/'))
val String.toPackageFormat: String get() = replace('/', '.')
fun pathsOf(vararg types: KClass<*>): Set<String> = types.map { it.java.name.toPathFormat }.toSet()
fun TemporaryFolder.pathOf(vararg elements: String): Path = Paths.get(root.absolutePath, *elements)
fun arrayOfJunk(size: Int) = ByteArray(size).apply {
for (i in 0 until size) {
this[i] = (i and 0xFF).toByte()
}
}
@Throws(MalformedURLException::class)
fun classLoaderFor(jar: Path) = URLClassLoader(arrayOf(jar.toUri().toURL()), classLoader)
@Suppress("UNCHECKED_CAST")
@Throws(ClassNotFoundException::class)
fun <T> ClassLoader.load(className: String)
= Class.forName(className, true, this) as Class<T>
fun Path.getClassNames(prefix: String): List<String> {
val resourcePrefix = prefix.toPathFormat
return ZipFile(toFile()).stream()
.filter { it.name.startsWith(resourcePrefix) && it.name.endsWith(".class") }
.map { it.name.removeSuffix(".class").toPackageFormat }
.collect(toList<String>())
}

View File

@ -0,0 +1,42 @@
package net.corda.gradle.jarfilter
import org.assertj.core.api.Assertions.assertThat
import org.gradle.api.GradleException
import org.gradle.api.InvalidUserDataException
import org.junit.Test
import java.io.IOException
import kotlin.test.assertFailsWith
class UtilsTest {
@Test
fun testRethrowingCheckedException() {
val ex = assertFailsWith<GradleException> { rethrowAsUncheckedException(IOException(MESSAGE)) }
assertThat(ex)
.hasMessage(MESSAGE)
.hasCauseExactlyInstanceOf(IOException::class.java)
}
@Test
fun testRethrowingCheckExceptionWithoutMessage() {
val ex = assertFailsWith<GradleException> { rethrowAsUncheckedException(IOException()) }
assertThat(ex)
.hasMessage("")
.hasCauseExactlyInstanceOf(IOException::class.java)
}
@Test
fun testRethrowingUncheckedException() {
val ex = assertFailsWith<IllegalArgumentException> { rethrowAsUncheckedException(IllegalArgumentException(MESSAGE)) }
assertThat(ex)
.hasMessage(MESSAGE)
.hasNoCause()
}
@Test
fun testRethrowingGradleException() {
val ex = assertFailsWith<InvalidUserDataException> { rethrowAsUncheckedException(InvalidUserDataException(MESSAGE)) }
assertThat(ex)
.hasMessage(MESSAGE)
.hasNoCause()
}
}

View File

@ -0,0 +1,8 @@
package net.corda.gradle.jarfilter.annotations
import kotlin.annotation.AnnotationRetention.BINARY
import kotlin.annotation.AnnotationTarget.PROPERTY
@Retention(BINARY)
@Target(PROPERTY)
annotation class Deletable

View File

@ -0,0 +1,47 @@
@file:JvmName("AsmTools")
package net.corda.gradle.jarfilter.asm
import net.corda.gradle.jarfilter.descriptor
import net.corda.gradle.jarfilter.toPathFormat
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.ClassWriter.COMPUTE_MAXS
import java.io.ByteArrayInputStream
import java.io.InputStream
fun ByteArray.accept(visitor: (ClassVisitor) -> ClassVisitor): ByteArray {
return ClassWriter(COMPUTE_MAXS).let { writer ->
ClassReader(this).accept(visitor(writer), 0)
writer.toByteArray()
}
}
private val String.resourceName: String get() = "$toPathFormat.class"
val Class<*>.resourceName get() = name.resourceName
val Class<*>.bytecode: ByteArray get() = classLoader.getResourceAsStream(resourceName).use { it.readBytes() }
val Class<*>.descriptor: String get() = name.descriptor
/**
* Functions for converting bytecode into a "live" Java class.
*/
inline fun <reified T: R, reified R: Any> ByteArray.toClass(): Class<out R> = toClass(T::class.java, R::class.java)
fun <T: R, R: Any> ByteArray.toClass(type: Class<in T>, asType: Class<out R>): Class<out R>
= BytecodeClassLoader(this, type.name, type.classLoader).createClass().asSubclass(asType)
private class BytecodeClassLoader(
private val bytecode: ByteArray,
private val className: String,
parent: ClassLoader
) : ClassLoader(parent) {
internal fun createClass(): Class<*> {
return defineClass(className, bytecode, 0, bytecode.size).apply { resolveClass(this) }
}
// Ensure that the class we create also honours Class<*>.bytecode (above).
override fun getResourceAsStream(name: String): InputStream? {
return if (name == className.resourceName) ByteArrayInputStream(bytecode) else super.getResourceAsStream(name)
}
}

View File

@ -0,0 +1,47 @@
package net.corda.gradle.jarfilter.asm
import net.corda.gradle.jarfilter.MetadataTransformer
import net.corda.gradle.jarfilter.toPackageFormat
import net.corda.gradle.jarfilter.mutableList
import org.gradle.api.logging.Logger
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.deserialization.TypeTable
internal class ClassMetadata(
logger: Logger,
d1: List<String>,
d2: List<String>
) : MetadataTransformer<ProtoBuf.Class>(
logger,
emptyList(),
emptyList(),
emptyList(),
emptyList(),
emptyList(),
{},
d1,
d2,
ProtoBuf.Class::parseFrom
) {
override val typeTable = TypeTable(message.typeTable)
override val className = nameResolver.getString(message.fqName)
override val nestedClassNames = mutableList(message.nestedClassNameList)
override val properties = mutableList(message.propertyList)
override val functions = mutableList(message.functionList)
override val constructors = mutableList(message.constructorList)
override val typeAliases = mutableList(message.typeAliasList)
override val sealedSubclassNames = mutableList(message.sealedSubclassFqNameList)
override fun rebuild(): ProtoBuf.Class = message
val sealedSubclasses: List<String> = sealedSubclassNames.map {
// Transform "a/b/c/BaseName.SubclassName" -> "a.b.c.BaseName$SubclassName"
nameResolver.getString(it).replace('.', '$').toPackageFormat }.toList()
val nestedClasses: List<String>
init {
val internalClassName = className.toPackageFormat
nestedClasses = nestedClassNames.map { "$internalClassName\$${nameResolver.getString(it)}" }.toList()
}
}

View File

@ -0,0 +1,33 @@
package net.corda.gradle.jarfilter.asm
import net.corda.gradle.jarfilter.MetadataTransformer
import net.corda.gradle.jarfilter.mutableList
import org.gradle.api.logging.Logger
import org.jetbrains.kotlin.metadata.ProtoBuf
import org.jetbrains.kotlin.metadata.deserialization.TypeTable
internal class FileMetadata(
logger: Logger,
d1: List<String>,
d2: List<String>
) : MetadataTransformer<ProtoBuf.Package>(
logger,
emptyList(),
emptyList(),
emptyList(),
emptyList(),
emptyList(),
{},
d1,
d2,
ProtoBuf.Package::parseFrom
) {
override val typeTable = TypeTable(message.typeTable)
override val properties = mutableList(message.propertyList)
override val functions = mutableList(message.functionList)
override val typeAliases = mutableList(message.typeAliasList)
override fun rebuild(): ProtoBuf.Package = message
val typeAliasNames: List<String> = typeAliases.map { nameResolver.getString(it.name) }.toList()
}

View File

@ -0,0 +1,86 @@
@file:JvmName("MetadataTools")
package net.corda.gradle.jarfilter.asm
import net.corda.gradle.jarfilter.StdOutLogging
import org.jetbrains.kotlin.load.java.JvmAnnotationNames.*
import org.objectweb.asm.*
import org.objectweb.asm.Opcodes.ASM6
@Suppress("UNCHECKED_CAST")
private val metadataClass: Class<out Annotation>
= object {}.javaClass.classLoader.loadClass("kotlin.Metadata") as Class<out Annotation>
/**
* Rewrite the bytecode for this class with the Kotlin @Metadata of another class.
*/
inline fun <reified T: Any, reified X: Any> recodeMetadataFor(): ByteArray = T::class.java.metadataAs(X::class.java)
fun <T: Any, X: Any> Class<in T>.metadataAs(template: Class<in X>): ByteArray {
val metadata = template.readMetadata().let { m ->
val templateDescriptor = template.descriptor
val templatePrefix = templateDescriptor.dropLast(1) + '$'
val targetDescriptor = descriptor
val targetPrefix = targetDescriptor.dropLast(1) + '$'
Pair(m.first, m.second.map { s ->
when {
// Replace any references to the template class with the target class.
s == templateDescriptor -> targetDescriptor
s.startsWith(templatePrefix) -> targetPrefix + s.substring(templatePrefix.length)
else -> s
}
}.toList())
}
return bytecode.accept { w -> MetadataWriter(metadata, w) }
}
/**
* Kotlin reflection only supports classes atm, so use this to examine file metadata.
*/
internal val Class<*>.fileMetadata: FileMetadata get() {
val (d1, d2) = readMetadata()
return FileMetadata(StdOutLogging(kotlin), d1, d2)
}
/**
* For accessing the parts of class metadata that Kotlin reflection cannot reach.
*/
internal val Class<*>.classMetadata: ClassMetadata get() {
val (d1, d2) = readMetadata()
return ClassMetadata(StdOutLogging(kotlin), d1, d2)
}
private fun Class<*>.readMetadata(): Pair<List<String>, List<String>> {
val metadata = getAnnotation(metadataClass)
val d1 = metadataClass.getMethod(METADATA_DATA_FIELD_NAME)
val d2 = metadataClass.getMethod(METADATA_STRINGS_FIELD_NAME)
return Pair(d1.invoke(metadata).asList(), d2.invoke(metadata).asList())
}
@Suppress("UNCHECKED_CAST")
fun <T> Any.asList(): List<T> {
return (this as? Array<T>)?.toList() ?: emptyList()
}
private class MetadataWriter(metadata: Pair<List<String>, List<String>>, visitor: ClassVisitor) : ClassVisitor(ASM6, visitor) {
private val kotlinMetadata: MutableMap<String, List<String>> = mutableMapOf(
METADATA_DATA_FIELD_NAME to metadata.first,
METADATA_STRINGS_FIELD_NAME to metadata.second
)
override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? {
val av = super.visitAnnotation(descriptor, visible) ?: return null
return if (descriptor == METADATA_DESC) KotlinMetadataWriter(av) else av
}
private inner class KotlinMetadataWriter(av: AnnotationVisitor) : AnnotationVisitor(api, av) {
override fun visitArray(name: String): AnnotationVisitor? {
val av = super.visitArray(name)
if (av != null) {
val data = kotlinMetadata.remove(name) ?: return av
data.forEach { av.visit(null, it) }
av.visitEnd()
}
return null
}
}
}

View File

@ -0,0 +1,79 @@
@file:JvmName("JavaMatchers")
package net.corda.gradle.jarfilter.matcher
import org.hamcrest.Description
import org.hamcrest.DiagnosingMatcher
import org.hamcrest.Matcher
import org.hamcrest.core.IsEqual.*
import java.lang.reflect.Method
import kotlin.reflect.KClass
fun isMethod(name: Matcher<in String>, returnType: Matcher<in Class<*>>, vararg parameters: Matcher<in Class<*>>): Matcher<Method> {
return MethodMatcher(name, returnType, *parameters)
}
fun isMethod(name: String, returnType: Class<*>, vararg parameters: Class<*>): Matcher<Method> {
return isMethod(equalTo(name), equalTo(returnType), *parameters.map(::equalTo).toTypedArray())
}
val <T: Any> KClass<T>.javaDeclaredMethods: List<Method> get() = java.declaredMethods.toList()
/**
* Matcher logic for a Java [Method] object. Also applicable to constructors.
*/
private class MethodMatcher(
private val name: Matcher<in String>,
private val returnType: Matcher<in Class<*>>,
vararg parameters: Matcher<in Class<*>>
) : DiagnosingMatcher<Method>() {
private val parameters = listOf(*parameters)
override fun describeTo(description: Description) {
description.appendText("Method[name as ").appendDescriptionOf(name)
.appendText(", returnType as ").appendDescriptionOf(returnType)
.appendText(", parameters as '")
if (parameters.isNotEmpty()) {
val param = parameters.iterator()
description.appendValue(param.next())
while (param.hasNext()) {
description.appendText(",").appendValue(param.next())
}
}
description.appendText("']")
}
override fun matches(obj: Any?, mismatch: Description): Boolean {
if (obj == null) {
mismatch.appendText("is null")
return false
}
val method: Method = obj as? Method ?: return false
if (!name.matches(method.name)) {
mismatch.appendText("name is ").appendValue(method.name)
return false
}
method.returnType.apply {
if (!returnType.matches(this)) {
mismatch.appendText("returnType is ").appendValue(this.name)
return false
}
}
if (method.parameterTypes.size != parameters.size) {
mismatch.appendText("number of parameters is ").appendValue(method.parameterTypes.size)
.appendText(", parameters=").appendValueList("[", ",", "]", method.parameterTypes)
return false
}
var i = 0
method.parameterTypes.forEach { param ->
if (!parameters[i].matches(param)) {
mismatch.appendText("parameter[").appendValue(i).appendText("] is ").appendValue(param)
return false
}
++i
}
return true
}
}

View File

@ -0,0 +1,193 @@
@file:JvmName("KotlinMatchers")
package net.corda.gradle.jarfilter.matcher
import org.hamcrest.Description
import org.hamcrest.DiagnosingMatcher
import org.hamcrest.Matcher
import org.hamcrest.core.IsEqual.equalTo
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty
import kotlin.reflect.full.valueParameters
import kotlin.reflect.jvm.jvmName
fun isFunction(name: Matcher<in String>, returnType: Matcher<in String>, vararg parameters: Matcher<in KParameter>): Matcher<KFunction<*>> {
return KFunctionMatcher(name, returnType, *parameters)
}
fun isFunction(name: String, returnType: KClass<*>, vararg parameters: KClass<*>): Matcher<KFunction<*>> {
return isFunction(equalTo(name), matches(returnType), *parameters.map(::hasParam).toTypedArray())
}
fun isConstructor(returnType: Matcher<in String>, vararg parameters: Matcher<in KParameter>): Matcher<KFunction<*>> {
return KFunctionMatcher(equalTo("<init>"), returnType, *parameters)
}
fun isConstructor(returnType: KClass<*>, vararg parameters: KClass<*>): Matcher<KFunction<*>> {
return isConstructor(matches(returnType), *parameters.map(::hasParam).toTypedArray())
}
fun isConstructor(returnType: String, vararg parameters: Matcher<in KParameter>): Matcher<KFunction<*>> {
return isConstructor(equalTo(returnType), *parameters)
}
fun hasParam(type: Matcher<in String>): Matcher<KParameter> = KParameterMatcher(type)
fun hasParam(type: KClass<*>): Matcher<KParameter> = hasParam(matches(type))
fun isProperty(name: String, type: KClass<*>): Matcher<KProperty<*>> = isProperty(equalTo(name), matches(type))
fun isProperty(name: Matcher<in String>, type: Matcher<in String>): Matcher<KProperty<*>> = KPropertyMatcher(name, type)
fun isClass(name: String): Matcher<KClass<*>> = KClassMatcher(equalTo(name))
fun matches(type: KClass<*>): Matcher<in String> = equalTo(type.qualifiedName)
/**
* Matcher logic for a Kotlin [KFunction] object. Also applicable to constructors.
*/
private class KFunctionMatcher(
private val name: Matcher<in String>,
private val returnType: Matcher<in String>,
vararg parameters: Matcher<in KParameter>
) : DiagnosingMatcher<KFunction<*>>() {
private val parameters = listOf(*parameters)
override fun describeTo(description: Description) {
description.appendText("KFunction[name as ").appendDescriptionOf(name)
.appendText(", returnType as ").appendDescriptionOf(returnType)
.appendText(", parameters as '")
if (parameters.isNotEmpty()) {
val param = parameters.iterator()
description.appendValue(param.next())
while (param.hasNext()) {
description.appendText(",").appendValue(param.next())
}
}
description.appendText("']")
}
override fun matches(obj: Any?, mismatch: Description): Boolean {
if (obj == null) {
mismatch.appendText("is null")
return false
}
val function: KFunction<*> = obj as? KFunction<*> ?: return false
if (!name.matches(function.name)) {
mismatch.appendText("name is ").appendValue(function.name)
return false
}
function.returnType.toString().apply {
if (!returnType.matches(this)) {
mismatch.appendText("returnType is ").appendValue(this)
return false
}
}
if (function.valueParameters.size != parameters.size) {
mismatch.appendText("number of parameters is ").appendValue(function.valueParameters.size)
.appendText(", parameters=").appendValueList("[", ",", "]", function.valueParameters)
return false
}
var i = 0
function.valueParameters.forEach { param ->
if (!parameters[i].matches(param)) {
mismatch.appendText("parameter[").appendValue(i).appendText("] is ").appendValue(param)
return false
}
++i
}
return true
}
}
/**
* Matcher logic for a Kotlin [KParameter] object.
*/
private class KParameterMatcher(
private val type: Matcher<in String>
) : DiagnosingMatcher<KParameter>() {
override fun describeTo(description: Description) {
description.appendText("KParameter[type as ").appendDescriptionOf(type)
.appendText("]")
}
override fun matches(obj: Any?, mismatch: Description): Boolean {
if (obj == null) {
mismatch.appendText("is null")
return false
}
val parameter: KParameter = obj as? KParameter ?: return false
parameter.type.toString().apply {
if (!type.matches(this)) {
mismatch.appendText("type is ").appendValue(this)
return false
}
}
return true
}
}
/**
* Matcher logic for a Kotlin [KProperty] object.
*/
private class KPropertyMatcher(
private val name: Matcher<in String>,
private val type: Matcher<in String>
) : DiagnosingMatcher<KProperty<*>>() {
override fun describeTo(description: Description) {
description.appendText("KProperty[name as ").appendDescriptionOf(name)
.appendText(", type as ").appendDescriptionOf(type)
.appendText("]")
}
override fun matches(obj: Any?, mismatch: Description): Boolean {
if (obj == null) {
mismatch.appendText("is null")
return false
}
val property: KProperty<*> = obj as? KProperty<*> ?: return false
if (!name.matches(property.name)) {
mismatch.appendText("name is ").appendValue(property.name)
return false
}
property.returnType.toString().apply {
if (!type.matches(this)) {
mismatch.appendText("type is ").appendValue(this)
return false
}
}
return true
}
}
/**
* Matcher logic for a Kotlin [KClass] object.
*/
private class KClassMatcher(private val className: Matcher<in String>) : DiagnosingMatcher<KClass<*>>() {
override fun describeTo(description: Description) {
description.appendText("KClass[name as ").appendDescriptionOf(className)
.appendText("]")
}
override fun matches(obj: Any?, mismatch: Description): Boolean {
if (obj == null) {
mismatch.appendText("is null")
return false
}
val type: KClass<*> = obj as? KClass<*> ?: return false
type.jvmName.apply {
if (!className.matches(this)) {
mismatch.appendText("name is ").appendValue(this)
return false
}
}
return true
}
}

View File

@ -0,0 +1,34 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/abstract-function/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
compileOnly files('../../unwanteds/build/libs/unwanteds.jar')
}
jar {
baseName = 'abstract-function'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
forStub = ["net.corda.gradle.jarfilter.StubMeOut"]
}
}

View File

@ -0,0 +1,13 @@
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.jarfilter.StubMeOut
abstract class AbstractFunctions {
@DeleteMe
abstract fun toDelete(value: Long): Long
@StubMeOut
abstract fun toStubOut(value: Long): Long
}

View File

@ -0,0 +1,20 @@
package net.corda.gradle.jarfilter
import kotlin.annotation.AnnotationRetention.*
import kotlin.annotation.AnnotationTarget.*
import kotlin.annotation.Retention
import kotlin.annotation.Target
@Target(
FILE,
CLASS,
CONSTRUCTOR,
FUNCTION,
PROPERTY,
PROPERTY_GETTER,
PROPERTY_SETTER,
FIELD,
TYPEALIAS
)
@Retention(BINARY)
annotation class DeleteMe

View File

@ -0,0 +1,19 @@
package net.corda.gradle.jarfilter
import kotlin.annotation.AnnotationRetention.*
import kotlin.annotation.AnnotationTarget.*
import kotlin.annotation.Retention
import kotlin.annotation.Target
@Target(
FILE,
CLASS,
CONSTRUCTOR,
FUNCTION,
PROPERTY,
PROPERTY_GETTER,
PROPERTY_SETTER,
FIELD
)
@Retention(RUNTIME)
annotation class RemoveMe

View File

@ -0,0 +1,15 @@
package net.corda.gradle.jarfilter
import kotlin.annotation.AnnotationRetention.*
import kotlin.annotation.AnnotationTarget.*
import kotlin.annotation.Retention
import kotlin.annotation.Target
@Target(
CONSTRUCTOR,
FUNCTION,
PROPERTY_GETTER,
PROPERTY_SETTER
)
@Retention(RUNTIME)
annotation class StubMeOut

View File

@ -0,0 +1,34 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/delete-and-stub/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
compileOnly files('../../unwanteds/build/libs/unwanteds.jar')
}
jar {
baseName = 'delete-and-stub'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
forStub = ["net.corda.gradle.jarfilter.StubMeOut"]
}
}

View File

@ -0,0 +1,12 @@
@file:JvmName("DeletePackageWithStubbed")
@file:Suppress("UNUSED")
@file:DeleteMe
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.jarfilter.StubMeOut
fun bracket(str: String): String = "[$str]"
@StubMeOut
fun stubbed(str: String): String = bracket(str)

View File

@ -0,0 +1,28 @@
@file:JvmName("HasDeletedInsideStubbed")
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.jarfilter.StubMeOut
import net.corda.gradle.unwanted.HasString
import net.corda.gradle.unwanted.HasUnwantedFun
import net.corda.gradle.unwanted.HasUnwantedVal
import net.corda.gradle.unwanted.HasUnwantedVar
class DeletedFunctionInsideStubbed(private val data: String): HasString, HasUnwantedFun {
@DeleteMe
override fun unwantedFun(str: String): String = str
@StubMeOut
override fun stringData(): String = unwantedFun(data)
}
class DeletedValInsideStubbed(@DeleteMe override val unwantedVal: String): HasString, HasUnwantedVal {
@StubMeOut
override fun stringData(): String = unwantedVal
}
class DeletedVarInsideStubbed(@DeleteMe override var unwantedVar: String) : HasString, HasUnwantedVar {
@StubMeOut
override fun stringData(): String = unwantedVar
}

View File

@ -0,0 +1,21 @@
@file:JvmName("HasPropertyForDeleteAndStub")
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.jarfilter.StubMeOut
import net.corda.gradle.unwanted.*
class HasVarPropertyForDeleteAndStub(value: Long) : HasLongVar {
@DeleteMe
@get:StubMeOut
@set:StubMeOut
override var longVar: Long = value
}
class HasValPropertyForDeleteAndStub(str: String) : HasStringVal {
@DeleteMe
@get:StubMeOut
override val stringVal: String = str
}

View File

@ -0,0 +1,33 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/delete-constructor/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
compileOnly files('../../unwanteds/build/libs/unwanteds.jar')
}
jar {
baseName = 'delete-constructor'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
}
}

View File

@ -0,0 +1,15 @@
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.unwanted.HasAll
class HasConstructorToDelete(private val message: String, private val data: Long) : HasAll {
@DeleteMe constructor(message: String) : this(message, 0)
@DeleteMe constructor(data: Long) : this("<nothing>", data)
constructor(data: Int) : this("<nothing>", data.toLong())
override fun stringData(): String = message
override fun longData(): Long = data
override fun intData(): Int = data.toInt()
}

View File

@ -0,0 +1,21 @@
@file:JvmName("PrimaryConstructorsToDelete")
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.unwanted.HasInt
import net.corda.gradle.unwanted.HasLong
import net.corda.gradle.unwanted.HasString
class PrimaryIntConstructorToDelete @DeleteMe constructor(private val value: Int) : HasInt {
override fun intData() = value
}
class PrimaryLongConstructorToDelete @DeleteMe constructor(private val value: Long) : HasLong {
override fun longData() = value
}
class PrimaryStringConstructorToDelete @DeleteMe constructor(private val value: String) : HasString {
override fun stringData() = value
}

View File

@ -0,0 +1,33 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/delete-extension-val/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
compileOnly files('../../unwanteds/build/libs/unwanteds.jar')
}
jar {
baseName = 'delete-extension-val'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
}
}

View File

@ -0,0 +1,10 @@
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.unwanted.HasUnwantedVal
class HasValExtension(override val unwantedVal: String) : HasUnwantedVal {
@DeleteMe
val List<String>.unwantedVal: String get() = this[0]
}

View File

@ -0,0 +1,32 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/delete-field/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
}
jar {
baseName = 'delete-field'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
}
}

View File

@ -0,0 +1,23 @@
@file:JvmName("HasFieldToDelete")
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
class HasStringFieldToDelete(value: String) {
@JvmField
@field:DeleteMe
val stringField: String = value
}
class HasLongFieldToDelete(value: Long) {
@JvmField
@field:DeleteMe
val longField: Long = value
}
class HasIntFieldToDelete(value: Int) {
@JvmField
@field:DeleteMe
val intField: Int = value
}

View File

@ -0,0 +1,32 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/delete-file-typealias/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
}
jar {
baseName = 'delete-file-typealias'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
}
}

View File

@ -0,0 +1,12 @@
@file:JvmName("FileWithTypeAlias")
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
typealias FileWantedType = Long
@DeleteMe
typealias FileUnwantedType = (String) -> Boolean
val Any.FileUnwantedType: String get() = "<value>"

View File

@ -0,0 +1,33 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/delete-function/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
compileOnly files('../../unwanteds/build/libs/unwanteds.jar')
}
jar {
baseName = 'delete-function'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
}
}

View File

@ -0,0 +1,12 @@
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.unwanted.HasUnwantedFun
class HasFunctionToDelete : HasUnwantedFun {
@DeleteMe
override fun unwantedFun(str: String): String {
return str
}
}

View File

@ -0,0 +1,13 @@
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.unwanted.HasString
import net.corda.gradle.unwanted.HasUnwantedFun
class HasIndirectFunctionToDelete(private val data: String) : HasUnwantedFun, HasString {
@DeleteMe
override fun unwantedFun(str: String): String = str
override fun stringData() = unwantedFun(data)
}

View File

@ -0,0 +1,33 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/delete-lazy/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
compileOnly files('../../unwanteds/build/libs/unwanteds.jar')
}
jar {
baseName = 'delete-lazy'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
}
}

View File

@ -0,0 +1,13 @@
@file:JvmName("HasLazy")
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
import net.corda.gradle.unwanted.HasUnwantedVal
class HasLazyVal(private val message: String) : HasUnwantedVal {
@DeleteMe
override val unwantedVal: String by lazy {
message
}
}

View File

@ -0,0 +1,32 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/delete-multifile/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
}
jar {
baseName = 'delete-multifile'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
}
}

View File

@ -0,0 +1,9 @@
@file:JvmName("HasMultiData")
@file:JvmMultifileClass
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
@DeleteMe
fun intToDelete(data: Int): Int = data

View File

@ -0,0 +1,9 @@
@file:JvmName("HasMultiData")
@file:JvmMultifileClass
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
@DeleteMe
fun longToDelete(data: Long): Long = data

View File

@ -0,0 +1,9 @@
@file:JvmName("HasMultiData")
@file:JvmMultifileClass
@file:Suppress("UNUSED")
package net.corda.gradle
import net.corda.gradle.jarfilter.DeleteMe
@DeleteMe
fun stringToDelete(str: String): String = str

View File

@ -0,0 +1,32 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '$kotlin_version'
id 'net.corda.plugins.jar-filter'
}
apply from: 'repositories.gradle'
sourceSets {
main {
kotlin {
srcDir files(
'../resources/test/delete-nested-class/kotlin',
'../resources/test/annotations/kotlin'
)
}
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
}
jar {
baseName = 'delete-nested-class'
}
import net.corda.gradle.jarfilter.JarFilterTask
task jarFilter(type: JarFilterTask) {
jars jar
annotations {
forDelete = ["net.corda.gradle.jarfilter.DeleteMe"]
}
}

Some files were not shown because too many files have changed in this diff Show More