Implement reflective support for Java Records (#1381)

* Standardize around JDK 8

* Update GJF to support newer JDKs

* Fix misc java 8 issues in tests

* Prepare java 16/records checking at runtime

* Implement real RecordJsonAdapter

* Spotless

* Prepare build for JDK 16+

* Fix property name for kapt

* Small cleanup

* Make FallbackEnum java-8 happy

* Remove animalsniffer

* Fix format

* Add opens for ExtendsPlatformClassWithProtectedFields

* Return null every time in shim for main tests

* Use JDK 16 + release 8 to replace animalsniffer

* Simplify accessor accessible handling

* Remove manifest attrs

* Fix typo

* Fix KCT tests + upgrade it

* Cover another

* Try explicit kotlin daemon args for java 17?

* Disable 17-ea for now until kotlin 1.5.30

* Add JsonQualifier and Json(name) tests + fix qualifiers

* Ensure constructor is accessible

* GJF it properly

* GJF 1.11

* Unwrap InvocationTargetException

* Use MethodHandle for constructor

* Use MethodHandle for accessor too

* Revert "Remove manifest attrs"

This reverts commit 3eb768fd6904bb5c979aa01c3c182e0fb9329d62.

* Proper MR jar

* *actually* fix GJF, which wasn't getting applied before

We can just enable this everywhere now since we require JDK 16 anyway

* Make IDE happy about modules access

* Fixup records tests to play nice with modules

Gotta be public

* Add complex smoke test

* Remove comment

Not a regression test in this case
This commit is contained in:
Zac Sweers
2021-08-23 12:00:06 -04:00
committed by GitHub
parent 2572c29e42
commit 95250b0359
20 changed files with 653 additions and 104 deletions

View File

@@ -10,14 +10,7 @@ jobs:
fail-fast: false
matrix:
java-version:
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
steps:
- name: Checkout
@@ -46,7 +39,7 @@ jobs:
run: ./gradlew build check --stacktrace
- name: Publish (default branch only)
if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' && matrix.java-version == '8'
if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' && matrix.java-version == '16'
run: ./gradlew uploadArchives
env:
ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}

View File

