Skip to content

Commit

Permalink
Merge pull request #4065 from square/jw.mr-jars.2024-01-26
Browse files Browse the repository at this point in the history
Clean up `Platform` to be more maintainable
  • Loading branch information
JakeWharton authored Jan 29, 2024
2 parents b9fe70a + aa593fc commit 2b2c108
Show file tree
Hide file tree
Showing 31 changed files with 380 additions and 370 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
59 changes: 31 additions & 28 deletions retrofit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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'
}
}
6 changes: 6 additions & 0 deletions retrofit/java-test/README.md
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 34 additions & 0 deletions retrofit/java-test/build.gradle
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion retrofit/kotlin-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ dependencies {
testImplementation libs.junit
testImplementation libs.truth
testImplementation libs.mockwebserver
testImplementation libs.kotlin.stdLib
testImplementation libs.kotlinCoroutines
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
29 changes: 29 additions & 0 deletions retrofit/src/main/java/retrofit2/AndroidMainExecutor.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
52 changes: 52 additions & 0 deletions retrofit/src/main/java/retrofit2/BuiltInFactories.java
Original file line number Diff line number Diff line change
@@ -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<? extends CallAdapter.Factory> createDefaultCallAdapterFactories(
@Nullable Executor callbackExecutor) {
return singletonList(new DefaultCallAdapterFactory(callbackExecutor));
}

List<? extends Converter.Factory> createDefaultConverterFactories() {
return emptyList();
}

@TargetApi(24)
static final class Java8 extends BuiltInFactories {
@Override
List<? extends CallAdapter.Factory> createDefaultCallAdapterFactories(
@Nullable Executor callbackExecutor) {
return asList(
new CompletableFutureCallAdapterFactory(),
new DefaultCallAdapterFactory(callbackExecutor));
}

@Override
List<? extends Converter.Factory> createDefaultConverterFactories() {
return singletonList(new OptionalConverterFactory());
}
}
}
54 changes: 54 additions & 0 deletions retrofit/src/main/java/retrofit2/DefaultMethodSupport.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
53 changes: 53 additions & 0 deletions retrofit/src/main/java/retrofit2/DefaultMethodSupportJvm.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Note: This class has multi-release jar variants for newer versions of Java.
*/
class DefaultMethodSupportJvm extends DefaultMethodSupport {
private @Nullable Constructor<Lookup> lookupConstructor;

@Override
boolean isDefaultMethod(Method method) {
return method.isDefault();
}

@Override
Object invokeDefaultMethod(
Method method, Class<?> declaringClass, Object proxy, @Nullable Object[] args)
throws Throwable {
Constructor<Lookup> 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);
}
}
Loading

0 comments on commit 2b2c108

Please sign in to comment.