├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ └── release-drafter.yml ├── .gitignore ├── .mvn ├── extensions.xml └── maven.config ├── Jenkinsfile ├── README.md ├── annotation ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── infradna │ └── tool │ └── bridge_method_injector │ ├── BridgeMethodsAdded.java │ └── WithBridgeMethods.java ├── injector ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── infradna │ │ └── tool │ │ └── bridge_method_injector │ │ ├── ClassAnnotationInjector.java │ │ ├── MethodInjector.java │ │ └── ProcessMojo.java │ └── test │ ├── client │ └── Main.java │ ├── java │ └── com │ │ └── infradna │ │ └── tool │ │ └── bridge_method_injector │ │ └── Junit4TestsRanTest.java │ ├── synthetics │ ├── A.java │ └── B.java │ ├── v1 │ ├── Adapter.java │ ├── Bar.java │ ├── Foo.java │ ├── IBar.java │ └── IFoo.java │ └── v2 │ ├── Adapter.java │ ├── Bar.java │ ├── Foo.java │ ├── IBar.java │ └── IFoo.java └── pom.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | --- 3 | version: 2 4 | updates: 5 | - package-ecosystem: "maven" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | ignore: 10 | # maven core artifacts are provided by the running maven, do not update to prevent consuming something unavailable 11 | - dependency-name: "org.apache.maven:*" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc 2 | 3 | _extends: .github 4 | tag-template: bridge-method-injector-parent-$NEXT_MINOR_VERSION 5 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Automates creation of Release Drafts using Release Drafter 2 | # More Info: https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v6 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | # mvn hpi:run 4 | work 5 | 6 | # IntelliJ IDEA project files 7 | *.iml 8 | *.iws 9 | *.ipr 10 | .idea 11 | 12 | # Eclipse project files 13 | .settings 14 | .classpath 15 | .project 16 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* 2 | * While this is not a plugin, it is much simpler to reuse the pipeline code for CI. This allows for 3 | * easy Linux/Windows testing and produces incrementals. The only feature that relates to plugins is 4 | * allowing one to test against multiple Jenkins versions. 5 | */ 6 | buildPlugin(useContainerAgent: true, configurations: [ 7 | [platform: 'linux', jdk: 21], 8 | [platform: 'windows', jdk: 17], 9 | ]) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bridge Method Injector 2 | 3 | ## What's this? 4 | 5 | When you are writing a library, there are various restrictions about the kind of changes you can make, in order to maintain binary compatibility. 6 | 7 | One such restriction is an inability to restrict the return type. Say in v1 of your library you had the following code: 8 | 9 | ```java 10 | public Foo getFoo() { 11 | return new Foo(); 12 | } 13 | ``` 14 | 15 | In v2, say if you introduce a subtype of `Foo` called `FooSubType`, and you want to change the getFoo method to return `FooSubType`. 16 | 17 | ```java 18 | public FooSubType getFoo() { 19 | return new FooSubType(); 20 | } 21 | ``` 22 | 23 | But if you do this, you break the binary compatibility. The clients need to be recompiled to be able to work with the new signature. This is where this bridge method injector can help. By adding an annotation like the following: 24 | 25 | ```java 26 | @WithBridgeMethods(Foo.class) 27 | public FooSubType getFoo() { 28 | return new FooSubType(); 29 | } 30 | ``` 31 | 32 | ... and running the bytecode post processor, your class file will get the additional "bridge methods." In pseudocode, it'll look like this: 33 | 34 | ```java 35 | // your original definition 36 | @WithBridgeMethods(Foo.class) 37 | public FooSubType getFoo() { 38 | return new FooSubType(); 39 | } 40 | 41 | // added bridge method 42 | public Foo getFoo() { 43 | invokevirtual this.getFoo()LFooSubType; 44 | areturn 45 | } 46 | ``` 47 | 48 | Such code isn't allowed in Java source files, but class files allow that. With this addition, existing clients will continue to function. 49 | 50 | In this way, you can evolve your classes more easily without breaking backward compatibility. 51 | 52 | ## Widening the return type 53 | 54 | In some cases, it's convenient to widen the return type of a method. As this is potentially a type-unsafe change 55 | (as the callee can return a type that's not assignable to what the caller expects), so 56 | you as a programmer explicitly need to tell us that you know what you are doing by adding 57 | `castRequired` to the annotation. For example, suppose that v1 had a method: 58 | 59 | ```java 60 | public createFoo(Class clazz) { 61 | return clazz.newInstance(); 62 | } 63 | ``` 64 | 65 | and in v2 you wanted to widen this method to. Note that you can prove that this is still type-safe, while 66 | your compile cannot: 67 | 68 | ```java 69 | public createFoo(Class clazz) { 70 | return clazz.newInstance(); 71 | } 72 | ``` 73 | 74 | The annotation to provide backwards compatibility would be: 75 | 76 | ```java 77 | @WithBridgeMethods(value=FooSubType.class, castRequired=true) 78 | public createFoo(Class clazz) { 79 | return clazz.newInstance(); 80 | } 81 | ``` 82 | 83 | Running the bytecode post processor, the resulting class file will look like the following pseudocode: 84 | 85 | ```java 86 | // your original definition 87 | @WithBridgeMethods(value=FooSubType.class, castRequired=true) 88 | public createFoo(Class clazz) { 89 | return clazz.newInstance(); 90 | } 91 | 92 | // added bridge method 93 | public FooSubType createFoo(Class clazz) { 94 | invokevirtual this.createFoo(java/lang/Class)LFoo 95 | checkcast FooSubType 96 | areturn 97 | } 98 | ``` 99 | 100 | ## Adapter methods 101 | 102 | In extreme cases, we can add a method whose return type has nothing to do with the return type of the declared method. 103 | For example, if you have the following code: 104 | 105 | ```java 106 | @WithBridgeMethods(value = String.class, adapterMethod = "convert") 107 | public URL getURL() { 108 | URL url = .... 109 | return url; 110 | } 111 | 112 | private Object convert(URL url, Class targetType) { 113 | return url.toString(); 114 | } 115 | ``` 116 | 117 | The Maven mojo will insert the following bridge method: 118 | 119 | ```java 120 | public String getURL() { 121 | return (String) convert(getURL(), String.class); // invokeVirtual to getURL that returns URL 122 | } 123 | ``` 124 | 125 | The specified adapter method must be a method specified on the current class or its ancestors. 126 | It cannot be a static method. 127 | 128 | ## Bridge methods and interfaces 129 | 130 | You can use `@WithBridgeMethods` with interfaces, too. However, making this work correctly is tricky, 131 | as you have to ensure that bridge methods are implemented on all the classes that implement the interface, 132 | for example by adding `@WithBridgeMethods` on every implementation of the method in question, 133 | or by introducing a base class that provides a bridge method. 134 | 135 | For adapter methods, the bridge method annotation on the interface does not need to declare the 136 | adapter method, but the bridge method annotation on the implementation does. 137 | 138 | See the Javadoc for more details: 139 | 140 | - [`bridge-method-annotation`](https://javadoc.jenkins.io/component/bridge-method-annotation/) 141 | - [`bridge-method-injector`](https://javadoc.jenkins.io/component/bridge-method-injector/) 142 | 143 | ## Maven repository 144 | 145 | Starting with 1.25, this library is published at: 146 | 147 | ```xml 148 | 149 | repo.jenkins-ci.org 150 | https://repo.jenkins-ci.org/releases/ 151 | 152 | ``` 153 | 154 | ## Java support 155 | 156 | Starting with 1.25, this library requires Java 11 or newer. 157 | 158 | ## Integration into your build 159 | 160 | Add the following dependency in your POM. (This dependency is not needed at runtime, but it is necessary 161 | for compilation of source code that transitively depend on this, so it is the simplest to just treat 162 | this like a regular library dependency) 163 | 164 | ```xml 165 | 166 | com.infradna.tool 167 | bridge-method-annotation 168 | 1.25 169 | 170 | ``` 171 | 172 | Then put the following fragment in your build to have the byte-code post processor kick in to inject the necessary bridge methods. 173 | 174 | ```xml 175 | 176 | 177 | 178 | com.infradna.tool 179 | bridge-method-injector 180 | 1.25 181 | 182 | 183 | 184 | process 185 | 186 | 187 | 188 | 189 | 190 | 191 | ``` 192 | -------------------------------------------------------------------------------- /annotation/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.infradna.tool 6 | bridge-method-injector-parent 7 | ${revision}${changelist} 8 | 9 | bridge-method-annotation 10 | Bridge method injection annotations 11 | 12 | 13 | 14 | org.jenkins-ci 15 | annotation-indexer 16 | 1.18 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/infradna/tool/bridge_method_injector/BridgeMethodsAdded.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010, InfraDNA, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package com.infradna.tool.bridge_method_injector; 25 | 26 | import java.lang.annotation.ElementType; 27 | import java.lang.annotation.Retention; 28 | import java.lang.annotation.RetentionPolicy; 29 | import java.lang.annotation.Target; 30 | 31 | /** 32 | * This annotation is added after the class transformation to indicate that 33 | * the class has already been processed by bridge method injector. 34 | * 35 | *