@@ -14,18 +14,9 @@
* limitations under the License.
*/
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
id("com.vanniktech.maven.publish")
id("ru.vyarus.animalsniffer")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "1.6"
}
}
dependencies {

View File

@@ -33,7 +33,6 @@ plugins {
id("com.vanniktech.maven.publish") version "0.14.2" apply false
id("org.jetbrains.dokka") version "1.4.32" apply false
id("com.diffplug.spotless") version "5.12.4"
id("ru.vyarus.animalsniffer") version "1.5.3" apply false
id("me.champeau.gradle.japicmp") version "0.2.9" apply false
}
@@ -44,8 +43,6 @@ spotless {
indentWithSpaces(2)
endWithNewline()
}
// GJF not compatible with JDK 15 yet
if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) {
val externalJavaFiles = arrayOf(
"**/ClassFactory.java",
"**/Iso8601Utils.java",
@@ -70,7 +67,7 @@ spotless {
"**/TypesTest.java"
)
val configureCommonJavaFormat: JavaExtension.() -> Unit = {
googleJavaFormat("1.7")
googleJavaFormat("1.11.0")
}
java {
configureCommonJavaFormat()
@@ -88,7 +85,6 @@ spotless {
configureCommonJavaFormat()
target(*externalJavaFiles)
}
}
kotlin {
ktlint(Dependencies.ktlintVersion).userData(mapOf("indent_size" to "2"))
target("**/*.kt")
@@ -129,15 +125,14 @@ subprojects {
// Apply with "java" instead of just "java-library" so kotlin projects get it too
pluginManager.withPlugin("java") {
configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
toolchain {
languageVersion.set(JavaLanguageVersion.of(16))
}
}
pluginManager.withPlugin("ru.vyarus.animalsniffer") {
dependencies {
"compileOnly"(Dependencies.AnimalSniffer.annotations)
"signature"(Dependencies.AnimalSniffer.java7Signature)
if (project.name != "records-tests") {
tasks.withType<JavaCompile>().configureEach {
options.release.set(8)
}
}
}
@@ -146,6 +141,7 @@ subprojects {
kotlinOptions {
@Suppress("SuspiciousCollectionReassignment")
freeCompilerArgs += listOf("-progressive")
jvmTarget = "1.8"
}
}

View File

@@ -21,11 +21,6 @@ object Dependencies {
const val ktlintVersion = "0.41.0"
const val okio = "com.squareup.okio:okio:2.10.0"
object AnimalSniffer {
const val annotations = "org.codehaus.mojo:animal-sniffer-annotations:1.16"
const val java7Signature = "org.codehaus.mojo.signature:java17:1.0@signature"
}
object AutoService {
private const val version = "1.0"
const val annotations = "com.google.auto.service:auto-service-annotations:$version"
@@ -53,7 +48,7 @@ object Dependencies {
object Testing {
const val assertj = "org.assertj:assertj-core:3.11.1"
const val compileTesting = "com.github.tschuchortdev:kotlin-compile-testing:1.4.0"
const val compileTesting = "com.github.tschuchortdev:kotlin-compile-testing:1.4.3"
const val junit = "junit:junit:4.13.2"
const val truth = "com.google.truth:truth:1.0.1"
}

View File

@@ -58,9 +58,9 @@ final class FallbackEnum {
if (!(annotation instanceof Fallback)) {
return null;
}
Class<Enum> enumType = (Class<Enum>) rawType;
Enum<?> fallback = Enum.valueOf(enumType, ((Fallback) annotation).value());
return new FallbackEnumJsonAdapter<>(enumType, fallback);
//noinspection rawtypes
return new FallbackEnumJsonAdapter<>(
(Class<? extends Enum>) rawType, ((Fallback) annotation).value());
}
};
@@ -70,9 +70,9 @@ final class FallbackEnum {
final JsonReader.Options options;
final T defaultValue;
FallbackEnumJsonAdapter(Class<T> enumType, T defaultValue) {
FallbackEnumJsonAdapter(Class<T> enumType, String fallbackName) {
this.enumType = enumType;
this.defaultValue = defaultValue;
this.defaultValue = Enum.valueOf(enumType, fallbackName);
try {
constants = enumType.getEnumConstants();
nameStrings = new String[constants.length];

View File

@@ -15,9 +15,36 @@
#
# For Dokka https://github.com/Kotlin/dokka/issues/1405
org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
kapt.includeCompileClasspath=false
# TODO move this to DSL in Kotlin 1.5.30 https://youtrack.jetbrains.com/issue/KT-44266
kotlin.daemon.jvmargs=-Dfile.encoding=UTF-8 \
--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
kapt.include.compile.classpath=false
GROUP=com.squareup.moshi
VERSION_NAME=1.13.0-SNAPSHOT

View File

@@ -27,7 +27,6 @@ plugins {
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
@Suppress("SuspiciousCollectionReassignment")
freeCompilerArgs += listOf(
"-Xopt-in=com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview"
@@ -35,10 +34,20 @@ tasks.withType<KotlinCompile>().configureEach {
}
}
// To make Gradle happy
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
tasks.withType<Test>().configureEach {
// For kapt to work with kotlin-compile-testing
jvmArgs(
"--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
)
}
val shade: Configuration = configurations.maybeCreate("compileShaded")

View File

@@ -21,6 +21,11 @@ plugins {
kotlin("kapt")
}
tasks.withType<Test>().configureEach {
// ExtendsPlatformClassWithProtectedField tests a case where we set a protected ByteArrayOutputStream.buf field
jvmArgs("--add-opens=java.base/java.io=ALL-UNNAMED")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
@Suppress("SuspiciousCollectionReassignment")

View File

@@ -19,14 +19,49 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
id("com.vanniktech.maven.publish")
id("ru.vyarus.animalsniffer")
}
val mainSourceSet by sourceSets.named("main")
val java16 by sourceSets.creating {
java {
srcDir("src/main/java16")
}
}
tasks.named<JavaCompile>("compileJava16Java") {
javaCompiler.set(
javaToolchains.compilerFor {
languageVersion.set(JavaLanguageVersion.of(16))
}
)
options.release.set(16)
}
// Package our actual RecordJsonAdapter from java16 sources in and denote it as an MRJAR
tasks.named<Jar>("jar") {
from(java16.output) {
into("META-INF/versions/16")
}
manifest {
attributes("Multi-Release" to "true")
}
}
configurations {
"java16Implementation" {
extendsFrom(api.get())
extendsFrom(implementation.get())
}
}
tasks.withType<Test>().configureEach {
// ExtendsPlatformClassWithProtectedField tests a case where we set a protected ByteArrayOutputStream.buf field
jvmArgs("--add-opens=java.base/java.io=ALL-UNNAMED")
}
tasks.withType<KotlinCompile>()
.configureEach {
kotlinOptions {
jvmTarget = "1.6"
if (name.contains("test", true)) {
@Suppress("SuspiciousCollectionReassignment") // It's not suspicious
freeCompilerArgs += listOf("-Xopt-in=kotlin.ExperimentalStdlibApi")
@@ -35,6 +70,8 @@ tasks.withType<KotlinCompile>()
}
dependencies {
// So the j16 source set can "see" main Moshi sources
"java16Implementation"(mainSourceSet.output)
compileOnly(Dependencies.jsr305)
api(Dependencies.okio)

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2021 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
*
* https://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.
*/
plugins {
`java-library`
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(16)
}
dependencies {
testImplementation(project(":moshi"))
testCompileOnly(Dependencies.jsr305)
testImplementation(Dependencies.Testing.junit)
testImplementation(Dependencies.Testing.truth)
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright (C) 2021 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
*
* https://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 com.squareup.moshi.records;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.squareup.moshi.FromJson;
import com.squareup.moshi.Json;
import com.squareup.moshi.JsonQualifier;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.ToJson;
import com.squareup.moshi.Types;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.junit.Test;
public final class RecordsTest {
private final Moshi moshi = new Moshi.Builder().build();
@Test
public void smokeTest() throws IOException {
var stringAdapter = moshi.adapter(String.class);
var adapter =
moshi
.newBuilder()
.add(CharSequence.class, stringAdapter)
.add(Types.subtypeOf(CharSequence.class), stringAdapter)
.add(Types.supertypeOf(CharSequence.class), stringAdapter)
.build()
.adapter(SmokeTestType.class);
var instance =
new SmokeTestType(
"John",
"Smith",
25,
List.of("American"),
70.5f,
null,
true,
List.of("super wildcards!"),
List.of("extend wildcards!"),
List.of("unbounded"),
List.of("objectList"),
new int[] {1, 2, 3},
new String[] {"fav", "arrays"},
Map.of("italian", "pasta"),
Set.of(List.of(Map.of("someKey", new int[] {1}))),
new Map[] {Map.of("Hello", "value")});
var json = adapter.toJson(instance);
var deserialized = adapter.fromJson(json);
assertThat(deserialized).isEqualTo(instance);
}
public static record SmokeTestType(
@Json(name = "first_name") String firstName,
@Json(name = "last_name") String lastName,
int age,
List<String> nationalities,
float weight,
Boolean tattoos, // Boxed primitive test
boolean hasChildren,
List<? super CharSequence> superWildcard,
List<? extends CharSequence> extendsWildcard,
List<?> unboundedWildcard,
List<Object> objectList,
int[] favoriteThreeNumbers,
String[] favoriteArrayValues,
Map<String, String> foodPreferences,
Set<List<Map<String, int[]>>> setListMapArrayInt,
Map<String, Object>[] nestedArray) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SmokeTestType that = (SmokeTestType) o;
return age == that.age
&& Float.compare(that.weight, weight) == 0
&& hasChildren == that.hasChildren
&& firstName.equals(that.firstName)
&& lastName.equals(that.lastName)
&& nationalities.equals(that.nationalities)
&& Objects.equals(tattoos, that.tattoos)
&& superWildcard.equals(that.superWildcard)
&& extendsWildcard.equals(that.extendsWildcard)
&& unboundedWildcard.equals(that.unboundedWildcard)
&& objectList.equals(that.objectList)
&& Arrays.equals(favoriteThreeNumbers, that.favoriteThreeNumbers)
&& Arrays.equals(favoriteArrayValues, that.favoriteArrayValues)
&& foodPreferences.equals(that.foodPreferences)
// && setListMapArrayInt.equals(that.setListMapArrayInt) // Nested array equality doesn't
// carry over
&& Arrays.equals(nestedArray, that.nestedArray);
}
@Override
public int hashCode() {
int result =
Objects.hash(
firstName,
lastName,
age,
nationalities,
weight,
tattoos,
hasChildren,
superWildcard,
extendsWildcard,
unboundedWildcard,
objectList,
foodPreferences,
setListMapArrayInt);
result = 31 * result + Arrays.hashCode(favoriteThreeNumbers);
result = 31 * result + Arrays.hashCode(favoriteArrayValues);
result = 31 * result + Arrays.hashCode(nestedArray);
return result;
}
}
@Test
public void genericRecord() throws IOException {
var adapter =
moshi.<GenericRecord<String>>adapter(
Types.newParameterizedTypeWithOwner(
RecordsTest.class, GenericRecord.class, String.class));
assertThat(adapter.fromJson("{\"value\":\"Okay!\"}")).isEqualTo(new GenericRecord<>("Okay!"));
}
public static record GenericRecord<T>(T value) {}
@Test
public void genericBoundedRecord() throws IOException {
var adapter =
moshi.<GenericBoundedRecord<Integer>>adapter(
Types.newParameterizedTypeWithOwner(
RecordsTest.class, GenericBoundedRecord.class, Integer.class));
assertThat(adapter.fromJson("{\"value\":4}")).isEqualTo(new GenericBoundedRecord<>(4));
}
@Test
public void qualifiedValues() throws IOException {
var adapter = moshi.newBuilder().add(new ColorAdapter()).build().adapter(QualifiedValues.class);
assertThat(adapter.fromJson("{\"value\":\"#ff0000\"}"))
.isEqualTo(new QualifiedValues(16711680));
}
public static record QualifiedValues(@HexColor int value) {}
@Retention(RUNTIME)
@JsonQualifier
@interface HexColor {}
/** Converts strings like #ff0000 to the corresponding color ints. */
public static class ColorAdapter {
@ToJson
public String toJson(@HexColor int rgb) {
return String.format("#%06x", rgb);
}
@FromJson
@HexColor
public int fromJson(String rgb) {
return Integer.parseInt(rgb.substring(1), 16);
}
}
public static record GenericBoundedRecord<T extends Number>(T value) {}
@Test
public void jsonName() throws IOException {
var adapter = moshi.adapter(JsonName.class);
assertThat(adapter.fromJson("{\"actualValue\":3}")).isEqualTo(new JsonName(3));
}
public static record JsonName(@Json(name = "actualValue") int value) {}
}

View File

@@ -51,6 +51,7 @@ public final class Moshi {
BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(RecordJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY);
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2021 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
*
* https://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 com.squareup.moshi;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Set;
import javax.annotation.Nullable;
/**
* This is just a simple shim for linking in {@link StandardJsonAdapters} and swapped with a real
* implementation in Java 16 via MR Jar.
*/
final class RecordJsonAdapter<T> extends JsonAdapter<T> {
static final JsonAdapter.Factory FACTORY =
new JsonAdapter.Factory() {
@Nullable
@Override
public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
return null;
}
};
@Nullable
@Override
public T fromJson(JsonReader reader) throws IOException {
throw new AssertionError();
}
@Override
public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
throw new AssertionError();
}
}

View File

@@ -0,0 +1,218 @@
/*
* Copyright (C) 2021 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
*
* https://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 com.squareup.moshi;
import static java.lang.invoke.MethodType.methodType;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.RecordComponent;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* A {@link JsonAdapter} that supports Java {@code record} classes via reflection.
*
* <p><em>NOTE:</em> Java records require JDK 16 or higher.
*/
final class RecordJsonAdapter<T> extends JsonAdapter<T> {
static final JsonAdapter.Factory FACTORY =
(type, annotations, moshi) -> {
if (!annotations.isEmpty()) {
return null;
}
if (!(type instanceof Class) && !(type instanceof ParameterizedType)) {
return null;
}
var rawType = Types.getRawType(type);
if (!rawType.isRecord()) {
return null;
}
Map<String, Type> mappedTypeArgs = null;
if (type instanceof ParameterizedType parameterizedType) {
Type[] typeArgs = parameterizedType.getActualTypeArguments();
var typeVars = rawType.getTypeParameters();
mappedTypeArgs = new LinkedHashMap<>(typeArgs.length);
for (int i = 0; i < typeArgs.length; ++i) {
var typeVarName = typeVars[i].getName();
var materialized = typeArgs[i];
mappedTypeArgs.put(typeVarName, materialized);
}
}
var components = rawType.getRecordComponents();
var bindings = new LinkedHashMap<String, ComponentBinding<?>>();
var constructorParams = new Class<?>[components.length];
var lookup = MethodHandles.lookup();
for (int i = 0, componentsLength = components.length; i < componentsLength; i++) {
RecordComponent component = components[i];
constructorParams[i] = component.getType();
var name = component.getName();
var componentType = component.getGenericType();
if (componentType instanceof TypeVariable<?> typeVariable) {
var typeVarName = typeVariable.getName();
if (mappedTypeArgs == null) {
throw new AssertionError(
"No mapped type arguments found for type '" + typeVarName + "'");
}
var mappedType = mappedTypeArgs.get(typeVarName);
if (mappedType == null) {
throw new AssertionError(
"No materialized type argument found for type '" + typeVarName + "'");
}
componentType = mappedType;
}
var jsonName = name;
Set<Annotation> qualifiers = null;
for (var annotation : component.getDeclaredAnnotations()) {
if (annotation instanceof Json jsonAnnotation) {
jsonName = jsonAnnotation.name();
} else {
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
if (qualifiers == null) {
qualifiers = new LinkedHashSet<>();
}
qualifiers.add(annotation);
}
}
}
if (qualifiers == null) {
qualifiers = Collections.emptySet();
}
var adapter = moshi.adapter(componentType, qualifiers);
MethodHandle accessor;
try {
accessor = lookup.unreflect(component.getAccessor());
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
var componentBinding = new ComponentBinding<>(name, jsonName, adapter, accessor);
var replaced = bindings.put(jsonName, componentBinding);
if (replaced != null) {
throw new IllegalArgumentException(
"Conflicting components:\n"
+ " "
+ replaced.name
+ "\n"
+ " "
+ componentBinding.name);
}
}
MethodHandle constructor;
try {
constructor = lookup.findConstructor(rawType, methodType(void.class, constructorParams));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new AssertionError(e);
}
return new RecordJsonAdapter<>(constructor, rawType.getSimpleName(), bindings).nullSafe();
};
private static record ComponentBinding<T>(
String name, String jsonName, JsonAdapter<T> adapter, MethodHandle accessor) {}
private final String targetClass;
private final MethodHandle constructor;
private final ComponentBinding<Object>[] componentBindingsArray;
private final JsonReader.Options options;
@SuppressWarnings("ToArrayCallWithZeroLengthArrayArgument")
public RecordJsonAdapter(
MethodHandle constructor,
String targetClass,
Map<String, ComponentBinding<?>> componentBindings) {
this.constructor = constructor;
this.targetClass = targetClass;
//noinspection unchecked
this.componentBindingsArray =
componentBindings.values().toArray(new ComponentBinding[componentBindings.size()]);
this.options =
JsonReader.Options.of(
componentBindings.keySet().toArray(new String[componentBindings.size()]));
}
@Override
public T fromJson(JsonReader reader) throws IOException {
var resultsArray = new Object[componentBindingsArray.length];
reader.beginObject();
while (reader.hasNext()) {
int index = reader.selectName(options);
if (index == -1) {
reader.skipName();
reader.skipValue();
continue;
}
var result = componentBindingsArray[index].adapter.fromJson(reader);
resultsArray[index] = result;
}
reader.endObject();
try {
//noinspection unchecked
return (T) constructor.invokeWithArguments(resultsArray);
} catch (Throwable e) {
if (e instanceof InvocationTargetException ite) {
Throwable cause = ite.getCause();
if (cause instanceof RuntimeException) throw (RuntimeException) cause;
if (cause instanceof Error) throw (Error) cause;
throw new RuntimeException(cause);
} else {
throw new AssertionError(e);
}
}
}
@Override
public void toJson(JsonWriter writer, T value) throws IOException {
writer.beginObject();
for (var binding : componentBindingsArray) {
writer.name(binding.jsonName);
try {
binding.adapter.toJson(writer, binding.accessor.invoke(value));
} catch (Throwable e) {
if (e instanceof InvocationTargetException ite) {
Throwable cause = ite.getCause();
if (cause instanceof RuntimeException) throw (RuntimeException) cause;
if (cause instanceof Error) throw (Error) cause;
throw new RuntimeException(cause);
} else {
throw new AssertionError(e);
}
}
}
writer.endObject();
}
@Override
public String toString() {
return "JsonAdapter(" + targetClass + ")";
}
}

View File

@@ -126,7 +126,7 @@ public final class JsonAdapterTest {
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessageThat().isEqualTo("Unexpected null at $[1]");
assertThat(reader.nextNull()).isNull();
assertThat(reader.<Object>nextNull()).isNull();
}
assertThat(toUpperCase.fromJson(reader)).isEqualTo("C");
reader.endArray();

View File

@@ -565,7 +565,7 @@ public final class JsonReaderTest {
assertThat(reader2.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
JsonReader reader3 = newReader("null");
assertThat(reader3.nextNull()).isNull();
assertThat(reader3.<Object>nextNull()).isNull();
assertThat(reader3.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
JsonReader reader4 = newReader("123");

View File

@@ -59,7 +59,7 @@ public final class JsonValueReaderTest {
assertThat(reader.hasNext()).isTrue();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL);
assertThat(reader.nextNull()).isNull();
assertThat(reader.<Object>nextNull()).isNull();
assertThat(reader.hasNext()).isFalse();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_ARRAY);
@@ -103,7 +103,7 @@ public final class JsonValueReaderTest {
assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME);
assertThat(reader.nextName()).isEqualTo("d");
assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL);
assertThat(reader.nextNull()).isNull();
assertThat(reader.<Object>nextNull()).isNull();
assertThat(reader.hasNext()).isFalse();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT);

View File

@@ -152,10 +152,10 @@ public final class JsonValueWriterTest {
public void primitiveIntegerTypesEmitLong() throws Exception {
JsonValueWriter writer = new JsonValueWriter();
writer.beginArray();
writer.value(new Byte(Byte.MIN_VALUE));
writer.value(new Short(Short.MIN_VALUE));
writer.value(new Integer(Integer.MIN_VALUE));
writer.value(new Long(Long.MIN_VALUE));
writer.value(Byte.valueOf(Byte.MIN_VALUE));
writer.value(Short.valueOf(Short.MIN_VALUE));
writer.value(Integer.valueOf(Integer.MIN_VALUE));
writer.value(Long.valueOf(Long.MIN_VALUE));
writer.endArray();
List<Number> numbers =
@@ -167,8 +167,8 @@ public final class JsonValueWriterTest {
public void primitiveFloatingPointTypesEmitDouble() throws Exception {
JsonValueWriter writer = new JsonValueWriter();
writer.beginArray();
writer.value(new Float(0.5f));
writer.value(new Double(0.5d));
writer.value(Float.valueOf(0.5f));
writer.value(Double.valueOf(0.5d));
writer.endArray();
List<Number> numbers = Arrays.<Number>asList(0.5d, 0.5d);

View File

@@ -236,17 +236,17 @@ public final class JsonWriterTest {
JsonWriter writer = factory.newWriter();
writer.beginArray();
try {
writer.value(new Double(Double.NaN));
writer.value(Double.valueOf(Double.NaN));
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(new Double(Double.NEGATIVE_INFINITY));
writer.value(Double.valueOf(Double.NEGATIVE_INFINITY));
fail();
} catch (IllegalArgumentException expected) {
}
try {
writer.value(new Double(Double.POSITIVE_INFINITY));
writer.value(Double.valueOf(Double.POSITIVE_INFINITY));
fail();
} catch (IllegalArgumentException expected) {
}

View File

@@ -24,6 +24,7 @@ pluginManagement {
rootProject.name = "moshi-root"
include(":moshi")
include(":moshi:japicmp")
include(":moshi:records-tests")
include(":adapters")
include(":adapters:japicmp")
include(":examples")