diff --git a/build.gradle b/build.gradle index f78d171d54..acccdbaa2e 100644 --- a/build.gradle +++ b/build.gradle @@ -90,7 +90,7 @@ subprojects { googleJavaFormat(libs.googleJavaFormat.get().version) .formatJavadoc(false) removeUnusedImports() - target 'src/*/java/**/*.java' + target 'src/*/java*/**/*.java' } kotlin { ktlint(libs.ktlint.get().version) diff --git a/retrofit/build.gradle b/retrofit/build.gradle index b1c6b0de06..c58957cba1 100644 --- a/retrofit/build.gradle +++ b/retrofit/build.gradle @@ -2,6 +2,35 @@ apply plugin: 'java-library' apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'com.vanniktech.maven.publish' +def addMultiReleaseSourceSet(int version) { + def sourceSet = sourceSets.create("java$version") + sourceSet.java.srcDir("src/main/java$version") + + // Propagate dependencies to be visible to this version's source set. + configurations.getByName("java${version}Implementation").extendsFrom(configurations.getByName('implementation')) + configurations.getByName("java${version}Api").extendsFrom(configurations.getByName('api')) + configurations.getByName("java${version}CompileOnly").extendsFrom(configurations.getByName('compileOnly')) + + // Allow types in the main source set to be visible to this version's source set. + dependencies.add("java${version}Implementation", sourceSets.getByName("main").output) + + tasks.named("compileJava${version}Java", JavaCompile) { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(version) + vendor = JvmVendorSpec.AZUL + } + } + + tasks.named('jar', Jar) { + from(sourceSet.output) { + into("META-INF/versions/$version") + } + } +} + +addMultiReleaseSourceSet(14) +addMultiReleaseSourceSet(16) + dependencies { api libs.okhttp @@ -11,37 +40,11 @@ dependencies { compileOnly libs.animalSnifferAnnotations compileOnly libs.findBugsAnnotations - - testImplementation projects.retrofit.testHelpers - testImplementation libs.junit - testImplementation libs.truth - testImplementation libs.guava - testImplementation libs.mockwebserver } jar { manifest { - attributes 'Automatic-Module-Name': 'retrofit2' - } -} - -// Create a test task for each supported JDK. -(8..21).each { majorVersion -> - def jdkTest = tasks.register("testJdk$majorVersion", Test) { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(majorVersion) - vendor = JvmVendorSpec.AZUL - } - - description = "Runs the test suite on JDK $majorVersion" - group = LifecycleBasePlugin.VERIFICATION_GROUP - - // Copy inputs from normal Test task. - def testTask = tasks.getByName("test") - classpath = testTask.classpath - testClassesDirs = testTask.testClassesDirs - } - tasks.named("check").configure { - dependsOn(jdkTest) + attributes 'Automatic-Module-Name': 'retrofit2' + attributes 'Multi-Release': 'true' } } diff --git a/retrofit/java-test/README.md b/retrofit/java-test/README.md new file mode 100644 index 0000000000..eced926fcf --- /dev/null +++ b/retrofit/java-test/README.md @@ -0,0 +1,6 @@ +# Retrofit Java Tests + +These are in a separate module for two reasons: + +- It ensures optional dependencies (Kotlin stuff) are completely absent. +- It uses the multi-release jar on the classpath rather than only the classes folder. diff --git a/retrofit/java-test/build.gradle b/retrofit/java-test/build.gradle new file mode 100644 index 0000000000..e4928f1a02 --- /dev/null +++ b/retrofit/java-test/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'java-library' + +dependencies { + testImplementation projects.retrofit + testImplementation projects.retrofit.testHelpers + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.guava + testImplementation libs.mockwebserver +} + +// Create a test task for each supported JDK. +(8..21).each { majorVersion -> + def jdkTest = tasks.register("testJdk$majorVersion", Test) { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(majorVersion) + vendor = JvmVendorSpec.AZUL + } + + description = "Runs the test suite on JDK $majorVersion" + group = LifecycleBasePlugin.VERIFICATION_GROUP + + // Copy inputs from normal Test task. + def testTask = tasks.getByName("test") + classpath = testTask.classpath + testClassesDirs = testTask.testClassesDirs + } + tasks.named("check").configure { + dependsOn(jdkTest) + } +} + +// We don't need the built-in task which uses Gradle's JVM given the above variants. +tasks.getByName('test').enabled = false diff --git a/retrofit/src/test/java/retrofit2/AnnotationArraySubject.java b/retrofit/java-test/src/test/java/retrofit2/AnnotationArraySubject.java similarity index 100% rename from retrofit/src/test/java/retrofit2/AnnotationArraySubject.java rename to retrofit/java-test/src/test/java/retrofit2/AnnotationArraySubject.java diff --git a/retrofit/src/test/java/retrofit2/CallAdapterTest.java b/retrofit/java-test/src/test/java/retrofit2/CallAdapterTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/CallAdapterTest.java rename to retrofit/java-test/src/test/java/retrofit2/CallAdapterTest.java diff --git a/retrofit/src/test/java/retrofit2/CallTest.java b/retrofit/java-test/src/test/java/retrofit2/CallTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/CallTest.java rename to retrofit/java-test/src/test/java/retrofit2/CallTest.java diff --git a/retrofit/src/test/java/retrofit2/CompletableFutureCallAdapterFactoryTest.java b/retrofit/java-test/src/test/java/retrofit2/CompletableFutureCallAdapterFactoryTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/CompletableFutureCallAdapterFactoryTest.java rename to retrofit/java-test/src/test/java/retrofit2/CompletableFutureCallAdapterFactoryTest.java diff --git a/retrofit/src/test/java/retrofit2/CompletableFutureTest.java b/retrofit/java-test/src/test/java/retrofit2/CompletableFutureTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/CompletableFutureTest.java rename to retrofit/java-test/src/test/java/retrofit2/CompletableFutureTest.java diff --git a/retrofit/src/test/java/retrofit2/DefaultCallAdapterFactoryTest.java b/retrofit/java-test/src/test/java/retrofit2/DefaultCallAdapterFactoryTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/DefaultCallAdapterFactoryTest.java rename to retrofit/java-test/src/test/java/retrofit2/DefaultCallAdapterFactoryTest.java diff --git a/retrofit/src/test/java/retrofit2/DefaultMethodsTest.java b/retrofit/java-test/src/test/java/retrofit2/DefaultMethodsTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/DefaultMethodsTest.java rename to retrofit/java-test/src/test/java/retrofit2/DefaultMethodsTest.java diff --git a/retrofit/src/test/java/retrofit2/HttpExceptionTest.java b/retrofit/java-test/src/test/java/retrofit2/HttpExceptionTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/HttpExceptionTest.java rename to retrofit/java-test/src/test/java/retrofit2/HttpExceptionTest.java diff --git a/retrofit/src/test/java/retrofit2/InvocationTest.java b/retrofit/java-test/src/test/java/retrofit2/InvocationTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/InvocationTest.java rename to retrofit/java-test/src/test/java/retrofit2/InvocationTest.java diff --git a/retrofit/src/test/java/retrofit2/Java8DefaultStaticMethodsInValidationTest.java b/retrofit/java-test/src/test/java/retrofit2/Java8DefaultStaticMethodsInValidationTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/Java8DefaultStaticMethodsInValidationTest.java rename to retrofit/java-test/src/test/java/retrofit2/Java8DefaultStaticMethodsInValidationTest.java diff --git a/retrofit/src/test/java/retrofit2/NonFatalError.java b/retrofit/java-test/src/test/java/retrofit2/NonFatalError.java similarity index 100% rename from retrofit/src/test/java/retrofit2/NonFatalError.java rename to retrofit/java-test/src/test/java/retrofit2/NonFatalError.java diff --git a/retrofit/src/test/java/retrofit2/OptionalConverterFactoryTest.java b/retrofit/java-test/src/test/java/retrofit2/OptionalConverterFactoryTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/OptionalConverterFactoryTest.java rename to retrofit/java-test/src/test/java/retrofit2/OptionalConverterFactoryTest.java diff --git a/retrofit/src/test/java/retrofit2/RequestFactoryBuilderTest.java b/retrofit/java-test/src/test/java/retrofit2/RequestFactoryBuilderTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/RequestFactoryBuilderTest.java rename to retrofit/java-test/src/test/java/retrofit2/RequestFactoryBuilderTest.java diff --git a/retrofit/src/test/java/retrofit2/RequestFactoryTest.java b/retrofit/java-test/src/test/java/retrofit2/RequestFactoryTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/RequestFactoryTest.java rename to retrofit/java-test/src/test/java/retrofit2/RequestFactoryTest.java diff --git a/retrofit/src/test/java/retrofit2/ResponseTest.java b/retrofit/java-test/src/test/java/retrofit2/ResponseTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/ResponseTest.java rename to retrofit/java-test/src/test/java/retrofit2/ResponseTest.java diff --git a/retrofit/src/test/java/retrofit2/RetrofitTest.java b/retrofit/java-test/src/test/java/retrofit2/RetrofitTest.java similarity index 100% rename from retrofit/src/test/java/retrofit2/RetrofitTest.java rename to retrofit/java-test/src/test/java/retrofit2/RetrofitTest.java diff --git a/retrofit/kotlin-test/build.gradle b/retrofit/kotlin-test/build.gradle index bc6e133e48..6c25cb1e95 100644 --- a/retrofit/kotlin-test/build.gradle +++ b/retrofit/kotlin-test/build.gradle @@ -6,6 +6,5 @@ dependencies { testImplementation libs.junit testImplementation libs.truth testImplementation libs.mockwebserver - testImplementation libs.kotlin.stdLib testImplementation libs.kotlinCoroutines } diff --git a/retrofit/robovm-test/src/main/java/retrofit2/RoboVmPlatformTest.java b/retrofit/robovm-test/src/main/java/retrofit2/RoboVmPlatformTest.java index 6e312e8269..1b09bd48d6 100644 --- a/retrofit/robovm-test/src/main/java/retrofit2/RoboVmPlatformTest.java +++ b/retrofit/robovm-test/src/main/java/retrofit2/RoboVmPlatformTest.java @@ -17,8 +17,12 @@ public final class RoboVmPlatformTest { public static void main(String[] args) { - Platform platform = Platform.get(); - if (platform.createDefaultCallAdapterFactories(null).size() > 1) { + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://example.com") + .callFactory(c -> { throw new AssertionError(); }) + .build(); + + if (retrofit.callAdapterFactories().size() > 1) { // Everyone gets the callback executor adapter. If RoboVM was correctly detected it will NOT // get the Java 8-supporting CompletableFuture call adapter factory. System.exit(1); diff --git a/retrofit/src/main/java/retrofit2/AndroidMainExecutor.java b/retrofit/src/main/java/retrofit2/AndroidMainExecutor.java new file mode 100644 index 0000000000..c7c7448562 --- /dev/null +++ b/retrofit/src/main/java/retrofit2/AndroidMainExecutor.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package retrofit2; + +import android.os.Handler; +import android.os.Looper; +import java.util.concurrent.Executor; + +final class AndroidMainExecutor implements Executor { + private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable r) { + handler.post(r); + } +} diff --git a/retrofit/src/main/java/retrofit2/BuiltInFactories.java b/retrofit/src/main/java/retrofit2/BuiltInFactories.java new file mode 100644 index 0000000000..752dfd83bb --- /dev/null +++ b/retrofit/src/main/java/retrofit2/BuiltInFactories.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package retrofit2; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +import android.annotation.TargetApi; +import java.util.List; +import java.util.concurrent.Executor; +import javax.annotation.Nullable; + +class BuiltInFactories { + List createDefaultCallAdapterFactories( + @Nullable Executor callbackExecutor) { + return singletonList(new DefaultCallAdapterFactory(callbackExecutor)); + } + + List createDefaultConverterFactories() { + return emptyList(); + } + + @TargetApi(24) + static final class Java8 extends BuiltInFactories { + @Override + List createDefaultCallAdapterFactories( + @Nullable Executor callbackExecutor) { + return asList( + new CompletableFutureCallAdapterFactory(), + new DefaultCallAdapterFactory(callbackExecutor)); + } + + @Override + List createDefaultConverterFactories() { + return singletonList(new OptionalConverterFactory()); + } + } +} diff --git a/retrofit/src/main/java/retrofit2/DefaultMethodSupport.java b/retrofit/src/main/java/retrofit2/DefaultMethodSupport.java new file mode 100644 index 0000000000..5aa09ac122 --- /dev/null +++ b/retrofit/src/main/java/retrofit2/DefaultMethodSupport.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package retrofit2; + +import android.annotation.TargetApi; +import android.os.Build; +import java.lang.reflect.Method; +import javax.annotation.Nullable; + +class DefaultMethodSupport { + boolean isDefaultMethod(Method method) { + return false; + } + + @Nullable + Object invokeDefaultMethod( + Method method, Class declaringClass, Object proxy, @Nullable Object[] args) + throws Throwable { + throw new AssertionError(); + } + + /** + * Android does not support MR jars, so this extends the baseline JVM support which targets + * Java 8 APIs. Default methods and the reflection API to detect them were added to API 24 + * as part of the initial Java 8 set. MethodHandle, our means of invoking the default method + * through the proxy, was not added until API 26. + */ + @TargetApi(24) + static final class Android24 extends DefaultMethodSupportJvm { + @Override + Object invokeDefaultMethod( + Method method, Class declaringClass, Object proxy, @Nullable Object[] args) + throws Throwable { + if (Build.VERSION.SDK_INT < 26) { + throw new UnsupportedOperationException( + "Calling default methods on API 24 and 25 is not supported"); + } + return super.invokeDefaultMethod(method, declaringClass, proxy, args); + } + } +} diff --git a/retrofit/src/main/java/retrofit2/DefaultMethodSupportJvm.java b/retrofit/src/main/java/retrofit2/DefaultMethodSupportJvm.java new file mode 100644 index 0000000000..ed696977c3 --- /dev/null +++ b/retrofit/src/main/java/retrofit2/DefaultMethodSupportJvm.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package retrofit2; + +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import org.jetbrains.annotations.Nullable; + +/** + * From Java 8 to Java 13, the only way to invoke a default method on a proxied interface is by + * reflectively creating a trusted {@link Lookup} to invoke a method handle. + *

+ * Note: This class has multi-release jar variants for newer versions of Java. + */ +class DefaultMethodSupportJvm extends DefaultMethodSupport { + private @Nullable Constructor lookupConstructor; + + @Override + boolean isDefaultMethod(Method method) { + return method.isDefault(); + } + + @Override + Object invokeDefaultMethod( + Method method, Class declaringClass, Object proxy, @Nullable Object[] args) + throws Throwable { + Constructor lookupConstructor = this.lookupConstructor; + if (lookupConstructor == null) { + lookupConstructor = Lookup.class.getDeclaredConstructor(Class.class, int.class); + lookupConstructor.setAccessible(true); + this.lookupConstructor = lookupConstructor; + } + return lookupConstructor + .newInstance(declaringClass, -1 /* trusted */) + .unreflectSpecial(method, declaringClass) + .bindTo(proxy) + .invokeWithArguments(args); + } +} diff --git a/retrofit/src/main/java/retrofit2/Platform.java b/retrofit/src/main/java/retrofit2/Platform.java index d67f7a5d00..113a4df499 100644 --- a/retrofit/src/main/java/retrofit2/Platform.java +++ b/retrofit/src/main/java/retrofit2/Platform.java @@ -15,347 +15,42 @@ */ package retrofit2; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; +import static android.os.Build.VERSION.SDK_INT; -import android.annotation.TargetApi; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodHandles.Lookup; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.List; import java.util.concurrent.Executor; import javax.annotation.Nullable; -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; -abstract class Platform { - private static final Platform PLATFORM = createPlatform(); +final class Platform { + static final @Nullable Executor callbackExecutor; + static final DefaultMethodSupport defaultMethodSupport; + static final BuiltInFactories builtInFactories; - static Platform get() { - return PLATFORM; - } - - private static Platform createPlatform() { + static { switch (System.getProperty("java.vm.name")) { case "Dalvik": - if (Android24.isSupported()) { - return new Android24(); + callbackExecutor = new AndroidMainExecutor(); + if (SDK_INT >= 24) { + defaultMethodSupport = new DefaultMethodSupport.Android24(); + builtInFactories = new BuiltInFactories.Java8(); + } else { + defaultMethodSupport = new DefaultMethodSupport(); + builtInFactories = new BuiltInFactories(); } - return new Android21(); + break; case "RoboVM": - return new RoboVm(); + callbackExecutor = null; + defaultMethodSupport = new DefaultMethodSupport(); + builtInFactories = new BuiltInFactories(); + break; default: - if (Java16.isSupported()) { - return new Java16(); - } - if (Java14.isSupported()) { - return new Java14(); - } - return new Java8(); - } - } - - abstract @Nullable Executor defaultCallbackExecutor(); - - abstract List createDefaultCallAdapterFactories( - @Nullable Executor callbackExecutor); - - abstract List createDefaultConverterFactories(); - - abstract boolean isDefaultMethod(Method method); - - abstract @Nullable Object invokeDefaultMethod( - Method method, Class declaringClass, Object proxy, Object... args) throws Throwable; - - private static final class Android21 extends Platform { - @Override - boolean isDefaultMethod(Method method) { - return false; - } - - @Nullable - @Override - Object invokeDefaultMethod( - Method method, Class declaringClass, Object proxy, Object... args) { - throw new AssertionError(); - } - - @Override - Executor defaultCallbackExecutor() { - return MainThreadExecutor.INSTANCE; - } - - @Override - List createDefaultCallAdapterFactories( - @Nullable Executor callbackExecutor) { - return singletonList(new DefaultCallAdapterFactory(callbackExecutor)); - } - - @Override - List createDefaultConverterFactories() { - return emptyList(); - } - } - - @IgnoreJRERequirement // Only used on Android API 24+ - @TargetApi(24) - private static final class Android24 extends Platform { - static boolean isSupported() { - return Build.VERSION.SDK_INT >= 24; - } - - private @Nullable Constructor lookupConstructor; - - @Override - Executor defaultCallbackExecutor() { - return MainThreadExecutor.INSTANCE; - } - - @Override - List createDefaultCallAdapterFactories( - @Nullable Executor callbackExecutor) { - return asList( - new CompletableFutureCallAdapterFactory(), - new DefaultCallAdapterFactory(callbackExecutor)); - } - - @Override - List createDefaultConverterFactories() { - return singletonList(new OptionalConverterFactory()); - } - - @Override - public boolean isDefaultMethod(Method method) { - return method.isDefault(); - } - - @Nullable - @Override - public Object invokeDefaultMethod( - Method method, Class declaringClass, Object proxy, Object... args) throws Throwable { - if (Build.VERSION.SDK_INT < 26) { - throw new UnsupportedOperationException( - "Calling default methods on API 24 and 25 is not supported"); - } - Constructor lookupConstructor = this.lookupConstructor; - if (lookupConstructor == null) { - lookupConstructor = Lookup.class.getDeclaredConstructor(Class.class, int.class); - lookupConstructor.setAccessible(true); - this.lookupConstructor = lookupConstructor; - } - return lookupConstructor - .newInstance(declaringClass, -1 /* trusted */) - .unreflectSpecial(method, declaringClass) - .bindTo(proxy) - .invokeWithArguments(args); - } - } - - private static final class RoboVm extends Platform { - @Nullable - @Override - Executor defaultCallbackExecutor() { - return null; - } - - @Override - List createDefaultCallAdapterFactories( - @Nullable Executor callbackExecutor) { - return singletonList(new DefaultCallAdapterFactory(callbackExecutor)); - } - - @Override - List createDefaultConverterFactories() { - return emptyList(); - } - - @Override - boolean isDefaultMethod(Method method) { - return false; - } - - @Nullable - @Override - Object invokeDefaultMethod( - Method method, Class declaringClass, Object proxy, Object... args) { - throw new AssertionError(); - } - } - - @IgnoreJRERequirement // Only used on JVM and Java 8 is the minimum-supported version. - @SuppressWarnings("NewApi") // Not used for Android. - private static final class Java8 extends Platform { - private @Nullable Constructor lookupConstructor; - - @Nullable - @Override - Executor defaultCallbackExecutor() { - return null; - } - - @Override - List createDefaultCallAdapterFactories( - @Nullable Executor callbackExecutor) { - return asList( - new CompletableFutureCallAdapterFactory(), - new DefaultCallAdapterFactory(callbackExecutor)); - } - - @Override - List createDefaultConverterFactories() { - return singletonList(new OptionalConverterFactory()); - } - - @Override - public boolean isDefaultMethod(Method method) { - return method.isDefault(); - } - - @Override - public @Nullable Object invokeDefaultMethod( - Method method, Class declaringClass, Object proxy, Object... args) throws Throwable { - Constructor lookupConstructor = this.lookupConstructor; - if (lookupConstructor == null) { - lookupConstructor = Lookup.class.getDeclaredConstructor(Class.class, int.class); - lookupConstructor.setAccessible(true); - this.lookupConstructor = lookupConstructor; - } - return lookupConstructor - .newInstance(declaringClass, -1 /* trusted */) - .unreflectSpecial(method, declaringClass) - .bindTo(proxy) - .invokeWithArguments(args); - } - } - - /** - * Java 14 allows a regular lookup to succeed for invoking default methods. - * - *

https://bugs.openjdk.java.net/browse/JDK-8209005 - */ - @IgnoreJRERequirement // Only used on JVM and Java 14. - @SuppressWarnings("NewApi") // Not used for Android. - private static final class Java14 extends Platform { - static boolean isSupported() { - try { - Object version = Runtime.class.getMethod("version").invoke(null); - Integer feature = (Integer) version.getClass().getMethod("feature").invoke(version); - return feature >= 14; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException ignored) { - return false; - } - } - - @Nullable - @Override - Executor defaultCallbackExecutor() { - return null; - } - - @Override - List createDefaultCallAdapterFactories( - @Nullable Executor callbackExecutor) { - return asList( - new CompletableFutureCallAdapterFactory(), - new DefaultCallAdapterFactory(callbackExecutor)); - } - - @Override - List createDefaultConverterFactories() { - return singletonList(new OptionalConverterFactory()); - } - - @Override - public boolean isDefaultMethod(Method method) { - return method.isDefault(); - } - - @Nullable - @Override - public Object invokeDefaultMethod( - Method method, Class declaringClass, Object proxy, Object... args) throws Throwable { - return MethodHandles.lookup() - .unreflectSpecial(method, declaringClass) - .bindTo(proxy) - .invokeWithArguments(args); - } - } - - /** - * Java 16 has a supported public API for invoking default methods on a proxy. We invoke it - * reflectively because we cannot compile against the API directly. - */ - @IgnoreJRERequirement // Only used on JVM and Java 16. - @SuppressWarnings("NewApi") // Not used for Android. - private static final class Java16 extends Platform { - static boolean isSupported() { - try { - Object version = Runtime.class.getMethod("version").invoke(null); - Integer feature = (Integer) version.getClass().getMethod("feature").invoke(version); - return feature >= 16; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException ignored) { - return false; - } - } - - private @Nullable Method invokeDefaultMethod; - - @Nullable - @Override - Executor defaultCallbackExecutor() { - return null; - } - - @Override - List createDefaultCallAdapterFactories( - @Nullable Executor callbackExecutor) { - return asList( - new CompletableFutureCallAdapterFactory(), - new DefaultCallAdapterFactory(callbackExecutor)); - } - - @Override - List createDefaultConverterFactories() { - return singletonList(new OptionalConverterFactory()); - } - - @Override - public boolean isDefaultMethod(Method method) { - return method.isDefault(); - } - - @SuppressWarnings("JavaReflectionMemberAccess") // Only available on Java 16, as we expect. - @Nullable - @Override - public Object invokeDefaultMethod( - Method method, Class declaringClass, Object proxy, Object... args) throws Throwable { - Method invokeDefaultMethod = this.invokeDefaultMethod; - if (invokeDefaultMethod == null) { - invokeDefaultMethod = - InvocationHandler.class.getMethod( - "invokeDefault", Object.class, Method.class, Object[].class); - this.invokeDefaultMethod = invokeDefaultMethod; - } - return invokeDefaultMethod.invoke(null, proxy, method, args); + callbackExecutor = null; + defaultMethodSupport = new DefaultMethodSupportJvm(); + builtInFactories = new BuiltInFactories.Java8(); + break; } } - private static final class MainThreadExecutor implements Executor { - static final Executor INSTANCE = new MainThreadExecutor(); - - private final Handler handler = new Handler(Looper.getMainLooper()); - - @Override - public void execute(Runnable r) { - handler.post(r); - } - } + private Platform() {} } diff --git a/retrofit/src/main/java/retrofit2/Retrofit.java b/retrofit/src/main/java/retrofit2/Retrofit.java index e6dd8690ce..f855534a79 100644 --- a/retrofit/src/main/java/retrofit2/Retrofit.java +++ b/retrofit/src/main/java/retrofit2/Retrofit.java @@ -171,9 +171,9 @@ public T create(final Class service) { return method.invoke(this, args); } args = args != null ? args : emptyArgs; - Platform platform = Platform.get(); - return platform.isDefaultMethod(method) - ? platform.invokeDefaultMethod(method, service, proxy, args) + DefaultMethodSupport defaultMethodSupport = Platform.defaultMethodSupport; + return defaultMethodSupport.isDefaultMethod(method) + ? defaultMethodSupport.invokeDefaultMethod(method, service, proxy, args) : loadServiceMethod(method).invoke(args); } }); @@ -200,9 +200,9 @@ private void validateServiceInterface(Class service) { } if (validateEagerly) { - Platform platform = Platform.get(); + DefaultMethodSupport defaultMethodSupport = Platform.defaultMethodSupport; for (Method method : service.getDeclaredMethods()) { - if (!platform.isDefaultMethod(method) + if (!defaultMethodSupport.isDefaultMethod(method) && !Modifier.isStatic(method.getModifiers()) && !method.isSynthetic()) { loadServiceMethod(method); @@ -656,8 +656,6 @@ public Retrofit build() { throw new IllegalStateException("Base URL required."); } - Platform platform = Platform.get(); - okhttp3.Call.Factory callFactory = this.callFactory; if (callFactory == null) { callFactory = new OkHttpClient(); @@ -665,18 +663,20 @@ public Retrofit build() { Executor callbackExecutor = this.callbackExecutor; if (callbackExecutor == null) { - callbackExecutor = platform.defaultCallbackExecutor(); + callbackExecutor = Platform.callbackExecutor; } + BuiltInFactories builtInFactories = Platform.builtInFactories; + // Make a defensive copy of the adapters and add the default Call adapter. List callAdapterFactories = new ArrayList<>(this.callAdapterFactories); List defaultCallAdapterFactories = - platform.createDefaultCallAdapterFactories(callbackExecutor); + builtInFactories.createDefaultCallAdapterFactories(callbackExecutor); callAdapterFactories.addAll(defaultCallAdapterFactories); // Make a defensive copy of the converters. List defaultConverterFactories = - platform.createDefaultConverterFactories(); + builtInFactories.createDefaultConverterFactories(); int defaultConverterFactoriesSize = defaultConverterFactories.size(); List converterFactories = new ArrayList<>(1 + this.converterFactories.size() + defaultConverterFactoriesSize); diff --git a/retrofit/src/main/java14/retrofit2/DefaultMethodSupportJvm.java b/retrofit/src/main/java14/retrofit2/DefaultMethodSupportJvm.java new file mode 100644 index 0000000000..3e59c0ed98 --- /dev/null +++ b/retrofit/src/main/java14/retrofit2/DefaultMethodSupportJvm.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package retrofit2; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import javax.annotation.Nullable; + +/** + * Java 14 allows a regular (i.e., non-trusted) lookup to succeed for invoking default methods. + *

+ * https://bugs.openjdk.java.net/browse/JDK-8209005 + */ +class DefaultMethodSupportJvm extends DefaultMethodSupport { + @Override + boolean isDefaultMethod(Method method) { + return method.isDefault(); + } + + @Override + @Nullable + Object invokeDefaultMethod( + Method method, Class declaringClass, Object proxy, @Nullable Object[] args) + throws Throwable { + return MethodHandles.lookup() + .unreflectSpecial(method, declaringClass) + .bindTo(proxy) + .invokeWithArguments(args); + } +} diff --git a/retrofit/src/main/java16/retrofit2/DefaultMethodSupportJvm.java b/retrofit/src/main/java16/retrofit2/DefaultMethodSupportJvm.java new file mode 100644 index 0000000000..09614af5b1 --- /dev/null +++ b/retrofit/src/main/java16/retrofit2/DefaultMethodSupportJvm.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package retrofit2; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import javax.annotation.Nullable; + +/** Java 16 finally has a public API for invoking default methods on a proxy. */ +class DefaultMethodSupportJvm extends DefaultMethodSupport { + @Override + boolean isDefaultMethod(Method method) { + return method.isDefault(); + } + + @Override + @Nullable + Object invokeDefaultMethod( + Method method, Class declaringClass, Object proxy, @Nullable Object[] args) + throws Throwable { + return InvocationHandler.invokeDefault(proxy, method, args); + } +} diff --git a/settings.gradle b/settings.gradle index 282ecc9a4a..a5a6b25685 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,9 @@ rootProject.name = 'retrofit-root' include ':retrofit' include ':retrofit-bom' + include ':retrofit:android-test' +include ':retrofit:java-test' include ':retrofit:kotlin-test' include ':retrofit:robovm-test' include ':retrofit:test-helpers'