36 | * Used for up-to-date check to avoid unnecessary transformations. 37 | * 38 | * @author Kohsuke Kawaguchi 39 | */ 40 | @Retention(RetentionPolicy.CLASS) 41 | @Target(ElementType.TYPE) 42 | public @interface BridgeMethodsAdded {} 43 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/infradna/tool/bridge_method_injector/WithBridgeMethods.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010, InfraDNA, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package com.infradna.tool.bridge_method_injector; 25 | 26 | import java.lang.annotation.Documented; 27 | import java.lang.annotation.ElementType; 28 | import java.lang.annotation.Retention; 29 | import java.lang.annotation.RetentionPolicy; 30 | import java.lang.annotation.Target; 31 | import org.jvnet.hudson.annotation_indexer.Indexed; 32 | 33 | /** 34 | * Request that bridge methods of the same name and same arguments be generated 35 | * with each specified type as the return type. This helps you maintain binary compatibility 36 | * as you evolve your classes. 37 | * 38 | *

39 | * For example, if you have the following code: 40 | * 41 | *

 42 |  * @WithBridgeMethods(Foo.class)
 43 |  * public FooSubType getFoo() { ... }
 44 |  * 
45 | * 46 | *

47 | * The Maven mojo will insert the following bridge method: 48 | * 49 | *

 50 |  * public Foo getFoo() {
 51 |  *     return getFoo(); // invokevirtual to getFoo() that returns FooSubType
 52 |  * }
 53 |  * 
54 | * 55 | *

56 | * In some cases, it's necessary to widen the return type of a method, but in a way that legacy 57 | * calls would still return instances of the original type. In this case, add 58 | * {@link #castRequired() castRequired=true} to the annotation. For example, if you have the 59 | * following code: 60 | *

 61 |  * @WithBridgeMethods(value=FooSubType.class, castRequired=true)
 62 |  * public <T extends Foo> createFoo(Class<T> clazz) {
 63 |  *   return clazz.newInstance();
 64 |  * }
 65 |  * 
66 | *

67 | * The Maven mojo will insert the following bridge method: 68 | * 69 | *

 70 |  * public FooSubType createFoo(Class clazz) {
 71 |  *   return (FooSubType) createFoo(clazz); // invokeVirtual to createFoo that returns Foo
 72 |  * }
 73 |  * 
74 | * 75 | *

76 | * In extreme cases, this method can add a method whose return type has nothing to do 77 | * with the return type of the declared method. For example, if you have the following code: 78 | * 79 | *

 80 |  * @WithBridgeMethods(value=String.class, adapterMethod="convert")
 81 |  * public URL getURL() {
 82 |  *   URL url = ....
 83 |  *   return url;
 84 |  * }
 85 |  *
 86 |  * private Object convert(URL url, Class targetType) { return url.toString(); }
 87 |  * 
88 | * 89 | *

90 | * The Maven mojo will insert the following bridge method: 91 | * 92 | *

 93 |  * public String getURL() {
 94 |  *   return (String)convert(getURL(),String.class);  // invokeVirtual to getURL that returns URL
 95 |  * }
 96 |  * 
97 | * 98 | *

99 | * The specified adapter method must be a method specified on the current class 100 | * or its ancestors. It cannot be a static method. 101 | * 102 | * @author Kohsuke Kawaguchi 103 | */ 104 | @Retention(RetentionPolicy.CLASS) 105 | @Target(ElementType.METHOD) 106 | @Documented 107 | @Indexed 108 | public @interface WithBridgeMethods { 109 | /** 110 | * Specifies the return types. These types must be assignable from the actual 111 | * method return type, or {@link #castRequired()} should be set to true. 112 | */ 113 | Class[] value(); 114 | 115 | /** 116 | * Specifies whether the injected bridge methods should perform a cast prior to returning. Only 117 | * set this to true when it is known that calls to the bridge methods will in fact return 118 | * objects assignable to {@linkplain #value() the bridge method return type}, even though 119 | * the declared method return type is not assignable to them. 120 | * 121 | * @since 1.4 122 | */ 123 | boolean castRequired() default false; 124 | 125 | /** 126 | * Specifies the method to convert return value. This lets bridge methods to return 127 | * any types, even if it's unrelated to the return type of the declared method. 128 | * 129 | * @since 1.14 130 | */ 131 | String adapterMethod() default ""; 132 | } 133 | -------------------------------------------------------------------------------- /injector/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.infradna.tool 6 | bridge-method-injector-parent 7 | ${revision}${changelist} 8 | 9 | 10 | bridge-method-injector 11 | maven-plugin 12 | bridge-method-injector 13 | Evolve your classes without breaking compatibility 14 | 15 | 16 | ${maven.version} 17 | 18 | 19 | 20 | 9.8 21 | 3.15.1 22 | 3.9.6 23 | 24 | 25 | 26 | 27 | ${project.groupId} 28 | bridge-method-annotation 29 | ${project.version} 30 | 31 | 32 | com.github.spotbugs 33 | spotbugs-annotations 34 | true 35 | 36 | 37 | com.google.code.findbugs 38 | jsr305 39 | 40 | 41 | 42 | 43 | org.ow2.asm 44 | asm 45 | ${asm.version} 46 | 47 | 48 | org.ow2.asm 49 | asm-commons 50 | ${asm.version} 51 | 52 | 53 | org.apache.maven 54 | maven-plugin-api 55 | ${maven.version} 56 | provided 57 | 58 | 59 | org.apache.maven.plugin-tools 60 | maven-plugin-annotations 61 | ${maven-plugin-tools.version} 62 | provided 63 | 64 | 65 | junit 66 | junit 67 | test 68 | 69 | 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-plugin-plugin 76 | ${maven-plugin-tools.version} 77 | 78 | bridge-method-injector 79 | 80 | 81 | 82 | maven-antrun-plugin 83 | 84 | 85 | 86 | run 87 | 88 | test 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /injector/src/main/java/com/infradna/tool/bridge_method_injector/ClassAnnotationInjector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010, InfraDNA, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package com.infradna.tool.bridge_method_injector; 25 | 26 | import org.objectweb.asm.ClassVisitor; 27 | import org.objectweb.asm.FieldVisitor; 28 | import org.objectweb.asm.MethodVisitor; 29 | import org.objectweb.asm.Opcodes; 30 | 31 | /** 32 | * Adds class annotations. 33 | * 34 | * @author Kohsuke Kawaguchi 35 | */ 36 | abstract class ClassAnnotationInjector extends ClassVisitor { 37 | ClassAnnotationInjector(ClassVisitor cv) { 38 | super(Opcodes.ASM9, cv); 39 | } 40 | 41 | private boolean emitted = false; 42 | 43 | private void maybeEmit() { 44 | if (!emitted) { 45 | emitted = true; 46 | emit(); 47 | } 48 | } 49 | 50 | protected abstract void emit(); 51 | 52 | @Override 53 | public void visitInnerClass(String name, String outerName, String innerName, int access) { 54 | maybeEmit(); 55 | super.visitInnerClass(name, outerName, innerName, access); 56 | } 57 | 58 | @Override 59 | public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { 60 | maybeEmit(); 61 | return super.visitField(access, name, desc, signature, value); 62 | } 63 | 64 | @Override 65 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 66 | maybeEmit(); 67 | return super.visitMethod(access, name, desc, signature, exceptions); 68 | } 69 | 70 | @Override 71 | public void visitEnd() { 72 | maybeEmit(); 73 | super.visitEnd(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /injector/src/main/java/com/infradna/tool/bridge_method_injector/MethodInjector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010, InfraDNA, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package com.infradna.tool.bridge_method_injector; 25 | 26 | import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; 27 | import static org.objectweb.asm.Opcodes.ACC_ABSTRACT; 28 | import static org.objectweb.asm.Opcodes.ACC_BRIDGE; 29 | import static org.objectweb.asm.Opcodes.ACC_STATIC; 30 | import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; 31 | import static org.objectweb.asm.Opcodes.ILOAD; 32 | import static org.objectweb.asm.Opcodes.INVOKESTATIC; 33 | import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; 34 | import static org.objectweb.asm.Opcodes.IRETURN; 35 | import static org.objectweb.asm.Opcodes.POP; 36 | import static org.objectweb.asm.Opcodes.POP2; 37 | 38 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 39 | import java.io.BufferedInputStream; 40 | import java.io.File; 41 | import java.io.FileInputStream; 42 | import java.io.FileOutputStream; 43 | import java.io.IOException; 44 | import java.util.ArrayList; 45 | import java.util.List; 46 | import org.objectweb.asm.AnnotationVisitor; 47 | import org.objectweb.asm.ClassReader; 48 | import org.objectweb.asm.ClassVisitor; 49 | import org.objectweb.asm.ClassWriter; 50 | import org.objectweb.asm.MethodVisitor; 51 | import org.objectweb.asm.Opcodes; 52 | import org.objectweb.asm.Type; 53 | import org.objectweb.asm.commons.GeneratorAdapter; 54 | 55 | /** 56 | * Injects bridge methods as per {@link WithBridgeMethods}. 57 | * 58 | * @author Kohsuke Kawaguchi 59 | */ 60 | public class MethodInjector { 61 | public void handleRecursively(File f) throws IOException { 62 | if (f.isDirectory()) { 63 | File[] files = f.listFiles(); 64 | if (files != null) { 65 | for (File c : files) { 66 | handleRecursively(c); 67 | } 68 | } 69 | } else if (f.getName().endsWith(".class")) { 70 | handle(f); 71 | } 72 | } 73 | 74 | public void handle(File classFile) throws IOException { 75 | byte[] image; 76 | try (FileInputStream in = new FileInputStream(classFile); 77 | BufferedInputStream bis = new BufferedInputStream(in)) { 78 | ClassReader cr = new ClassReader(bis); 79 | ClassWriter cw = new ClassWriter(cr, COMPUTE_MAXS); 80 | cr.accept(new Transformer(new ClassAnnotationInjectorImpl(cw)), 0); 81 | image = cw.toByteArray(); 82 | } catch (AlreadyUpToDate unused) { 83 | // no need to process this class. it's already up-to-date. 84 | return; 85 | } catch (IOException | RuntimeException e) { 86 | throw new IOException("Failed to process " + classFile, e); 87 | } 88 | 89 | // write it back 90 | try (FileOutputStream out = new FileOutputStream(classFile)) { 91 | out.write(image); 92 | } 93 | } 94 | 95 | /** 96 | * Thrown to indicate that there's no need to re-process this class file. 97 | */ 98 | static class AlreadyUpToDate extends RuntimeException { 99 | private static final long serialVersionUID = 1L; 100 | } 101 | 102 | static class ClassAnnotationInjectorImpl extends ClassAnnotationInjector { 103 | ClassAnnotationInjectorImpl(ClassVisitor cv) { 104 | super(cv); 105 | } 106 | 107 | @Override 108 | protected void emit() { 109 | AnnotationVisitor av = cv.visitAnnotation(SYNTHETIC_METHODS_ADDED, false); 110 | av.visitEnd(); 111 | } 112 | } 113 | 114 | private static class WithBridgeMethodsAnnotationVisitor extends AnnotationVisitor { 115 | protected boolean castRequired = false; 116 | protected String adapterMethod = null; 117 | protected final List types = new ArrayList<>(); 118 | 119 | public WithBridgeMethodsAnnotationVisitor(AnnotationVisitor av) { 120 | super(Opcodes.ASM9, av); 121 | } 122 | 123 | @Override 124 | public AnnotationVisitor visitArray(String name) { 125 | return new AnnotationVisitor(Opcodes.ASM9, super.visitArray(name)) { 126 | 127 | @Override 128 | public void visit(String name, Object value) { 129 | if (value instanceof Type) { 130 | // assume this is a member of the array of classes named "value" in WithBridgeMethods 131 | types.add((Type) value); 132 | } 133 | super.visit(name, value); 134 | } 135 | }; 136 | } 137 | 138 | @Override 139 | public void visit(String name, Object value) { 140 | if ("castRequired".equals(name) && value instanceof Boolean) { 141 | castRequired = (Boolean) value; 142 | } 143 | if ("adapterMethod".equals(name) && value instanceof String) { 144 | adapterMethod = (String) value; 145 | } 146 | super.visit(name, value); 147 | } 148 | } 149 | 150 | static class Transformer extends ClassVisitor { 151 | private String internalClassName; 152 | /** 153 | * Synthetic methods to be generated. 154 | */ 155 | private final List syntheticMethods = new ArrayList<>(); 156 | 157 | class SyntheticMethod { 158 | final int access; 159 | final String name; 160 | final String desc; 161 | final String originalSignature; 162 | final String[] exceptions; 163 | final boolean castRequired; 164 | final String adapterMethod; 165 | 166 | /** 167 | * Return type of the bridge method to be inserted. 168 | */ 169 | final Type returnType; 170 | /** 171 | * Return type of the declared method written in the source code. 172 | */ 173 | final Type originalReturnType; 174 | 175 | SyntheticMethod( 176 | int access, 177 | String name, 178 | String desc, 179 | String originalSignature, 180 | String[] exceptions, 181 | Type returnType, 182 | boolean castRequired, 183 | String adapterMethod) { 184 | this.access = access; 185 | this.name = name; 186 | this.desc = desc; 187 | this.originalSignature = originalSignature; 188 | this.exceptions = exceptions; 189 | this.returnType = returnType; 190 | this.castRequired = castRequired; 191 | this.adapterMethod = adapterMethod; 192 | originalReturnType = Type.getReturnType(desc); 193 | } 194 | 195 | /** 196 | * Injects a synthetic method and send it to cv. 197 | */ 198 | public void inject(ClassVisitor cv) { 199 | Type[] paramTypes = Type.getArgumentTypes(desc); 200 | 201 | int access = this.access | ACC_SYNTHETIC | ACC_BRIDGE; 202 | String methodDescriptor = Type.getMethodDescriptor(returnType, paramTypes); 203 | MethodVisitor mv = cv.visitMethod( 204 | access, name, methodDescriptor, null /*TODO:is this really correct?*/, exceptions); 205 | if ((access & ACC_ABSTRACT) == 0) { 206 | GeneratorAdapter ga = new GeneratorAdapter(mv, access, name, methodDescriptor); 207 | mv.visitCode(); 208 | 209 | int sz = 0; 210 | 211 | if (hasAdapterMethod()) { 212 | // the LHS of the adapter method invocation 213 | ga.loadThis(); 214 | sz++; 215 | } 216 | 217 | boolean isStatic = (access & ACC_STATIC) != 0; 218 | if (!isStatic) { 219 | ga.loadThis(); 220 | sz++; 221 | } 222 | 223 | int argpos = 0; 224 | for (Type p : paramTypes) { 225 | mv.visitVarInsn(p.getOpcode(ILOAD), argpos + (isStatic ? 0 : 1)); 226 | argpos += p.getSize(); 227 | } 228 | sz += argpos; 229 | 230 | mv.visitMethodInsn(isStatic ? INVOKESTATIC : INVOKEVIRTUAL, internalClassName, name, desc, false); 231 | if (hasAdapterMethod()) { 232 | insertAdapterMethod(ga); 233 | } else if (castRequired || returnType.equals(Type.VOID_TYPE)) { 234 | ga.unbox(returnType); 235 | } else { 236 | ga.box(originalReturnType); 237 | } 238 | if (returnType.equals(Type.VOID_TYPE) 239 | || returnType.getClassName().equals("java.lang.Void")) { 240 | // bridge to void, which means disregard the return value from the original method 241 | switch (originalReturnType.getSize()) { 242 | case 0: 243 | throw new IllegalArgumentException("Cannot bridge " + name 244 | + " from void to void; did you mean to use a different type?"); 245 | case 1: 246 | mv.visitInsn(POP); 247 | break; 248 | case 2: 249 | mv.visitInsn(POP2); 250 | break; 251 | default: 252 | throw new AssertionError("Unexpected operand size: " + originalReturnType); 253 | } 254 | } 255 | mv.visitInsn(returnType.getOpcode(IRETURN)); 256 | mv.visitMaxs(sz, 0); 257 | } 258 | mv.visitEnd(); 259 | } 260 | 261 | private boolean hasAdapterMethod() { 262 | return adapterMethod != null && adapterMethod.length() > 0; 263 | } 264 | 265 | private void insertAdapterMethod(GeneratorAdapter ga) { 266 | ga.push(returnType); 267 | ga.visitMethodInsn( 268 | INVOKEVIRTUAL, 269 | internalClassName, 270 | adapterMethod, 271 | Type.getMethodDescriptor( 272 | Type.getType(Object.class), // return type 273 | originalReturnType, 274 | Type.getType(Class.class)), 275 | false); 276 | ga.unbox(returnType); 277 | } 278 | } 279 | 280 | Transformer(ClassVisitor cv) { 281 | super(Opcodes.ASM9, cv); 282 | } 283 | 284 | @Override 285 | public void visit( 286 | int version, int access, String name, String signature, String superName, String[] interfaces) { 287 | this.internalClassName = name; 288 | super.visit(version, access, name, signature, superName, interfaces); 289 | } 290 | 291 | @Override 292 | public AnnotationVisitor visitAnnotation(String desc, boolean visible) { 293 | if (desc.equals(SYNTHETIC_METHODS_ADDED)) { 294 | throw new AlreadyUpToDate(); // no need to process this class 295 | } 296 | return super.visitAnnotation(desc, visible); 297 | } 298 | 299 | /** 300 | * Look for methods annotated with {@link WithBridgeMethods}. 301 | */ 302 | @Override 303 | public MethodVisitor visitMethod( 304 | final int access, 305 | final String name, 306 | final String mdesc, 307 | final String signature, 308 | final String[] exceptions) { 309 | MethodVisitor mv = super.visitMethod(access, name, mdesc, signature, exceptions); 310 | return new MethodVisitor(Opcodes.ASM9, mv) { 311 | @Override 312 | public AnnotationVisitor visitAnnotation(String adesc, boolean visible) { 313 | AnnotationVisitor av = super.visitAnnotation(adesc, visible); 314 | if (adesc.equals(WITH_SYNTHETIC_METHODS) && (access & ACC_SYNTHETIC) == 0) { 315 | return new WithBridgeMethodsAnnotationVisitor(av) { 316 | 317 | @Override 318 | public void visitEnd() { 319 | super.visitEnd(); 320 | for (Type type : this.types) { 321 | syntheticMethods.add(new SyntheticMethod( 322 | access, 323 | name, 324 | mdesc, 325 | signature, 326 | exceptions, 327 | type, 328 | this.castRequired, 329 | this.adapterMethod)); 330 | } 331 | } 332 | }; 333 | } 334 | return av; 335 | } 336 | }; 337 | } 338 | 339 | /** 340 | * Inject methods at the end. 341 | */ 342 | @Override 343 | public void visitEnd() { 344 | for (SyntheticMethod m : syntheticMethods) { 345 | m.inject(cv); 346 | } 347 | super.visitEnd(); 348 | } 349 | } 350 | 351 | @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "user-provided value for running the program") 352 | public static void main(String[] args) throws IOException { 353 | MethodInjector mi = new MethodInjector(); 354 | for (String a : args) { 355 | mi.handleRecursively(new File(a)); 356 | } 357 | } 358 | 359 | private static final String SYNTHETIC_METHODS_ADDED = Type.getDescriptor(BridgeMethodsAdded.class); 360 | private static final String WITH_SYNTHETIC_METHODS = Type.getDescriptor(WithBridgeMethods.class); 361 | } 362 | -------------------------------------------------------------------------------- /injector/src/main/java/com/infradna/tool/bridge_method_injector/ProcessMojo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010, InfraDNA, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package com.infradna.tool.bridge_method_injector; 25 | 26 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 27 | import java.io.File; 28 | import java.io.IOException; 29 | import java.net.URL; 30 | import java.net.URLClassLoader; 31 | import org.apache.maven.plugin.AbstractMojo; 32 | import org.apache.maven.plugin.MojoExecutionException; 33 | import org.apache.maven.plugins.annotations.LifecyclePhase; 34 | import org.apache.maven.plugins.annotations.Mojo; 35 | import org.apache.maven.plugins.annotations.Parameter; 36 | import org.apache.maven.plugins.annotations.ResolutionScope; 37 | import org.jvnet.hudson.annotation_indexer.Index; 38 | 39 | /** 40 | * @author Kohsuke Kawaguchi 41 | */ 42 | @Mojo( 43 | name = "process", 44 | requiresDependencyResolution = ResolutionScope.RUNTIME, 45 | defaultPhase = LifecyclePhase.PROCESS_CLASSES, 46 | threadSafe = true) 47 | public class ProcessMojo extends AbstractMojo { 48 | /** 49 | * The directory containing generated classes. 50 | */ 51 | @Parameter(defaultValue = "${project.build.outputDirectory}", required = true) 52 | private File classesDirectory; 53 | 54 | @Override 55 | @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "user-provided value for running the program") 56 | public void execute() throws MojoExecutionException { 57 | try { 58 | for (String line : Index.listClassNames( 59 | WithBridgeMethods.class, 60 | new URLClassLoader( 61 | new URL[] {classesDirectory.toURI().toURL()}, 62 | ClassLoader.getSystemClassLoader().getParent()))) { 63 | File classFile = new File(classesDirectory, line.replace('.', '/') + ".class"); 64 | getLog().debug("Processing " + line); 65 | new MethodInjector().handle(classFile); 66 | } 67 | } catch (IOException e) { 68 | throw new MojoExecutionException("Failed to process @WithBridgeMethods", e); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /injector/src/test/client/Main.java: -------------------------------------------------------------------------------- 1 | public class Main { 2 | public static void main(String[] args) throws Exception { 3 | // invocation of void method. verify that it runs without error 4 | 5 | Foo.toggle = false; 6 | Foo.hello(); 7 | assertEquals(true, Foo.toggle); 8 | 9 | Foo.toggle = false; 10 | Foo.hello2(); 11 | assertEquals(true, Foo.toggle); 12 | 13 | Foo.toggle = false; 14 | new Foo().hello3(); 15 | assertEquals(true, Foo.toggle); 16 | 17 | Foo.toggle = false; 18 | new Foo().hello4(); 19 | assertEquals(true, Foo.toggle); 20 | 21 | Foo.unbox(); 22 | Foo.box(); 23 | 24 | Object o = new Foo().getMessage(); 25 | assertEquals(args[0],o); 26 | 27 | String n = new Foo().getString(); 28 | assertEquals(args[0],n); 29 | 30 | Object s = Foo.getStaticMessage(); 31 | assertEquals(args[0],s); 32 | 33 | Object w = Foo.methodToWiden(String.class); 34 | assertEquals(args[0],w); 35 | 36 | // using reflection to ensure that JIT isn't doing inlining 37 | check((Foo)Foo.class.newInstance(),args[0]); 38 | check((Bar)Bar.class.newInstance(),args[0]); 39 | 40 | Adapter a = new Adapter(); 41 | assertEquals(1,a.i()); 42 | assertEquals("http://kohsuke.org/",a.o()); 43 | assertEquals("http://kohsuke.org/" + args[0], a.oParam(args[0])); 44 | assertEquals("http://kohsuke.org/" + args[0] + "/" + args[0] + "/" + args[0], a.oParams(args[0], args[0], args[0])); 45 | 46 | String[] array = a.array(); 47 | assertEquals(1, array.length); 48 | assertEquals("http://kohsuke.org/", array[0]); 49 | 50 | new Adapter.SomeClass().someMethod(); 51 | 52 | assertEquals(1,a.l()); 53 | } 54 | 55 | private static void assertEquals(Object expected, Object actual) { 56 | System.out.println("We got "+actual+", expecting "+expected); 57 | if (!actual.equals(expected)) { 58 | System.exit(1); 59 | } 60 | } 61 | 62 | private static void check(IFoo f, String expected) { 63 | Object o = f.getMessage(); 64 | assertEquals(expected,o); 65 | 66 | String n = f.getString(); 67 | assertEquals(expected,n); 68 | } 69 | 70 | private static void check(IBar f, String expected) { 71 | Object o = f.narrow(); 72 | assertEquals(expected,o); 73 | 74 | String n = f.widen(); 75 | assertEquals(expected,n); 76 | 77 | String u = f.adapter(); 78 | assertEquals("http://example.com/", u); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /injector/src/test/java/com/infradna/tool/bridge_method_injector/Junit4TestsRanTest.java: -------------------------------------------------------------------------------- 1 | package com.infradna.tool.bridge_method_injector; 2 | 3 | import org.junit.Test; 4 | 5 | public class Junit4TestsRanTest { 6 | 7 | @Test 8 | public void anything() { 9 | /* 10 | * Intentionally blank. We just want a test that runs with JUnit so that buildPlugin() works 11 | * in the Jenkinsfile. 12 | */ 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /injector/src/test/synthetics/A.java: -------------------------------------------------------------------------------- 1 | public class A { 2 | public Object getProperty() { 3 | return null; 4 | } 5 | } -------------------------------------------------------------------------------- /injector/src/test/synthetics/B.java: -------------------------------------------------------------------------------- 1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods; 2 | 3 | public class B extends A { 4 | 5 | @WithBridgeMethods(value=String.class,castRequired=true) 6 | public CharSequence getProperty() { 7 | return null; 8 | } 9 | } -------------------------------------------------------------------------------- /injector/src/test/v1/Adapter.java: -------------------------------------------------------------------------------- 1 | public class Adapter { 2 | public int i() { return 1; } 3 | public String o() { return "http://kohsuke.org/"; } 4 | public String[] array() { 5 | return new String[]{ "http://kohsuke.org/" }; 6 | } 7 | 8 | public String oParam(String path) { 9 | return "http://kohsuke.org/" + path; 10 | } 11 | 12 | public String oParams(String path1, String path2, String path3) { 13 | return "http://kohsuke.org/" + path1 + "/" + path2 + "/" + path3; 14 | } 15 | 16 | // Just making sure we do not barf on Java 8 constructs: 17 | interface SomeInterface { 18 | default void someMethod() {} 19 | } 20 | static class SomeClass implements SomeInterface { 21 | @Override 22 | public void someMethod() { 23 | SomeInterface.super.someMethod(); 24 | } 25 | } 26 | 27 | // we will evolve this from int -> long 28 | public int l() { return 1; } 29 | } 30 | -------------------------------------------------------------------------------- /injector/src/test/v1/Bar.java: -------------------------------------------------------------------------------- 1 | public class Bar implements IBar { 2 | public String widen() { return "foo"; } 3 | public Object narrow() { return "foo"; } 4 | public String adapter() { return "http://example.com/"; } 5 | } -------------------------------------------------------------------------------- /injector/src/test/v1/Foo.java: -------------------------------------------------------------------------------- 1 | public class Foo implements IFoo { 2 | /** 3 | * Testing narrowing. In v2, we'll narrow this to return the String type. This is type safe. 4 | */ 5 | public Object getMessage() { 6 | return "foo"; 7 | } 8 | 9 | /** 10 | * Testing widening. In v2, we'll widen this to Object. Potentially type unsafe. 11 | */ 12 | public String getString() { 13 | return "foo"; 14 | } 15 | 16 | public static Object getStaticMessage() { 17 | return "foo"; 18 | } 19 | 20 | public static T methodToWiden(Class clazz) { 21 | return clazz.cast("foo"); 22 | } 23 | 24 | static boolean toggle; 25 | 26 | public static void hello() { 27 | toggle = true; 28 | } 29 | 30 | public static void hello2() { 31 | toggle = true; 32 | } 33 | 34 | public void hello3() { 35 | toggle = true; 36 | } 37 | 38 | public void hello4() { 39 | toggle = true; 40 | } 41 | 42 | public static int unbox() {return Integer.MIN_VALUE;} 43 | 44 | public static int box() {return Integer.MAX_VALUE;} 45 | } -------------------------------------------------------------------------------- /injector/src/test/v1/IBar.java: -------------------------------------------------------------------------------- 1 | public interface IBar { 2 | String widen(); 3 | Object narrow(); 4 | String adapter(); 5 | } -------------------------------------------------------------------------------- /injector/src/test/v1/IFoo.java: -------------------------------------------------------------------------------- 1 | public interface IFoo { 2 | public Object getMessage(); 3 | public String getString(); 4 | } -------------------------------------------------------------------------------- /injector/src/test/v2/Adapter.java: -------------------------------------------------------------------------------- 1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | 6 | public class Adapter { 7 | @WithBridgeMethods(value=int.class, adapterMethod="_i") 8 | public String i() { return "1"; } 9 | 10 | private Object _i(String o, Class type) { 11 | return Integer.parseInt(o); 12 | } 13 | 14 | @WithBridgeMethods(value=String.class, adapterMethod="_o") 15 | URL o() throws IOException { return new URL("http://kohsuke.org/"); } 16 | 17 | private Object _o(URL o, Class type) { 18 | return o.toString(); 19 | } 20 | 21 | @WithBridgeMethods(value = String.class, adapterMethod="_o") 22 | public URL oParam(String path) throws IOException { 23 | return new URL("http://kohsuke.org/" + path); 24 | } 25 | 26 | @WithBridgeMethods(value = String.class, adapterMethod="_o") 27 | public URL oParams(String path1, String path2, String path3) throws IOException { 28 | return new URL("http://kohsuke.org/" + path1 + "/" + path2 + "/" + path3); 29 | } 30 | 31 | @WithBridgeMethods(value = String[].class, adapterMethod = "_array") 32 | URL[] array() throws IOException { 33 | return new URL[]{ new URL("http://kohsuke.org/") }; 34 | } 35 | 36 | private Object _array(URL[] array, Class type) { 37 | String[] result = new String[array.length]; 38 | for (int i = 0; i < result.length; i++) { 39 | result[i] = array[i].toString(); 40 | } 41 | return result; 42 | } 43 | 44 | interface SomeInterface { 45 | default void someMethod() {} 46 | } 47 | static class SomeClass implements SomeInterface { 48 | @Override 49 | public void someMethod() { 50 | SomeInterface.super.someMethod(); 51 | } 52 | } 53 | 54 | @WithBridgeMethods(value=int.class, adapterMethod="l2i") 55 | public long l() { return 1L; } 56 | 57 | private Object l2i(long v, Class type) { return (int)v; } 58 | } -------------------------------------------------------------------------------- /injector/src/test/v2/Bar.java: -------------------------------------------------------------------------------- 1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods; 2 | import java.io.UncheckedIOException; 3 | import java.net.MalformedURLException; 4 | import java.net.URL; 5 | 6 | public class Bar implements IBar { 7 | @WithBridgeMethods(value = String.class, castRequired = true) 8 | public Object widen() { return "bar"; } 9 | 10 | @WithBridgeMethods(Object.class) 11 | public String narrow() { return "bar"; } 12 | 13 | @WithBridgeMethods(value = String.class, adapterMethod = "convert") 14 | public URL adapter() { 15 | try { 16 | return new URL("http://example.com/"); 17 | } catch (MalformedURLException e) { 18 | throw new UncheckedIOException(e); 19 | } 20 | } 21 | 22 | private Object convert(URL url, Class type) { 23 | return url.toString(); 24 | } 25 | } -------------------------------------------------------------------------------- /injector/src/test/v2/Foo.java: -------------------------------------------------------------------------------- 1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods; 2 | 3 | public class Foo implements IFoo { 4 | @WithBridgeMethods(Object.class) 5 | public String getMessage() { 6 | return "bar"; 7 | } 8 | 9 | /** 10 | * Widening String to Object. 11 | */ 12 | @WithBridgeMethods(value=String.class,castRequired=true) 13 | public Object getString() { 14 | return "bar"; 15 | } 16 | 17 | @WithBridgeMethods(Object.class) 18 | public static String getStaticMessage() { 19 | return "bar"; 20 | } 21 | 22 | @WithBridgeMethods(value=String.class, castRequired=true) 23 | public static T methodToWiden(Class clazz) { 24 | return clazz.cast("bar"); 25 | } 26 | 27 | static boolean toggle; 28 | 29 | @WithBridgeMethods(void.class) 30 | public static boolean hello() { 31 | toggle = true; 32 | return true; 33 | } 34 | 35 | @WithBridgeMethods(void.class) 36 | public static String hello2() { 37 | toggle = true; 38 | return "hello2"; 39 | } 40 | 41 | @WithBridgeMethods(void.class) 42 | public boolean hello3() { 43 | toggle = true; 44 | return true; 45 | } 46 | 47 | @WithBridgeMethods(void.class) 48 | public String hello4() { 49 | toggle = true; 50 | return "hello4"; 51 | } 52 | 53 | @WithBridgeMethods(value=int.class, castRequired=true) 54 | public static Integer unbox() { 55 | return Integer.MIN_VALUE; 56 | } 57 | 58 | @WithBridgeMethods(value=Integer.class) 59 | public static int box() { 60 | return Integer.MAX_VALUE; 61 | } 62 | } -------------------------------------------------------------------------------- /injector/src/test/v2/IBar.java: -------------------------------------------------------------------------------- 1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods; 2 | import java.net.URL; 3 | 4 | public interface IBar { 5 | @WithBridgeMethods(value=String.class,castRequired=true) 6 | Object widen(); 7 | @WithBridgeMethods(Object.class) 8 | String narrow(); 9 | @WithBridgeMethods(String.class) 10 | URL adapter(); 11 | } -------------------------------------------------------------------------------- /injector/src/test/v2/IFoo.java: -------------------------------------------------------------------------------- 1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods; 2 | 3 | public interface IFoo { 4 | @WithBridgeMethods(Object.class) 5 | public String getMessage(); 6 | 7 | @WithBridgeMethods(value=String.class,castRequired=true) 8 | public Object getString(); 9 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci 6 | jenkins 7 | 1.132 8 | 9 | 10 | 11 | com.infradna.tool 12 | bridge-method-injector-parent 13 | ${revision}${changelist} 14 | pom 15 | Bridge Method Injection Parent POM 16 | Evolve your classes without breaking compatibility 17 | https://github.com/jenkinsci/bridge-method-injector 18 | 19 | 20 | 21 | MIT License 22 | https://opensource.org/licenses/MIT 23 | repository 24 | 25 | 26 | 27 | 28 | 29 | kohsuke 30 | Kohsuke Kawaguchi 31 | 32 | 33 | 34 | 35 | annotation 36 | injector 37 | 38 | 39 | 40 | scm:git:https://github.com/${gitHubRepo}.git 41 | scm:git:git@github.com:${gitHubRepo}.git 42 | ${scmTag} 43 | https://github.com/${gitHubRepo} 44 | 45 | 46 | 47 | 1.32 48 | -SNAPSHOT 49 | jenkinsci/bridge-method-injector 50 | false 51 | 52 | 53 | 54 | 55 | repo.jenkins-ci.org 56 | https://repo.jenkins-ci.org/public/ 57 | 58 | 59 | 60 | 61 | 62 | repo.jenkins-ci.org 63 | https://repo.jenkins-ci.org/public/ 64 | 65 | 66 | 67 | --------------------------------------------------------------------------------