diff --git a/ci/run_ci.sh b/ci/run_ci.sh index 0196ce043f..91318f4e8a 100755 --- a/ci/run_ci.sh +++ b/ci/run_ci.sh @@ -98,7 +98,7 @@ run_graalvm_tests() { # GraalVM jobs consume production jars only; Java CI owns test/source jar verification. # Run the install goal directly after package so verify is not repeated in every native job. mvn -T10 -B --no-transfer-progress clean package install:install \ - -pl .,fory-test-core,fory-core,fory-json \ + -pl .,fory-test-core,fory-core,fory-json,fory-annotation-processor \ -Dmaven.test.skip=true \ -Dmaven.source.skip=true \ -Dmaven.javadoc.skip=true diff --git a/ci/tasks/java.py b/ci/tasks/java.py index 7af581443f..86bd15ecfc 100644 --- a/ci/tasks/java.py +++ b/ci/tasks/java.py @@ -366,7 +366,8 @@ def run_graalvm_tests(main_class): # production multi-release jars consumed by native-image. common.exec_cmd( "mvn -T10 -B --no-transfer-progress clean package install:install " - "-pl .,fory-test-core,fory-core,fory-json -Dmaven.test.skip=true " + "-pl .,fory-test-core,fory-core,fory-json,fory-annotation-processor " + "-Dmaven.test.skip=true " "-Dmaven.source.skip=true -Dmaven.javadoc.skip=true" ) diff --git a/docs/guide/java/android-support.md b/docs/guide/java/android-support.md index 25bf7a5b2f..8891329f1c 100644 --- a/docs/guide/java/android-support.md +++ b/docs/guide/java/android-support.md @@ -81,8 +81,9 @@ Child codecs act on one direct level only. For example, `elementCodec` on `Money `Money[]`, and `elementCodec` on `AtomicReferenceArray` handles each `Money`. Use a complete `value` codec when deeper custom behavior is required. -`@JsonType` is optional. Add the annotation processor and mark application models with `JsonType` -when the build should generate exact R8 rules: +Add the annotation processor and mark application object models with `JsonType` to generate direct +field, getter, setter, Record constructor, and `JsonCreator` operations together with exact R8 +rules: ```kotlin dependencies { @@ -99,7 +100,8 @@ public final class Invoice { } ``` -Applications that omit `JsonType` can supply equivalent exact rules themselves. Retain every model +Ordinary non-Record classes that omit `JsonType` can supply equivalent exact rules themselves. +Retain every model constructor, field, method, generic signature, declaration annotation, and parameter annotation used by Fory JSON, plus the public no-argument constructor of every annotation-selected codec. For the previous `Invoice` example: @@ -118,8 +120,7 @@ previous `Invoice` example: ``` The same exact-rule approach supports every `JsonCodec` member; it is not limited to complete-value -codecs. `JsonType` only automates rule generation on Android and is not required for codec -selection. +codecs. `JsonType` is not required for codec selection on an ordinary class. For `@JsonType` models, the generated R8 rules also retain `JsonValue` fields and effective methods, fixed `JsonRawValue` and `JsonBase64` fields and getters, their runtime annotations, and the Base64 @@ -131,8 +132,14 @@ name that method explicitly. Android Fory JSON requires a retained no-argument constructor for an ordinary mutable class; it may be non-public when Android reflection can make it accessible. `JsonCreator` constructor-backed classes follow the normal creator rules instead. Retain every field and method used for reflection, -or use an application codec when a model cannot satisfy those requirements. Fory JSON Record -mapping is not supported on Android. +or use an application codec when a model cannot satisfy those requirements. + +Android-desugared Records require a direct `@JsonType` annotation and the annotation processor. +Manual R8 rules alone cannot reconstruct Record component order because Android does not provide +the Java Record reflection APIs. This also applies to a Record whose complete representation is a +`JsonValue` String: the generated companion identifies the propagated component accessor and calls +an annotated one-String canonical constructor directly. Generated child codecs act on one level +exactly as they do on the JVM; use a complete value codec for deeper nested behavior. ## Static Generated Serializers @@ -207,7 +214,6 @@ The following JVM features are not supported on Android: - Native-address serialization APIs and native-address `MemoryBuffer` wrapping. - Raw unsafe memory copy APIs. - `java/fory-format` row-format APIs. -- Fory JSON Record mapping. ## ByteBuffer diff --git a/docs/guide/java/graalvm-support.md b/docs/guide/java/graalvm-support.md index 9c389774f2..216c42942c 100644 --- a/docs/guide/java/graalvm-support.md +++ b/docs/guide/java/graalvm-support.md @@ -46,9 +46,20 @@ compilation is unavailable. ## Fory JSON -Fory JSON uses a separate Native Image workflow. It has no type-registration API and does not -create a `ForyJson` instance or generate JSON codecs while the image is built. Add `@JsonType` to -each object model that the native executable reads or writes: +Fory JSON uses a separate Native Image workflow. Add the Fory annotation processor to the +application compiler path: + +```xml + + + org.apache.fory + fory-annotation-processor + ${fory.version} + + +``` + +Then add `@JsonType` to each concrete object model that the native executable reads or writes: ```java import org.apache.fory.json.ForyJson; @@ -69,16 +80,19 @@ public class JsonExample { } ``` -The `fory-json` artifact activates its Native Image Feature automatically. Reachable `@JsonType` -classes gate object-model metadata. `@JsonType` is not inherited, so annotate every concrete runtime -model. An annotated base with a class-literal `@JsonSubTypes` table registers those listed subtypes -automatically. Reachable concrete `Collection` and `Map` root types are also supported when they +The processor generates direct property and creator operations. The `fory-json` artifact activates +its Native Image Feature automatically and retains the generated factories and required model +metadata. `@JsonType` is not inherited, so annotate every concrete runtime model. An annotated base +with a class-literal `@JsonSubTypes` table registers those listed subtypes automatically, but each +concrete object subtype needs its own direct `@JsonType` to receive generated operations. Reachable +concrete `Collection` and `Map` root types are also supported when they have the public no-argument constructor required by Fory JSON. Reachable `@JsonCodec` declarations register their codec constructor even when the declaration target is not an object model. A class referenced only by a runtime string is not reachable; `JsonSubTypes.Type.className` is therefore unsupported in a native image. -Native execution uses Fory JSON's interpreted object codec. `ForyJson.builder()` automatically +Native execution uses Fory JSON's interpreted readers and writers with the generated property and +creator operations. `ForyJson.builder()` automatically disables runtime code generation and asynchronous compilation in the native executable, while all other builder options retain their normal behavior. Applications can create differently configured `ForyJson` instances at runtime and do not need build-time initialization or reflection @@ -93,7 +107,8 @@ on the JVM and Android. one-String `JsonCreator` constructors and public static factories. Fixed `JsonRawValue` fields and getters support trusted raw String values, and fixed `JsonBase64` fields and getters support Base64 `byte[]` values as on the JVM. Annotate each reachable owning model with `JsonType` so Native Image -retains these members and the Base64 codec constructor. +retains these members and the Base64 codec constructor. A directly annotated `JsonValue` Record +uses its generated component accessor and canonical constructor operations. `JsonAnyProperty` and `JsonAnyGetter` flatten their Map into the enclosing object. Use `@JsonCodec(valueCodec = ...)` on that field or getter to customize each dynamic value. A second diff --git a/docs/guide/java/json-support.md b/docs/guide/java/json-support.md index 53a61e7a4e..bacad8bd35 100644 --- a/docs/guide/java/json-support.md +++ b/docs/guide/java/json-support.md @@ -328,10 +328,15 @@ import org.apache.fory.json.annotation.JsonType; import org.apache.fory.json.annotation.JsonValue; ``` -`JsonType` marks a reachable object model for GraalVM Native Image metadata. On Android it enables -processor-generated R8 rules. It has no effect on ordinary JVM JSON behavior and is not inherited. -See [GraalVM Support](graalvm-support.md) and -[Android Support](android-support.md) for the platform workflows. +`JsonType` asks the annotation processor to generate direct property and creator operations plus +the exact retention rules for an eligible concrete object model. A directly annotated +`JsonValue` Record also receives a companion so its value accessor and canonical constructor work +after Android desugaring. The same generated companion is used on the JVM, Android, and GraalVM +Native Image. The annotation is not inherited; a concrete subtype needs its own direct annotation +to receive a companion. See +[GraalVM Support](graalvm-support.md) and [Android Support](android-support.md) for setup. +A directly annotated model that uses the default object codec requires that generated companion; +the runtime reports a configuration error if the processor output is missing. ### `JsonProperty` @@ -999,8 +1004,9 @@ type-declaration codec is used for a more specific target, every decoded value m assignable to that target. The annotation has the same FIELD, METHOD, and PARAMETER behavior on the JVM, Android, and GraalVM -Native Image. Android applications may use `JsonType` for generated exact R8 rules or provide the -equivalent rules themselves. GraalVM object models follow the `JsonType` workflow in +Native Image. Ordinary Android classes may omit `JsonType` and provide equivalent exact rules; +Android-desugared Records, including `JsonValue` Records, require `JsonType` and the processor. +GraalVM object models follow the `JsonType` workflow in [GraalVM Support](graalvm-support.md). ## Type validation and untrusted input diff --git a/docs/guide/java/static-generated-serializers.md b/docs/guide/java/static-generated-serializers.md index fa14f6bd24..f3da9f0daa 100644 --- a/docs/guide/java/static-generated-serializers.md +++ b/docs/guide/java/static-generated-serializers.md @@ -82,7 +82,7 @@ The processor generates serializer classes in the same Java package as the annot - `Order_ForyNativeSerializer` for Java native mode. For a static nested type such as `Outer.Inner`, the generated top-level classes are -`Outer_Inner_ForySerializer` and `Outer_Inner_ForyNativeSerializer`. +`Outer_d_Inner_ForySerializer` and `Outer_d_Inner_ForyNativeSerializer`. ## Field Debug Tracing diff --git a/docs/guide/kotlin/static-generated-serializers.md b/docs/guide/kotlin/static-generated-serializers.md index 08c3f27f8c..875fbee46a 100644 --- a/docs/guide/kotlin/static-generated-serializers.md +++ b/docs/guide/kotlin/static-generated-serializers.md @@ -286,7 +286,7 @@ Generated Schema IDL modules use the same path. They call The generated serializer is emitted in the same package as the target class. Its name is `_ForySerializer`. For nested binary names, `$` is encoded -as `_`; source underscores are encoded as `_u_`. +as `_d_`; source underscores are encoded as `_u_`. These names are an implementation detail. They matter for diagnostics and Android shrinking, but user code should only register target classes. diff --git a/integration_tests/android_tests/README.md b/integration_tests/android_tests/README.md index 399261e92a..cb1a304738 100644 --- a/integration_tests/android_tests/README.md +++ b/integration_tests/android_tests/README.md @@ -3,8 +3,10 @@ This project runs Android API 26+ instrumented tests for Java `fory-core` and `fory-json`. API 26 runs both debug reflection coverage and the release-minified suite. API 36 runs the release-minified suite. Release coverage verifies static -serializers, processor-generated Fory JSON rules for ordinary codec declarations, -and equivalent application-authored exact rules. +serializers, processor-generated Fory JSON execution for mutable classes, +`JsonCreator` classes, object-mapped and `JsonValue` desugared Records, generated +retention rules, and equivalent application-authored exact rules for +unannotated ordinary classes. The tests consume `org.apache.fory:fory-core:1.4.0-SNAPSHOT`, `org.apache.fory:fory-json:1.4.0-SNAPSHOT`, and diff --git a/integration_tests/android_tests/build.gradle b/integration_tests/android_tests/build.gradle index e6dd25601b..e619c4daab 100644 --- a/integration_tests/android_tests/build.gradle +++ b/integration_tests/android_tests/build.gradle @@ -60,6 +60,11 @@ android { testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + testBuildType foryTestBuildType buildTypes { diff --git a/integration_tests/android_tests/proguard-rules.pro b/integration_tests/android_tests/proguard-rules.pro index e26edaa8c9..5541d767ab 100644 --- a/integration_tests/android_tests/proguard-rules.pro +++ b/integration_tests/android_tests/proguard-rules.pro @@ -11,6 +11,8 @@ public static void plainReflectionWithoutRules(boolean); public static void manualPlainRules(); public static void generatedPlainRules(); + public static void generatedRecord(); + public static void generatedValueRecord(); public static void manualCodecs(); public static void generatedCodecs(); } diff --git a/integration_tests/android_tests/src/androidTest/java/org/apache/fory/android/ForyAndroidInstrumentedTest.java b/integration_tests/android_tests/src/androidTest/java/org/apache/fory/android/ForyAndroidInstrumentedTest.java index 0c8d9d266e..17a299bf7d 100644 --- a/integration_tests/android_tests/src/androidTest/java/org/apache/fory/android/ForyAndroidInstrumentedTest.java +++ b/integration_tests/android_tests/src/androidTest/java/org/apache/fory/android/ForyAndroidInstrumentedTest.java @@ -45,6 +45,16 @@ public void generatedPlainJsonRules() { AndroidJsonScenarios.generatedPlainRules(); } + @Test + public void generatedRecordJson() { + AndroidJsonScenarios.generatedRecord(); + } + + @Test + public void generatedValueRecordJson() { + AndroidJsonScenarios.generatedValueRecord(); + } + @Test public void manualJsonCodecs() { AndroidJsonScenarios.manualCodecs(); diff --git a/integration_tests/android_tests/src/main/java/org/apache/fory/android/AndroidJsonScenarios.java b/integration_tests/android_tests/src/main/java/org/apache/fory/android/AndroidJsonScenarios.java index 679510f0d9..4423927231 100644 --- a/integration_tests/android_tests/src/main/java/org/apache/fory/android/AndroidJsonScenarios.java +++ b/integration_tests/android_tests/src/main/java/org/apache/fory/android/AndroidJsonScenarios.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; import org.apache.fory.json.ForyJson; +import org.apache.fory.json.annotation.JsonType; /** Android acceptance scenarios for reflection, declaration codecs, and R8 retention. */ public final class AndroidJsonScenarios { @@ -62,6 +63,41 @@ public static void generatedPlainRules() { checkEquals("generated-plain", decoded.name); } + public static void generatedRecord() { + try { + Class.forName( + "org.apache.fory.android.GeneratedJsonRecord_ForyJsonCodec", + false, + GeneratedJsonRecord.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new AssertionError("generated Record JSON codec was removed", e); + } + check(GeneratedJsonRecord.class.isAnnotationPresent(JsonType.class)); + ForyJson json = ForyJson.builder().build(); + String encoded = json.toJson(new GeneratedJsonRecord(30, "android")); + checkEquals("{\"id\":30,\"name\":\"ANDROID\"}", encoded); + GeneratedJsonRecord decoded = json.fromJson(encoded, GeneratedJsonRecord.class); + checkEquals(30, decoded.id()); + checkEquals("ANDROID", decoded.name()); + } + + public static void generatedValueRecord() { + try { + Class.forName( + "org.apache.fory.android.GeneratedJsonValueRecord_ForyJsonCodec", + false, + GeneratedJsonValueRecord.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new AssertionError("generated value Record JSON codec was removed", e); + } + ForyJson json = ForyJson.builder().build(); + GeneratedJsonValueRecord value = new GeneratedJsonValueRecord("android-value"); + checkEquals("\"android-value\"", json.toJson(value)); + checkEquals( + "decoded-value", + json.fromJson("\"decoded-value\"", GeneratedJsonValueRecord.class).value()); + } + public static void manualCodecs() { ForyJson json = ForyJson.builder().build(); ManualJsonModel value = new ManualJsonModel(); diff --git a/integration_tests/android_tests/src/main/java/org/apache/fory/android/GeneratedJsonRecord.java b/integration_tests/android_tests/src/main/java/org/apache/fory/android/GeneratedJsonRecord.java new file mode 100644 index 0000000000..cb9c0c207f --- /dev/null +++ b/integration_tests/android_tests/src/main/java/org/apache/fory/android/GeneratedJsonRecord.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.android; + +import java.util.Locale; +import org.apache.fory.json.annotation.JsonType; + +/** Processor-generated JSON execution fixture for an Android-desugared Record. */ +@JsonType +public record GeneratedJsonRecord(int id, String name) { + @Override + public String name() { + return name.toUpperCase(Locale.ROOT); + } +} diff --git a/integration_tests/android_tests/src/main/java/org/apache/fory/android/GeneratedJsonValueRecord.java b/integration_tests/android_tests/src/main/java/org/apache/fory/android/GeneratedJsonValueRecord.java new file mode 100644 index 0000000000..4d55d0bfac --- /dev/null +++ b/integration_tests/android_tests/src/main/java/org/apache/fory/android/GeneratedJsonValueRecord.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.android; + +import org.apache.fory.json.annotation.JsonCreator; +import org.apache.fory.json.annotation.JsonType; +import org.apache.fory.json.annotation.JsonValue; + +/** Processor-generated value execution fixture for an Android-desugared Record. */ +@JsonType +public record GeneratedJsonValueRecord(@JsonValue String value) { + @JsonCreator + public GeneratedJsonValueRecord {} +} diff --git a/integration_tests/graalvm_tests/pom.xml b/integration_tests/graalvm_tests/pom.xml index f2b872df06..eb49197af0 100644 --- a/integration_tests/graalvm_tests/pom.xml +++ b/integration_tests/graalvm_tests/pom.xml @@ -95,6 +95,13 @@ ${maven.compiler.source} ${maven.compiler.source} + + + org.apache.fory + fory-annotation-processor + ${project.version} + + diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java index 168be4712b..91642060c1 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java @@ -35,6 +35,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; +import org.apache.fory.graalvm.closed.ClosedJsonRecord; import org.apache.fory.json.ForyJson; import org.apache.fory.json.PropertyNamingStrategy; import org.apache.fory.json.annotation.JsonAnyProperty; @@ -69,9 +70,17 @@ public static void main(String[] args) { testGenericProperties(); testBigDecimal(); testSqlTypes(); + testClosedPackage(); System.out.println("Fory JSON succeed"); } + private static void testClosedPackage() { + ForyJson json = ForyJson.builder().build(); + ClosedJsonRecord value = new ClosedJsonRecord(17, "closed"); + String encoded = json.toJson(value); + Preconditions.checkArgument(json.fromJson(encoded, ClosedJsonRecord.class).equals(value)); + } + private static void testModels() { ForyJson json = ForyJson.builder().build(); Model value = new Model(); diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonRecord.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonRecord.java new file mode 100644 index 0000000000..6910c04ebf --- /dev/null +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonRecord.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.graalvm.closed; + +import org.apache.fory.json.annotation.JsonType; + +/** Record model in a package that is neither exported nor opened to Fory. */ +@JsonType +public record ClosedJsonRecord(int id, String name) {} diff --git a/java/README.md b/java/README.md index 80fdd66ca5..1dfd6408ab 100644 --- a/java/README.md +++ b/java/README.md @@ -367,8 +367,9 @@ java --add-modules=jdk.incubator.vector ... ### GraalVM Native Image Fory supports GraalVM Native Image without application reflection configuration. Binary -serialization generates serializers while the image is built; Fory JSON registers reachable -`@JsonType` models and uses its interpreted codec path. Build your native image as follows: +serialization generates serializers while the image is built; the Fory annotation processor +generates type-owned execution companions for Fory JSON `@JsonType` models. Build your native image +as follows: ```bash # Generate serializers at build time diff --git a/java/fory-annotation-processor/README.md b/java/fory-annotation-processor/README.md index aef940b433..335adafc32 100644 --- a/java/fory-annotation-processor/README.md +++ b/java/fory-annotation-processor/README.md @@ -1,10 +1,11 @@ # Fory Annotation Processor `fory-annotation-processor` generates static serializers for Java classes annotated with -`@ForyStruct`. It also generates exact Android R8 rules for Fory JSON classes annotated with -`@JsonType`. The generated rules retain effective `@JsonValue`, `@JsonRawValue`, and `@JsonBase64` -members, their runtime annotations, and selected codec constructors. The outputs are generated by -javac during the application build. +`@ForyStruct`. For concrete Fory JSON object models annotated with `@JsonType`, and for directly +annotated `JsonValue` Records, it generates a `_ForyJsonCodec` companion with direct member and +creator operations together with exact Android R8 rules. The generated rules retain effective +`@JsonValue`, `@JsonRawValue`, and `@JsonBase64` members, their runtime annotations, and selected +codec constructors. The outputs are generated by javac during the application build. Use this processor for: @@ -12,9 +13,11 @@ Use this processor for: - ordinary JVM applications using `ForyBuilder#withCodegen(false)`. - Android model classes that use Fory type-use annotations such as `@Ref`, `@UInt8Type`, or `@Float16Type`. -- Android Fory JSON models that need automatic R8 rules. +- Fory JSON object models that need direct member access, Android Record support, GraalVM metadata, + or automatic R8 rules. -For GraalVM native images, use Fory's GraalVM native-image build-time serializer generation instead. +For GraalVM native images, use the processor for `@JsonType` JSON models and Fory's native-image +build-time serializer generation for ordinary Fory serialization. ## Ownership @@ -22,9 +25,13 @@ The processor is an opt-in build tool. Applications add it to their annotation-p processor has no production dependency on `fory-core` or `fory-json`; generated sources compile against the corresponding runtime artifact already used by the application. -For Fory JSON on Android, `@JsonType` is optional. A model without it can use application-authored -exact R8 rules. `@JsonCodec` uses ordinary type, field, method, and parameter annotations, so every -supported codec member remains available through reflection with equivalent rules. See +For ordinary Fory JSON object models on Android, `@JsonType` is optional. A model without it can use +application-authored exact R8 rules and reflection. Android builds that need desugared Java Record +construction use `@JsonType`, because the generated companion calls the canonical constructor and +component accessors directly. This includes a Record using a String `JsonValue` representation and +an annotated canonical constructor. `@JsonCodec` uses ordinary type, field, method, and parameter +annotations, so every supported codec member remains available through reflection with equivalent +rules. See [`docs/guide/java/android-support.md`](../../docs/guide/java/android-support.md). For the full user-facing guide, see diff --git a/java/fory-annotation-processor/pom.xml b/java/fory-annotation-processor/pom.xml index fa1767af28..87b14e0bf7 100644 --- a/java/fory-annotation-processor/pom.xml +++ b/java/fory-annotation-processor/pom.xml @@ -32,7 +32,8 @@ fory-annotation-processor - Apache Fory annotation processor for static serializers and exact JSON R8 rules. + Apache Fory annotation processor for static serializers, generated JSON model operations, and + exact JSON R8 rules. diff --git a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/ForyStructProcessor.java b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/ForyStructProcessor.java index f06ec047df..bb14b00dbf 100644 --- a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/ForyStructProcessor.java +++ b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/ForyStructProcessor.java @@ -382,48 +382,14 @@ private String writeR8Rules(List structs) { } private static String escapedResourceName(String targetBinaryName) { - StringBuilder builder = new StringBuilder(targetBinaryName.length() + 32); - for (int i = 0; i < targetBinaryName.length(); ) { - int codePoint = targetBinaryName.codePointAt(i); - if (codePoint == '.') { - builder.append('.'); - } else if (codePoint == '$') { - builder.append('_'); - } else if (codePoint == '_') { - builder.append("_u_"); - } else if (Character.isJavaIdentifierPart(codePoint)) { - builder.appendCodePoint(codePoint); - } else { - builder.append("_x").append(Integer.toHexString(codePoint)).append('_'); - } - i += Character.charCount(codePoint); - } - return builder.toString(); + return GeneratedTypeNames.escapeBinaryName(targetBinaryName); } private static String generatedSerializerName( String targetBinaryName, String packageName, SerializerMode mode) { String binarySimpleName = targetBinaryName.substring(packageName.isEmpty() ? 0 : packageName.length() + 1); - return escapeBinarySimpleName(binarySimpleName) + mode.serializerSuffix; - } - - private static String escapeBinarySimpleName(String binarySimpleName) { - StringBuilder builder = new StringBuilder(binarySimpleName.length() + 32); - for (int i = 0; i < binarySimpleName.length(); ) { - int codePoint = binarySimpleName.codePointAt(i); - if (codePoint == '$') { - builder.append('_'); - } else if (codePoint == '_') { - builder.append("_u_"); - } else if (Character.isJavaIdentifierPart(codePoint)) { - builder.appendCodePoint(codePoint); - } else { - builder.append("_x").append(Integer.toHexString(codePoint)).append('_'); - } - i += Character.charCount(codePoint); - } - return builder.toString(); + return GeneratedTypeNames.escapeBinarySimpleName(binarySimpleName) + mode.serializerSuffix; } private boolean isKotlinClass(TypeElement type) { diff --git a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java new file mode 100644 index 0000000000..2e7945969c --- /dev/null +++ b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java @@ -0,0 +1,1468 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.annotation.processing; + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; + +/** Writes the direct JSON object operations owned by one {@code JsonType} model. */ +final class GeneratedJsonCodecSourceWriter { + private static final String JSON_PACKAGE = "org.apache.fory.json"; + private static final String JSON_CODEC = JSON_PACKAGE + ".annotation.JsonCodec"; + private static final String JSON_CREATOR = JSON_PACKAGE + ".annotation.JsonCreator"; + private static final String JSON_PROPERTY = JSON_PACKAGE + ".annotation.JsonProperty"; + private static final String JSON_VALUE = JSON_PACKAGE + ".annotation.JsonValue"; + private static final String JSON_ANY_GETTER = JSON_PACKAGE + ".annotation.JsonAnyGetter"; + private static final String JSON_ANY_SETTER = JSON_PACKAGE + ".annotation.JsonAnySetter"; + private static final String JSON_SUB_TYPES = JSON_PACKAGE + ".annotation.JsonSubTypes"; + private static final String NO_JSON_VALUE_CODEC = JSON_CODEC + ".NoJsonValueCodec"; + private static final String SUFFIX = "_ForyJsonCodec"; + private static final List ASSIGNABLE_OBJECT_CODEC_EXCLUSIONS = + Arrays.asList( + "java.lang.Number", + "java.lang.CharSequence", + "java.net.InetAddress", + "java.net.InetSocketAddress", + "java.net.URL", + "java.util.Collection", + "java.util.Map", + "java.util.Calendar", + "java.util.Date", + "java.time.ZoneId", + "java.nio.ByteBuffer", + "java.io.File", + "java.nio.file.Path"); + private static final List EXACT_OBJECT_CODEC_EXCLUSIONS = + Arrays.asList( + "java.lang.Class", + "java.util.Optional", + "java.util.concurrent.atomic.AtomicReference", + "java.util.concurrent.atomic.AtomicReferenceArray"); + + private final Filer filer; + private final Elements elements; + private final Types types; + + GeneratedJsonCodecSourceWriter(ProcessingEnvironment environment) { + filer = environment.getFiler(); + elements = environment.getElementUtils(); + types = environment.getTypeUtils(); + } + + Result write(TypeElement target) { + if (!needsGeneratedCodec(target)) { + return null; + } + Model model = buildModel(target); + try { + JavaFileObject file = filer.createSourceFile(model.qualifiedName, target); + try (Writer writer = file.openWriter()) { + writer.write(render(model)); + } + } catch (IOException e) { + throw invalid( + "Failed to write generated JSON codec " + model.qualifiedName + ": " + e, target); + } + return new Result( + model.binaryName, + Collections.unmodifiableList(new ArrayList<>(model.r8Members)), + model.anySetter != null, + model.creator != null, + model.creator != null && model.creator.factory, + model.creator != null && model.creator.record); + } + + private boolean needsGeneratedCodec(TypeElement target) { + boolean record = isRecord(target); + if (!(target.getKind() == ElementKind.CLASS || record) + || target.getModifiers().contains(Modifier.ABSTRACT) + || annotationMirror(target, JSON_SUB_TYPES) != null) { + return false; + } + if (hasCompleteTypeCodec(target) + || !record && hasEffectiveJsonValue(target) + || isObjectCodecExcluded(target)) { + return false; + } + if (!isNameable(target)) { + throw invalid("@JsonType model is not accessible to generated JSON code", target); + } + return true; + } + + private boolean hasEffectiveJsonValue(TypeElement target) { + for (TypeElement owner : classHierarchy(target)) { + for (VariableElement field : ElementFilter.fieldsIn(owner.getEnclosedElements())) { + if (annotationMirror(field, JSON_VALUE) != null) { + return true; + } + } + } + for (ExecutableElement method : effectiveMethods(target)) { + if (method.getModifiers().contains(Modifier.PUBLIC) + && annotationMirror(method, JSON_VALUE) != null) { + return true; + } + } + return false; + } + + private boolean hasCompleteTypeCodec(TypeElement target) { + AnnotationMirror direct = annotationMirror(target, JSON_CODEC); + if (direct != null) { + return selectsValueCodec(direct); + } + List declarations = allDeclarations(target); + List candidates = new ArrayList<>(); + for (int i = 1; i < declarations.size(); i++) { + TypeElement declaration = declarations.get(i); + if (annotationMirror(declaration, JSON_CODEC) != null) { + candidates.add(declaration); + } + } + for (TypeElement candidate : candidates) { + boolean dominated = false; + for (TypeElement other : candidates) { + if (!candidate.equals(other) + && types.isAssignable( + types.erasure(other.asType()), types.erasure(candidate.asType()))) { + dominated = true; + break; + } + } + if (!dominated && selectsValueCodec(annotationMirror(candidate, JSON_CODEC))) { + return true; + } + } + return false; + } + + private boolean selectsValueCodec(AnnotationMirror annotation) { + AnnotationValue value = annotationValue(annotation, "value"); + if (value == null || !(value.getValue() instanceof TypeMirror)) { + return false; + } + TypeElement codec = asTypeElement((TypeMirror) value.getValue()); + return codec != null && !codec.getQualifiedName().contentEquals(NO_JSON_VALUE_CODEC); + } + + private boolean isObjectCodecExcluded(TypeElement target) { + if (target.getKind() == ElementKind.ENUM) { + return true; + } + TypeMirror erased = types.erasure(target.asType()); + for (String name : ASSIGNABLE_OBJECT_CODEC_EXCLUSIONS) { + TypeElement excluded = elements.getTypeElement(name); + if (excluded != null && types.isAssignable(erased, types.erasure(excluded.asType()))) { + return true; + } + } + for (String name : EXACT_OBJECT_CODEC_EXCLUSIONS) { + TypeElement excluded = elements.getTypeElement(name); + if (excluded != null && types.isSameType(erased, types.erasure(excluded.asType()))) { + return true; + } + } + return false; + } + + private Model buildModel(TypeElement target) { + String packageName = elements.getPackageOf(target).getQualifiedName().toString(); + String targetBinaryName = elements.getBinaryName(target).toString(); + String binarySimpleName = + targetBinaryName.substring(packageName.isEmpty() ? 0 : packageName.length() + 1); + String simpleName = GeneratedTypeNames.escapeBinarySimpleName(binarySimpleName) + SUFFIX; + Model model = + new Model( + target, + packageName, + targetBinaryName, + simpleName, + packageName.isEmpty() ? simpleName : packageName + "." + simpleName, + packageName.isEmpty() ? simpleName : packageName + "." + simpleName, + sourceType(target.asType())); + collectFieldAccessors(model); + collectMethodAccessors(model); + model.anySetter = findAnySetter(model); + if (model.anySetter != null) { + model.addR8Member( + model.anySetter.ownerBinaryName, + "void " + + model.anySetter.methodName + + "(java.lang.String," + + model.anySetter.memberValueBinaryType + + ");"); + } + model.creator = isRecord(target) ? recordCreator(model) : explicitCreator(model); + if (model.creator != null) { + model.addR8Member(model.targetBinaryName, model.creator.r8Declaration); + } + return model; + } + + private void collectFieldAccessors(Model model) { + List hierarchy = classHierarchy(model.target); + Collections.reverse(hierarchy); + for (TypeElement owner : hierarchy) { + for (VariableElement field : ElementFilter.fieldsIn(owner.getEnclosedElements())) { + Set modifiers = field.getModifiers(); + if (field.getKind() == ElementKind.ENUM_CONSTANT + || modifiers.contains(Modifier.STATIC) + || modifiers.contains(Modifier.TRANSIENT) + || binaryType(field.asType()).equals("java.lang.Class") + || !isAccessible(field, model.packageName) + || !isNameable(owner.asType(), model.packageName) + || !isNameable(types.erasure(field.asType()), model.packageName)) { + continue; + } + TypeMirror resolved = types.asMemberOf((DeclaredType) model.target.asType(), field); + model.accessors.add( + Accessor.forField( + model.accessors.size(), + sourceType(owner.asType()), + field.getSimpleName().toString(), + sourceType(types.erasure(resolved)), + sourceType(types.erasure(field.asType())), + resolved.getKind(), + !modifiers.contains(Modifier.FINAL))); + model.addR8Member( + elements.getBinaryName(owner).toString(), + binaryType(field.asType()) + " " + field.getSimpleName() + ";"); + } + } + } + + private void collectMethodAccessors(Model model) { + Set seen = new HashSet<>(); + for (ExecutableElement method : effectiveMethods(model.target)) { + if (!method.getModifiers().contains(Modifier.PUBLIC) + || method.getModifiers().contains(Modifier.STATIC) + || annotationMirror(method, JSON_ANY_SETTER) != null + || method.isVarArgs() + || !method.getTypeParameters().isEmpty()) { + continue; + } + TypeElement owner = (TypeElement) method.getEnclosingElement(); + if (!isNameable(owner.asType(), model.packageName)) { + continue; + } + ExecutableType resolved = + (ExecutableType) types.asMemberOf((DeclaredType) model.target.asType(), method); + String key = methodKey(owner, method); + if (isGetter(method, resolved) + && isNameable(types.erasure(resolved.getReturnType()), model.packageName) + && seen.add(key)) { + model.accessors.add( + Accessor.forGetter( + model.accessors.size(), + sourceType(owner.asType()), + method.getSimpleName().toString(), + sourceType(types.erasure(resolved.getReturnType())), + resolved.getReturnType().getKind())); + model.addR8Member( + elements.getBinaryName(owner).toString(), + binaryType(method.getReturnType()) + " " + method.getSimpleName() + "();"); + } else if (isSetter(method, resolved) + && isNameable(types.erasure(resolved.getParameterTypes().get(0)), model.packageName) + && isNameable(types.erasure(method.getParameters().get(0).asType()), model.packageName) + && seen.add(key)) { + model.accessors.add( + Accessor.forSetter( + model.accessors.size(), + sourceType(owner.asType()), + method.getSimpleName().toString(), + sourceType(types.erasure(resolved.getParameterTypes().get(0))), + sourceType(types.erasure(method.getParameters().get(0).asType())), + resolved.getParameterTypes().get(0).getKind())); + model.addR8Member( + elements.getBinaryName(owner).toString(), + "void " + + method.getSimpleName() + + "(" + + binaryType(method.getParameters().get(0).asType()) + + ");"); + } + } + if (isRecord(model.target)) { + for (Element component : recordComponents(model.target)) { + ExecutableElement accessor = findRecordAccessor(model.target, component); + if (accessor == null || !seen.add(methodKey(model.target, accessor))) { + continue; + } + TypeMirror type = types.erasure(component.asType()); + if (!isNameable(type, model.packageName)) { + throw invalid( + "Record component type is not accessible to generated JSON code", component); + } + model.accessors.add( + Accessor.forGetter( + model.accessors.size(), + model.targetType, + accessor.getSimpleName().toString(), + sourceType(type), + type.getKind())); + model.addR8Member( + model.targetBinaryName, + binaryType(accessor.getReturnType()) + " " + accessor.getSimpleName() + "();"); + } + } + } + + private AnySetter findAnySetter(Model model) { + ExecutableElement selected = null; + for (ExecutableElement method : effectiveMethods(model.target)) { + if (annotationMirror(method, JSON_ANY_SETTER) == null) { + continue; + } + if (selected != null) { + throw invalid("At most one effective @JsonAnySetter method is allowed", method); + } + selected = method; + } + if (selected == null) { + return null; + } + TypeElement owner = (TypeElement) selected.getEnclosingElement(); + ExecutableType resolved = + (ExecutableType) types.asMemberOf((DeclaredType) model.target.asType(), selected); + if (!selected.getModifiers().contains(Modifier.PUBLIC) + || selected.getModifiers().contains(Modifier.STATIC) + || selected.isVarArgs() + || !selected.getTypeParameters().isEmpty() + || resolved.getReturnType().getKind() != TypeKind.VOID + || resolved.getParameterTypes().size() != 2 + || !binaryType(resolved.getParameterTypes().get(0)).equals("java.lang.String") + || !binaryType(selected.getParameters().get(0).asType()).equals("java.lang.String") + || !isNameable(owner.asType(), model.packageName) + || !isNameable(types.erasure(resolved.getParameterTypes().get(1)), model.packageName) + || !isNameable( + types.erasure(selected.getParameters().get(1).asType()), model.packageName)) { + // Runtime metadata validation owns the diagnostic for malformed annotations. + return null; + } + return new AnySetter( + sourceType(owner.asType()), + elements.getBinaryName(owner).toString(), + selected.getSimpleName().toString(), + sourceType(types.erasure(selected.getParameters().get(1).asType())), + binaryType(selected.getParameters().get(1).asType()), + resolved.getParameterTypes().get(1).getKind()); + } + + private Creator recordCreator(Model model) { + List components = recordComponents(model.target); + List names = new ArrayList<>(); + List sourceTypes = new ArrayList<>(); + List binaryTypes = new ArrayList<>(); + List kinds = new ArrayList<>(); + for (Element component : components) { + TypeMirror type = types.erasure(component.asType()); + if (!isNameable(type, model.packageName)) { + throw invalid("Record component type is not accessible to generated JSON code", component); + } + names.add(component.getSimpleName().toString()); + sourceTypes.add(sourceType(type)); + binaryTypes.add(binaryType(type)); + kinds.add(type.getKind()); + } + for (ExecutableElement method : ElementFilter.methodsIn(model.target.getEnclosedElements())) { + if (annotationMirror(method, JSON_CREATOR) != null) { + throw invalid("Records cannot declare @JsonCreator", method); + } + } + boolean valueRecord = hasEffectiveJsonValue(model.target); + for (ExecutableElement constructor : + ElementFilter.constructorsIn(model.target.getEnclosedElements())) { + if (annotationMirror(constructor, JSON_CREATOR) != null + && (!valueRecord || !isRecordValueCreator(constructor, components))) { + throw invalid("Records cannot declare @JsonCreator", constructor); + } + } + String declaration = "(" + join(binaryTypes) + ");"; + return new Creator(names, sourceTypes, kinds, null, true, declaration, false); + } + + private boolean isRecordValueCreator( + ExecutableElement constructor, List components) { + if (!constructor.getModifiers().contains(Modifier.PUBLIC) + || constructor.isVarArgs() + || !constructor.getTypeParameters().isEmpty() + || components.size() != 1 + || constructor.getParameters().size() != 1 + || !binaryType(components.get(0).asType()).equals("java.lang.String") + || !types.isSameType( + types.erasure(constructor.getParameters().get(0).asType()), + types.erasure(components.get(0).asType())) + || annotationMirror(constructor.getParameters().get(0), JSON_PROPERTY) != null) { + return false; + } + AnnotationMirror creator = annotationMirror(constructor, JSON_CREATOR); + return stringArray(annotationValue(creator, "value")).isEmpty(); + } + + private Creator explicitCreator(Model model) { + ExecutableElement creator = null; + boolean factory = false; + for (ExecutableElement constructor : + ElementFilter.constructorsIn(model.target.getEnclosedElements())) { + if (annotationMirror(constructor, JSON_CREATOR) != null) { + if (creator != null) { + throw invalid("Exactly one @JsonCreator constructor or factory is allowed", constructor); + } + creator = constructor; + } + } + for (ExecutableElement method : ElementFilter.methodsIn(model.target.getEnclosedElements())) { + if (annotationMirror(method, JSON_CREATOR) != null) { + if (creator != null) { + throw invalid("Exactly one @JsonCreator constructor or factory is allowed", method); + } + creator = method; + factory = true; + } + } + if (creator == null) { + return null; + } + validateCreator(model, creator, factory); + List names = creatorParameterNames(creator); + List sourceTypes = new ArrayList<>(); + List binaryTypes = new ArrayList<>(); + List kinds = new ArrayList<>(); + for (VariableElement parameter : creator.getParameters()) { + TypeMirror type = types.erasure(parameter.asType()); + if (!isNameable(type, model.packageName)) { + throw invalid("Creator parameter type is not accessible to generated JSON code", parameter); + } + sourceTypes.add(sourceType(type)); + binaryTypes.add(binaryType(type)); + kinds.add(type.getKind()); + } + String executableName = factory ? creator.getSimpleName().toString() : null; + String declaration = + factory + ? binaryType(creator.getReturnType()) + + " " + + executableName + + "(" + + join(binaryTypes) + + ");" + : "(" + join(binaryTypes) + ");"; + return new Creator(names, sourceTypes, kinds, executableName, false, declaration, factory); + } + + private void validateCreator(Model model, ExecutableElement creator, boolean factory) { + Set modifiers = creator.getModifiers(); + if (!modifiers.contains(Modifier.PUBLIC) + || creator.isVarArgs() + || !creator.getTypeParameters().isEmpty() + || creator.getParameters().isEmpty()) { + throw invalid( + "@JsonCreator must be public, non-generic, non-varargs, and have parameters", creator); + } + if (!factory + && model.target.getNestingKind() == NestingKind.MEMBER + && !model.target.getModifiers().contains(Modifier.STATIC)) { + throw invalid("A non-static member class must use a static @JsonCreator factory", creator); + } + if (factory + && (!modifiers.contains(Modifier.STATIC) + || !types.isSameType( + types.erasure(creator.getReturnType()), types.erasure(model.target.asType())))) { + throw invalid("@JsonCreator factory must be static and return the exact model type", creator); + } + } + + private List creatorParameterNames(ExecutableElement creator) { + AnnotationMirror annotation = annotationMirror(creator, JSON_CREATOR); + AnnotationValue value = annotationValue(annotation, "value"); + List names = stringArray(value); + if (!names.isEmpty()) { + if (names.size() != creator.getParameters().size()) { + throw invalid("@JsonCreator property count must match its parameter count", creator); + } + Set unique = new HashSet<>(); + for (int i = 0; i < names.size(); i++) { + VariableElement parameter = creator.getParameters().get(i); + if (annotationMirror(parameter, JSON_PROPERTY) != null) { + throw invalid( + "Property-list @JsonCreator parameters cannot declare @JsonProperty", parameter); + } + String name = names.get(i); + if (name.isEmpty() || !unique.add(name)) { + throw invalid("@JsonCreator property names must be non-empty and unique", creator); + } + } + return names; + } + names = new ArrayList<>(); + Set unique = new HashSet<>(); + for (VariableElement parameter : creator.getParameters()) { + AnnotationMirror property = annotationMirror(parameter, JSON_PROPERTY); + AnnotationValue propertyName = property == null ? null : annotationValue(property, "value"); + String name = propertyName == null ? "" : String.valueOf(propertyName.getValue()); + if (name.isEmpty() || !unique.add(name)) { + throw invalid( + "Every parameter-local @JsonCreator parameter needs a unique @JsonProperty name", + parameter); + } + names.add(name); + } + return names; + } + + private String render(Model model) { + StringBuilder source = new StringBuilder(16384); + source.append("// Generated by Apache Fory. Do not edit.\n"); + if (!model.packageName.isEmpty()) { + source.append("package ").append(model.packageName).append(";\n\n"); + } + source + .append("public final class ") + .append(model.simpleName) + .append(" extends org.apache.fory.json.codec.GeneratedJsonCodec<") + .append(model.targetType) + .append("> {\n") + .append(" public ") + .append(model.simpleName) + .append("() {}\n\n") + .append(" @Override\n") + .append(" public Class<") + .append(model.targetType) + .append("> type() {\n") + .append(" return ") + .append(model.targetType) + .append(".class;\n") + .append(" }\n\n") + .append(" @Override\n") + .append(" public org.apache.fory.json.meta.JsonFieldAccessor[] fieldAccessors() {\n") + .append(" return new org.apache.fory.json.meta.JsonFieldAccessor[] {"); + for (int i = 0; i < model.accessors.size(); i++) { + if (i != 0) { + source.append(", "); + } + source.append("new Accessor").append(i).append("()"); + } + source.append("};\n }\n\n"); + if (model.anySetter != null) { + source + .append(" @Override\n") + .append( + " public org.apache.fory.json.meta.JsonAnySetterAccessor anySetterAccessor() {\n") + .append(" return new AnySetterAccessor();\n") + .append(" }\n\n"); + } + if (model.creator != null) { + renderCreator(source, model); + } + source + .append( + " private static java.lang.reflect.Field declaredField(Class owner, String name) {\n") + .append(" try {\n") + .append(" return owner.getDeclaredField(name);\n") + .append(" } catch (Exception e) {\n") + .append(" throw new ExceptionInInitializerError(e);\n") + .append(" }\n") + .append(" }\n\n") + .append(" private static java.lang.reflect.Method declaredMethod(\n") + .append(" Class owner, String name, Class... parameterTypes) {\n") + .append(" try {\n") + .append(" return owner.getDeclaredMethod(name, parameterTypes);\n") + .append(" } catch (Exception e) {\n") + .append(" throw new ExceptionInInitializerError(e);\n") + .append(" }\n") + .append(" }\n\n"); + for (Accessor accessor : model.accessors) { + renderAccessor(source, model, accessor); + } + if (model.anySetter != null) { + renderAnySetter(source, model.anySetter); + } + source + .append(" public static final class Factory\n") + .append(" implements org.apache.fory.json.codec.GeneratedJsonCodecFactory {\n") + .append(" public Factory() {}\n\n") + .append(" @Override\n") + .append(" public org.apache.fory.json.codec.GeneratedJsonCodec create() {\n") + .append(" return new ") + .append(model.simpleName) + .append("();\n") + .append(" }\n") + .append(" }\n") + .append("}\n"); + return source.toString(); + } + + private void renderCreator(StringBuilder source, Model model) { + Creator creator = model.creator; + source + .append(" @Override\n") + .append(" public String[] creatorParameterNames() {\n") + .append(" return new String[] {"); + appendStrings(source, creator.names); + source + .append("};\n }\n\n") + .append(" @Override\n") + .append(" public Class[] creatorParameterTypes() {\n") + .append(" return new Class[] {"); + appendClassLiterals(source, creator.sourceTypes); + source.append("};\n }\n\n"); + if (creator.factoryName != null) { + source + .append(" @Override\n") + .append(" public String creatorFactoryName() {\n") + .append(" return \"") + .append(escapeJava(creator.factoryName)) + .append("\";\n") + .append(" }\n\n"); + } + if (creator.record) { + source + .append(" @Override\n") + .append(" public boolean isRecord() {\n") + .append(" return true;\n") + .append(" }\n\n"); + } + source + .append(" @Override\n") + .append(" public ") + .append(model.targetType) + .append(" newInstance(Object[] arguments) {\n") + .append(" try {\n") + .append(" return "); + if (creator.factory) { + source.append(model.targetType).append('.').append(creator.factoryName).append('('); + } else { + source.append("new ").append(model.targetType).append('('); + } + for (int i = 0; i < creator.sourceTypes.size(); i++) { + if (i != 0) { + source.append(", "); + } + source.append(argumentExpression(creator.sourceTypes.get(i), creator.kinds.get(i), i)); + } + source + .append(");\n") + .append(" } catch (Throwable throwable) {\n") + .append(" throw creatorFailure(throwable);\n") + .append(" }\n") + .append(" }\n\n"); + } + + private void renderAccessor(StringBuilder source, Model model, Accessor accessor) { + // The direct expression must be compiled against the same declaring class and erased value + // type as MEMBER. Using the model subtype or a resolved generic type can select a hidden field + // or a more-specific overload. A declaring-class method receiver still dispatches virtually, + // matching Method.invoke and MethodHandle invocation for real overrides. + source + .append(" private static final class Accessor") + .append(accessor.index) + .append(" extends org.apache.fory.json.meta.JsonFieldAccessor {\n"); + if (accessor.kind == AccessorKind.FIELD) { + source + .append(" private static final java.lang.reflect.Field MEMBER =\n") + .append(" declaredField(") + .append(accessor.ownerType) + .append(".class, \"") + .append(escapeJava(accessor.memberName)) + .append("\");\n\n") + .append(" @Override\n") + .append(" public java.lang.reflect.Field field() {\n") + .append(" return MEMBER;\n") + .append(" }\n\n"); + } else { + source + .append(" private static final java.lang.reflect.Method MEMBER =\n") + .append(" declaredMethod(") + .append(accessor.ownerType) + .append(".class, \"") + .append(escapeJava(accessor.memberName)) + .append("\""); + if (accessor.kind == AccessorKind.SETTER) { + source.append(", ").append(accessor.declaredValueType).append(".class"); + } + source.append(");\n\n"); + source + .append(" @Override\n") + .append(" public java.lang.reflect.Method ") + .append(accessor.kind == AccessorKind.GETTER ? "getter" : "setter") + .append("() {\n") + .append(" return MEMBER;\n") + .append(" }\n\n"); + } + if (accessor.readable()) { + renderGet(source, accessor); + } + if (accessor.writable()) { + renderPut(source, accessor); + } + source.append(" }\n\n"); + } + + private void renderGet(StringBuilder source, Accessor accessor) { + String suffix = primitiveSuffix(accessor.valueKind); + String method = suffix == null ? "getObject" : "get" + suffix; + source + .append(" @Override\n") + .append(" public ") + .append(suffix == null ? "Object" : accessor.valueType) + .append(' ') + .append(method) + .append("(Object target) {\n"); + if (accessor.kind == AccessorKind.FIELD) { + source.append(" return "); + appendReadCast(source, accessor); + source + .append("((") + .append(accessor.ownerType) + .append(") target).") + .append(accessor.memberName) + .append(";\n"); + } else { + source.append(" try {\n").append(" return "); + appendReadCast(source, accessor); + source + .append("((") + .append(accessor.ownerType) + .append(") target).") + .append(accessor.memberName) + .append("();\n") + .append(" } catch (Throwable throwable) {\n") + .append(" throw accessException(MEMBER, throwable);\n") + .append(" }\n"); + } + source.append(" }\n\n"); + if (suffix != null) { + source + .append(" @Override\n") + .append(" public Object getObject(Object target) {\n") + .append(" return ") + .append(method) + .append("(target);\n") + .append(" }\n\n"); + } + } + + private void appendReadCast(StringBuilder source, Accessor accessor) { + if (!accessor.valueKind.isPrimitive()) { + source.append('(').append(accessor.valueType).append(") "); + } + } + + private void renderPut(StringBuilder source, Accessor accessor) { + String suffix = primitiveSuffix(accessor.valueKind); + String method = suffix == null ? "putObject" : "put" + suffix; + source + .append(" @Override\n") + .append(" public void ") + .append(method) + .append("(Object target, ") + .append(suffix == null ? "Object" : accessor.valueType) + .append(" value) {\n"); + String value = suffix == null ? "(" + accessor.declaredValueType + ") value" : "value"; + if (accessor.kind == AccessorKind.FIELD) { + source + .append(" ((") + .append(accessor.ownerType) + .append(") target).") + .append(accessor.memberName) + .append(" = ") + .append(value) + .append(";\n"); + } else { + source + .append(" try {\n") + .append(" ((") + .append(accessor.ownerType) + .append(") target).") + .append(accessor.memberName) + .append('(') + .append(value) + .append(");\n") + .append(" } catch (Throwable throwable) {\n") + .append(" throw accessException(MEMBER, throwable);\n") + .append(" }\n"); + } + source.append(" }\n\n"); + if (suffix != null) { + source + .append(" @Override\n") + .append(" public void putObject(Object target, Object value) {\n") + .append(" ") + .append(method) + .append("(target, ") + .append(unboxExpression(accessor.valueType, "value")) + .append(");\n") + .append(" }\n\n"); + } + } + + private void renderAnySetter(StringBuilder source, AnySetter setter) { + source + .append(" private static final class AnySetterAccessor\n") + .append(" extends org.apache.fory.json.meta.JsonAnySetterAccessor {\n") + .append(" private static final java.lang.reflect.Method MEMBER =\n") + .append(" declaredMethod(") + .append(setter.ownerType) + .append(".class, \"") + .append(escapeJava(setter.methodName)) + .append("\", String.class, ") + .append(setter.memberValueType) + .append(".class);\n\n") + .append(" @Override\n") + .append(" public java.lang.reflect.Method setter() {\n") + .append(" return MEMBER;\n") + .append(" }\n\n") + .append(" @Override\n") + .append(" public void put(Object target, String name, Object value) {\n") + .append(" try {\n") + .append(" ((") + .append(setter.ownerType) + .append(") target).") + .append(setter.methodName) + .append("(name, ") + .append( + setter.valueKind.isPrimitive() + ? unboxExpression(setter.memberValueType, "value") + : "(" + setter.memberValueType + ") value") + .append(");\n") + .append(" } catch (Throwable throwable) {\n") + .append(" throw accessException(MEMBER, throwable);\n") + .append(" }\n") + .append(" }\n") + .append(" }\n\n"); + } + + private List effectiveMethods(TypeElement target) { + Map methods = new LinkedHashMap<>(); + for (ExecutableElement method : ElementFilter.methodsIn(elements.getAllMembers(target))) { + TypeElement owner = (TypeElement) method.getEnclosingElement(); + methods.put(methodKey(owner, method), method); + } + List result = new ArrayList<>(methods.values()); + result.sort( + Comparator.comparing( + method -> methodKey((TypeElement) method.getEnclosingElement(), method))); + return result; + } + + private boolean isGetter(ExecutableElement method, ExecutableType resolved) { + if (!resolved.getParameterTypes().isEmpty() + || resolved.getReturnType().getKind() == TypeKind.VOID + || binaryType(resolved.getReturnType()).equals("java.lang.Class")) { + return false; + } + String name = method.getSimpleName().toString(); + return annotationMirror(method, JSON_PROPERTY) != null + || annotationMirror(method, JSON_ANY_GETTER) != null + || name.startsWith("get") && name.length() > 3 + || name.startsWith("is") + && name.length() > 2 + && (resolved.getReturnType().getKind() == TypeKind.BOOLEAN + || binaryType(resolved.getReturnType()).equals("java.lang.Boolean")); + } + + private boolean isSetter(ExecutableElement method, ExecutableType resolved) { + if (resolved.getParameterTypes().size() != 1 + || resolved.getReturnType().getKind() != TypeKind.VOID + || binaryType(resolved.getParameterTypes().get(0)).equals("java.lang.Class")) { + return false; + } + String name = method.getSimpleName().toString(); + return annotationMirror(method, JSON_PROPERTY) != null + || name.startsWith("set") && name.length() > 3; + } + + private List classHierarchy(TypeElement target) { + List hierarchy = new ArrayList<>(); + TypeElement current = target; + while (current != null && !current.getQualifiedName().contentEquals("java.lang.Object")) { + hierarchy.add(current); + TypeMirror superclass = current.getSuperclass(); + current = superclass.getKind() == TypeKind.NONE ? null : asTypeElement(superclass); + } + return hierarchy; + } + + private List allDeclarations(TypeElement target) { + LinkedHashMap owners = new LinkedHashMap<>(); + Deque pending = new ArrayDeque<>(); + pending.add(target); + while (!pending.isEmpty()) { + TypeElement type = pending.removeFirst(); + if (owners.put(elements.getBinaryName(type).toString(), type) != null) { + continue; + } + TypeMirror superclass = type.getSuperclass(); + TypeElement superType = + superclass.getKind() == TypeKind.NONE ? null : asTypeElement(superclass); + if (superType != null && !superType.getQualifiedName().contentEquals("java.lang.Object")) { + pending.add(superType); + } + for (TypeMirror interfaceType : type.getInterfaces()) { + TypeElement interfaceElement = asTypeElement(interfaceType); + if (interfaceElement != null) { + pending.add(interfaceElement); + } + } + } + return new ArrayList<>(owners.values()); + } + + @SuppressWarnings("unchecked") + private List recordComponents(TypeElement target) { + try { + Method method = TypeElement.class.getMethod("getRecordComponents"); + return (List) method.invoke(target); + } catch (ReflectiveOperationException e) { + throw invalid("Cannot inspect Record components: " + e, target); + } + } + + private ExecutableElement findRecordAccessor(TypeElement target, Element component) { + for (ExecutableElement method : ElementFilter.methodsIn(target.getEnclosedElements())) { + if (method.getSimpleName().contentEquals(component.getSimpleName()) + && method.getParameters().isEmpty()) { + return method; + } + } + return null; + } + + private boolean isRecord(TypeElement type) { + return type.getKind().name().equals("RECORD"); + } + + private boolean isAccessible(Element element, String generatedPackage) { + Set modifiers = element.getModifiers(); + if (modifiers.contains(Modifier.PUBLIC)) { + return true; + } + if (modifiers.contains(Modifier.PRIVATE)) { + return false; + } + TypeElement owner = (TypeElement) element.getEnclosingElement(); + return elements.getPackageOf(owner).getQualifiedName().contentEquals(generatedPackage); + } + + private boolean isNameable(TypeMirror type, String generatedPackage) { + TypeMirror erased = types.erasure(type); + if (erased.getKind().isPrimitive()) { + return true; + } + if (erased.getKind() == TypeKind.ARRAY) { + return isNameable(((ArrayType) erased).getComponentType(), generatedPackage); + } + TypeElement element = asTypeElement(erased); + if (element == null || !isNameable(element)) { + return false; + } + if (elements.getPackageOf(element).getQualifiedName().contentEquals(generatedPackage)) { + return true; + } + Element current = element; + while (current instanceof TypeElement) { + if (!current.getModifiers().contains(Modifier.PUBLIC)) { + return false; + } + current = current.getEnclosingElement(); + } + return true; + } + + private boolean isNameable(TypeElement type) { + NestingKind nesting = type.getNestingKind(); + if (nesting != NestingKind.TOP_LEVEL && nesting != NestingKind.MEMBER) { + return false; + } + Element current = type; + while (current instanceof TypeElement) { + if (current.getModifiers().contains(Modifier.PRIVATE)) { + return false; + } + current = current.getEnclosingElement(); + } + return true; + } + + private String sourceType(TypeMirror type) { + TypeMirror erased = types.erasure(type); + if (erased.getKind() == TypeKind.ARRAY) { + return sourceType(((ArrayType) erased).getComponentType()) + "[]"; + } + if (erased.getKind().isPrimitive() || erased.getKind() == TypeKind.VOID) { + return erased.toString(); + } + TypeElement element = asTypeElement(erased); + return element == null ? erased.toString() : element.getQualifiedName().toString(); + } + + private String binaryType(TypeMirror type) { + TypeMirror erased = types.erasure(type); + if (erased.getKind() == TypeKind.ARRAY) { + return binaryType(((ArrayType) erased).getComponentType()) + "[]"; + } + if (erased.getKind().isPrimitive() || erased.getKind() == TypeKind.VOID) { + return erased.toString(); + } + TypeElement element = asTypeElement(erased); + return element == null ? erased.toString() : elements.getBinaryName(element).toString(); + } + + private TypeElement asTypeElement(TypeMirror type) { + Element element = types.asElement(type); + return element instanceof TypeElement ? (TypeElement) element : null; + } + + private AnnotationMirror annotationMirror(Element element, String annotationName) { + for (AnnotationMirror mirror : element.getAnnotationMirrors()) { + Element annotationType = mirror.getAnnotationType().asElement(); + if (annotationType instanceof TypeElement + && ((TypeElement) annotationType).getQualifiedName().contentEquals(annotationName)) { + return mirror; + } + } + return null; + } + + private AnnotationValue annotationValue(AnnotationMirror annotation, String name) { + for (Map.Entry entry : + elements.getElementValuesWithDefaults(annotation).entrySet()) { + if (entry.getKey().getSimpleName().contentEquals(name)) { + return entry.getValue(); + } + } + return null; + } + + private List stringArray(AnnotationValue value) { + if (value == null || !(value.getValue() instanceof List)) { + return Collections.emptyList(); + } + List strings = new ArrayList<>(); + for (Object entry : (List) value.getValue()) { + if (entry instanceof AnnotationValue) { + strings.add(String.valueOf(((AnnotationValue) entry).getValue())); + } + } + return strings; + } + + private String methodKey(TypeElement owner, ExecutableElement method) { + StringBuilder key = + new StringBuilder(elements.getBinaryName(owner).toString()) + .append('#') + .append(method.getSimpleName()) + .append('('); + for (VariableElement parameter : method.getParameters()) { + key.append(binaryType(parameter.asType())).append(';'); + } + return key.append(')').toString(); + } + + private static String primitiveSuffix(TypeKind kind) { + switch (kind) { + case BOOLEAN: + return "Boolean"; + case BYTE: + return "Byte"; + case SHORT: + return "Short"; + case INT: + return "Int"; + case LONG: + return "Long"; + case CHAR: + return "Char"; + case FLOAT: + return "Float"; + case DOUBLE: + return "Double"; + default: + return null; + } + } + + private static String argumentExpression(String type, TypeKind kind, int index) { + String value = "arguments[" + index + "]"; + return kind.isPrimitive() ? unboxExpression(type, value) : "(" + type + ") " + value; + } + + private static String unboxExpression(String type, String value) { + if (type.equals("boolean")) { + return "((Boolean) " + value + ").booleanValue()"; + } + if (type.equals("byte")) { + return "((Byte) " + value + ").byteValue()"; + } + if (type.equals("short")) { + return "((Short) " + value + ").shortValue()"; + } + if (type.equals("int")) { + return "((Integer) " + value + ").intValue()"; + } + if (type.equals("long")) { + return "((Long) " + value + ").longValue()"; + } + if (type.equals("char")) { + return "((Character) " + value + ").charValue()"; + } + if (type.equals("float")) { + return "((Float) " + value + ").floatValue()"; + } + if (type.equals("double")) { + return "((Double) " + value + ").doubleValue()"; + } + throw new IllegalArgumentException("Not a primitive type: " + type); + } + + private static void appendStrings(StringBuilder builder, List values) { + for (int i = 0; i < values.size(); i++) { + if (i != 0) { + builder.append(", "); + } + builder.append('"').append(escapeJava(values.get(i))).append('"'); + } + } + + private static void appendClassLiterals(StringBuilder builder, List values) { + for (int i = 0; i < values.size(); i++) { + if (i != 0) { + builder.append(", "); + } + builder.append(values.get(i)).append(".class"); + } + } + + private static String escapeJava(String value) { + StringBuilder builder = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + switch (ch) { + case '\b': + builder.append("\\b"); + break; + case '\t': + builder.append("\\t"); + break; + case '\n': + builder.append("\\n"); + break; + case '\f': + builder.append("\\f"); + break; + case '\r': + builder.append("\\r"); + break; + case '"': + builder.append("\\\""); + break; + case '\\': + builder.append("\\\\"); + break; + default: + if (Character.isISOControl(ch) || ch == '\u2028' || ch == '\u2029') { + builder.append("\\u"); + String hex = Integer.toHexString(ch); + for (int padding = hex.length(); padding < 4; padding++) { + builder.append('0'); + } + builder.append(hex); + } else { + builder.append(ch); + } + } + } + return builder.toString(); + } + + private static String join(List values) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < values.size(); i++) { + if (i != 0) { + builder.append(','); + } + builder.append(values.get(i)); + } + return builder.toString(); + } + + private InvalidJsonTypeException invalid(String message, Element element) { + return new InvalidJsonTypeException(message, element); + } + + static final class Result { + final String companionBinaryName; + final List r8Members; + final boolean hasAnySetter; + final boolean hasCreator; + final boolean hasCreatorFactory; + final boolean record; + + Result( + String companionBinaryName, + List r8Members, + boolean hasAnySetter, + boolean hasCreator, + boolean hasCreatorFactory, + boolean record) { + this.companionBinaryName = companionBinaryName; + this.r8Members = r8Members; + this.hasAnySetter = hasAnySetter; + this.hasCreator = hasCreator; + this.hasCreatorFactory = hasCreatorFactory; + this.record = record; + } + } + + static final class MemberRule { + final String ownerBinaryName; + final String declaration; + + MemberRule(String ownerBinaryName, String declaration) { + this.ownerBinaryName = ownerBinaryName; + this.declaration = declaration; + } + } + + private static final class Model { + final TypeElement target; + final String packageName; + final String targetBinaryName; + final String simpleName; + final String qualifiedName; + final String binaryName; + final String targetType; + final List accessors = new ArrayList<>(); + final List r8Members = new ArrayList<>(); + final Set r8MemberKeys = new HashSet<>(); + AnySetter anySetter; + Creator creator; + + Model( + TypeElement target, + String packageName, + String targetBinaryName, + String simpleName, + String qualifiedName, + String binaryName, + String targetType) { + this.target = target; + this.packageName = packageName; + this.targetBinaryName = targetBinaryName; + this.simpleName = simpleName; + this.qualifiedName = qualifiedName; + this.binaryName = binaryName; + this.targetType = targetType; + } + + void addR8Member(String ownerBinaryName, String declaration) { + if (r8MemberKeys.add(ownerBinaryName + "#" + declaration)) { + r8Members.add(new MemberRule(ownerBinaryName, declaration)); + } + } + } + + private enum AccessorKind { + FIELD, + GETTER, + SETTER + } + + private static final class Accessor { + final int index; + final AccessorKind kind; + final String ownerType; + final String memberName; + final String valueType; + final String declaredValueType; + final TypeKind valueKind; + final boolean mutable; + + private Accessor( + int index, + AccessorKind kind, + String ownerType, + String memberName, + String valueType, + String declaredValueType, + TypeKind valueKind, + boolean mutable) { + this.index = index; + this.kind = kind; + this.ownerType = ownerType; + this.memberName = memberName; + this.valueType = valueType; + this.declaredValueType = declaredValueType; + this.valueKind = valueKind; + this.mutable = mutable; + } + + static Accessor forField( + int index, + String ownerType, + String memberName, + String valueType, + String declaredValueType, + TypeKind valueKind, + boolean mutable) { + return new Accessor( + index, + AccessorKind.FIELD, + ownerType, + memberName, + valueType, + declaredValueType, + valueKind, + mutable); + } + + static Accessor forGetter( + int index, String ownerType, String memberName, String valueType, TypeKind valueKind) { + return new Accessor( + index, + AccessorKind.GETTER, + ownerType, + memberName, + valueType, + valueType, + valueKind, + false); + } + + static Accessor forSetter( + int index, + String ownerType, + String memberName, + String valueType, + String declaredValueType, + TypeKind valueKind) { + return new Accessor( + index, + AccessorKind.SETTER, + ownerType, + memberName, + valueType, + declaredValueType, + valueKind, + true); + } + + boolean readable() { + return kind != AccessorKind.SETTER; + } + + boolean writable() { + return kind == AccessorKind.SETTER || kind == AccessorKind.FIELD && mutable; + } + } + + private static final class AnySetter { + final String ownerType; + final String ownerBinaryName; + final String methodName; + final String memberValueType; + final String memberValueBinaryType; + final TypeKind valueKind; + + AnySetter( + String ownerType, + String ownerBinaryName, + String methodName, + String memberValueType, + String memberValueBinaryType, + TypeKind valueKind) { + this.ownerType = ownerType; + this.ownerBinaryName = ownerBinaryName; + this.methodName = methodName; + this.memberValueType = memberValueType; + this.memberValueBinaryType = memberValueBinaryType; + this.valueKind = valueKind; + } + } + + private static final class Creator { + final List names; + final List sourceTypes; + final List kinds; + final String factoryName; + final boolean record; + final String r8Declaration; + final boolean factory; + + Creator( + List names, + List sourceTypes, + List kinds, + String factoryName, + boolean record, + String r8Declaration, + boolean factory) { + this.names = names; + this.sourceTypes = sourceTypes; + this.kinds = kinds; + this.factoryName = factoryName; + this.record = record; + this.r8Declaration = r8Declaration; + this.factory = factory; + } + } + + static final class InvalidJsonTypeException extends RuntimeException { + final Element element; + + InvalidJsonTypeException(String message, Element element) { + super(message); + this.element = element; + } + } +} diff --git a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedTypeNames.java b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedTypeNames.java new file mode 100644 index 0000000000..12398eedd0 --- /dev/null +++ b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedTypeNames.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.annotation.processing; + +/** Collision-free generated names for the dependency-free annotation processor. */ +final class GeneratedTypeNames { + private GeneratedTypeNames() {} + + static String escapeBinarySimpleName(String binarySimpleName) { + return escape(binarySimpleName, false); + } + + static String escapeBinaryName(String binaryName) { + return escape(binaryName, true); + } + + private static String escape(String value, boolean preserveDots) { + // Keep this encoding identical to fory-core GeneratedClassNames. The processor intentionally + // has no runtime dependency, so generated source names need this local build-time owner. + StringBuilder builder = new StringBuilder(value.length() + 32); + for (int i = 0; i < value.length(); ) { + int codePoint = value.codePointAt(i); + if (codePoint == '$') { + builder.append("_d_"); + } else if (codePoint == '_') { + builder.append("_u_"); + } else if (preserveDots && codePoint == '.') { + builder.append('.'); + } else if (Character.isJavaIdentifierPart(codePoint)) { + builder.appendCodePoint(codePoint); + } else { + builder.append("_x").append(Integer.toHexString(codePoint)).append('_'); + } + i += Character.charCount(codePoint); + } + return builder.toString(); + } +} diff --git a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java index 980a5a1d2e..0c45462c7e 100644 --- a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java +++ b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java @@ -74,6 +74,8 @@ final class JsonTypeProcessor { private static final String NO_JSON_VALUE_CODEC = JSON_CODEC + "$NoJsonValueCodec"; private static final String NO_MAP_KEY_CODEC = JSON_CODEC + "$NoMapKeyCodec"; private static final String R8_PREFIX = "META-INF/proguard/fory-json-"; + private static final String NATIVE_IMAGE_PREFIX = + "META-INF/native-image/org.apache.fory/fory-json-"; private static final String[] CODEC_MEMBERS = { "value", "elementCodec", "contentCodec", "keyCodec", "valueCodec" }; @@ -82,6 +84,7 @@ final class JsonTypeProcessor { private final Messager messager; private final Elements elements; private final Types types; + private final GeneratedJsonCodecSourceWriter codecSourceWriter; private final Set processedTypes = new HashSet<>(); JsonTypeProcessor(ProcessingEnvironment environment) { @@ -89,6 +92,7 @@ final class JsonTypeProcessor { messager = environment.getMessager(); elements = environment.getElementUtils(); types = environment.getTypeUtils(); + codecSourceWriter = new GeneratedJsonCodecSourceWriter(environment); } void process(RoundEnvironment roundEnvironment) { @@ -110,10 +114,26 @@ void process(RoundEnvironment roundEnvironment) { } try { Model model = inspect(type); + if (hasAnnotation(type, JSON_TYPE)) { + GeneratedJsonCodecSourceWriter.Result generated = codecSourceWriter.write(type); + if (generated != null) { + model.companionBinaryName = generated.companionBinaryName; + model.companionHasAnySetter = generated.hasAnySetter; + model.companionHasCreator = generated.hasCreator; + model.companionHasCreatorFactory = generated.hasCreatorFactory; + model.companionIsRecord = generated.record; + for (GeneratedJsonCodecSourceWriter.MemberRule member : generated.r8Members) { + model.addR8Member(new R8Member(member.ownerBinaryName, member.declaration)); + } + } + } List subtypes = classLiteralSubtypes(type, model.binaryFallbackTypes); model.sort(); emitR8(model); + emitNativeImageProperties(model); pending.addAll(subtypes); + } catch (GeneratedJsonCodecSourceWriter.InvalidJsonTypeException e) { + messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), e.element); } catch (InvalidJsonTypeException e) { messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), e.element); } catch (RuntimeException e) { @@ -246,6 +266,27 @@ private void emitR8(Model model) { } } + private void emitNativeImageProperties(Model model) { + if (model.companionBinaryName == null) { + return; + } + // The hosted feature freezes factory instances into the image heap after reachability is + // known, but GraalVM accepts class-initialization configuration only before analysis starts. + // Emit the exact generated class here so unreachable model classes remain removable. + String resourceName = + NATIVE_IMAGE_PREFIX + model.companionBinaryName + "/native-image.properties"; + try { + javax.tools.FileObject file = + filer.createResource(StandardLocation.CLASS_OUTPUT, "", resourceName, model.target); + try (Writer writer = file.openWriter()) { + writer.write("Args=--initialize-at-build-time=" + model.companionBinaryName + "$Factory\n"); + } + } catch (IOException e) { + throw new InvalidJsonTypeException( + "Failed to write generated JSON Native Image properties: " + e, model.target); + } + } + private String writeR8(Model model) { StringBuilder builder = new StringBuilder(8192); builder.append("-keepattributes Signature,RuntimeVisibleAnnotations\n"); @@ -267,7 +308,9 @@ private String writeR8(Model model) { .add(member); } for (Map.Entry> entry : membersByOwner.entrySet()) { - boolean preserveName = model.binaryFallbackTypes.contains(entry.getKey()); + boolean preserveName = + model.binaryFallbackTypes.contains(entry.getKey()) + || model.companionBinaryName != null && model.binaryName.equals(entry.getKey()); builder .append("-keep,allowoptimization") .append(preserveName ? "" : ",allowobfuscation") @@ -317,6 +360,32 @@ private String writeR8(Model model) { .append(codec) .append(" { public (); }\n"); } + if (model.companionBinaryName != null) { + builder + .append("-keep,allowoptimization class ") + .append(model.companionBinaryName) + .append(" {\n") + .append(" public ();\n") + .append(" public java.lang.Class type();\n") + .append(" public org.apache.fory.json.meta.JsonFieldAccessor[] fieldAccessors();\n"); + if (model.companionHasAnySetter) { + builder.append( + " public org.apache.fory.json.meta.JsonAnySetterAccessor anySetterAccessor();\n"); + } + if (model.companionHasCreator) { + builder + .append(" public java.lang.String[] creatorParameterNames();\n") + .append(" public java.lang.Class[] creatorParameterTypes();\n") + .append(" public java.lang.Object newInstance(java.lang.Object[]);\n"); + } + if (model.companionHasCreatorFactory) { + builder.append(" public java.lang.String creatorFactoryName();\n"); + } + if (model.companionIsRecord) { + builder.append(" public boolean isRecord();\n"); + } + builder.append("}\n"); + } return builder.toString(); } @@ -702,6 +771,11 @@ private static final class Model { final Set codecTypes = new LinkedHashSet<>(); final Set binaryFallbackTypes = new LinkedHashSet<>(); final Set annotationOwnerTypes = new LinkedHashSet<>(); + String companionBinaryName; + boolean companionHasAnySetter; + boolean companionHasCreator; + boolean companionHasCreatorFactory; + boolean companionIsRecord; Model(TypeElement target, String binaryName) { this.target = target; diff --git a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/ForyStructProcessorTest.java b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/ForyStructProcessorTest.java index 3e915e9239..380297f698 100644 --- a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/ForyStructProcessorTest.java +++ b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/ForyStructProcessorTest.java @@ -355,8 +355,8 @@ public void testInnerTypeGeneratedAsTopLevelBinaryTail() throws Exception { + "}\n"); Assert.assertTrue(result.success, result.diagnostics()); try (URLClassLoader loader = result.classLoader()) { - Class xlangSerializer = loader.loadClass("test.Outer_Inner_ForySerializer"); - Class nativeSerializer = loader.loadClass("test.Outer_Inner_ForyNativeSerializer"); + Class xlangSerializer = loader.loadClass("test.Outer_d_Inner_ForySerializer"); + Class nativeSerializer = loader.loadClass("test.Outer_d_Inner_ForyNativeSerializer"); Assert.assertTrue(StaticGeneratedStructSerializer.class.isAssignableFrom(xlangSerializer)); Assert.assertTrue(StaticGeneratedStructSerializer.class.isAssignableFrom(nativeSerializer)); } @@ -375,7 +375,7 @@ public void testGeneratedNameCollisionFailsCompilation() throws Exception { + " public Inner() {}\n" + " }\n" + "}\n" - + "class Outer_Inner_ForySerializer {}\n"); + + "class Outer_d_Inner_ForySerializer {}\n"); Assert.assertFalse(result.success); Assert.assertTrue(result.diagnostics().contains("collides"), result.diagnostics()); } diff --git a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java index 5dafe68c90..a6e6d46171 100644 --- a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java +++ b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java @@ -22,9 +22,14 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNotSame; import static org.testng.Assert.assertTrue; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -40,11 +45,19 @@ import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; +import org.apache.fory.json.ForyJson; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.GeneratedJsonCodec; +import org.apache.fory.json.codec.GeneratedJsonCodecFactory; +import org.apache.fory.json.meta.JsonAnySetterAccessor; +import org.apache.fory.json.meta.JsonFieldAccessor; import org.testng.SkipException; import org.testng.annotations.Test; public class JsonTypeProcessorTest { private static final String RULE_PREFIX = "META-INF/proguard/fory-json-"; + private static final String NATIVE_IMAGE_PREFIX = + "META-INF/native-image/org.apache.fory/fory-json-"; @Test public void unannotatedType() throws Exception { @@ -77,6 +90,568 @@ public void jsonTypeRules() throws Exception { assertTrue(rules.contains("int getId();"), rules); assertTrue(rules.contains("();"), rules); assertTrue(rules.contains("(int);"), rules); + assertTrue(rules.contains("-keep,allowoptimization class test.Plain_ForyJsonCodec {"), rules); + assertFalse(rules.contains("test.Plain_ForyJsonCodec$Factory"), rules); + assertFalse(rules.contains("-keep,allowoptimization,allowobfuscation class test.Plain"), rules); + assertTrue(result.hasGeneratedSource("test/Plain_ForyJsonCodec.java")); + assertEquals( + result.generatedResource( + NATIVE_IMAGE_PREFIX + "test.Plain_ForyJsonCodec/native-image.properties"), + "Args=--initialize-at-build-time=test.Plain_ForyJsonCodec$Factory\n"); + } + + @Test + public void generatedAccessors() throws Exception { + CompilationResult result = + compile( + "test.GeneratedModel", + "package test;\n" + + "import java.util.*;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public class GeneratedModel {\n" + + " public int id;\n" + + " private String name;\n" + + " public final Map extra = new HashMap<>();\n" + + " public GeneratedModel() {}\n" + + " public String getName() { return name; }\n" + + " public void setName(String name) { this.name = name; }\n" + + " @JsonAnySetter public void putExtra(String key, Object value) {\n" + + " extra.put(key, value);\n" + + " }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + ClassLoader loader = result.classLoader(); + Class modelType = loader.loadClass("test.GeneratedModel"); + GeneratedJsonCodec codec = generatedCodec(loader, "test.GeneratedModel_ForyJsonCodec"); + assertEquals(codec.type(), modelType); + JsonFieldAccessor[] first = codec.fieldAccessors(); + JsonFieldAccessor[] second = codec.fieldAccessors(); + assertNotSame(first, second); + + Object model = modelType.getConstructor().newInstance(); + JsonFieldAccessor id = fieldAccessor(first, "id"); + id.putInt(model, 31); + assertEquals(id.getInt(model), 31); + JsonFieldAccessor setter = methodAccessor(first, "setName"); + JsonFieldAccessor getter = methodAccessor(first, "getName"); + setter.putObject(model, "fory"); + assertEquals(getter.getObject(model), "fory"); + + JsonAnySetterAccessor anySetter = codec.anySetterAccessor(); + assertNotNull(anySetter); + anySetter.put(model, "answer", 42); + Field extra = modelType.getField("extra"); + assertEquals(((Map) extra.get(model)).get("answer"), 42); + + GeneratedJsonCodecFactory factory = + (GeneratedJsonCodecFactory) + loader + .loadClass("test.GeneratedModel_ForyJsonCodec$Factory") + .getConstructor() + .newInstance(); + assertEquals(factory.create().type(), modelType); + } + + @Test + public void hiddenFieldAccessors() throws Exception { + CompilationResult result = + compile( + "test.HiddenFieldModel", + "package test;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "class HiddenFieldBase { public int value; }\n" + + "@JsonType public class HiddenFieldModel extends HiddenFieldBase {\n" + + " public int value;\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + ClassLoader loader = result.classLoader(); + Class modelType = loader.loadClass("test.HiddenFieldModel"); + Class baseType = loader.loadClass("test.HiddenFieldBase"); + GeneratedJsonCodec codec = generatedCodec(loader, "test.HiddenFieldModel_ForyJsonCodec"); + JsonFieldAccessor base = fieldAccessor(codec.fieldAccessors(), baseType, "value"); + JsonFieldAccessor child = fieldAccessor(codec.fieldAccessors(), modelType, "value"); + Object model = modelType.getConstructor().newInstance(); + base.putInt(model, 11); + child.putInt(model, 29); + assertEquals(base.getInt(model), 11); + assertEquals(child.getInt(model), 29); + Field baseField = baseType.getField("value"); + baseField.setAccessible(true); + assertEquals(baseField.getInt(model), 11); + assertEquals(modelType.getField("value").getInt(model), 29); + } + + @Test + public void genericMemberDispatch() throws Exception { + CompilationResult result = + compile( + "test.GenericDispatchModel", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "class GenericDispatchBase {\n" + + " public T value;\n" + + " public int baseSetterCalls;\n" + + " public int baseAnySetterCalls;\n" + + " @SuppressWarnings(\"unchecked\")\n" + + " public void setText(CharSequence value) {\n" + + " this.value = (T) value; baseSetterCalls++;\n" + + " }\n" + + " public T getValue() { return value; }\n" + + " public String getVirtualValue() { return \"base\"; }\n" + + " @SuppressWarnings(\"unchecked\")\n" + + " @JsonAnySetter public void putExtra(String name, CharSequence value) {\n" + + " this.value = (T) value; baseAnySetterCalls++;\n" + + " }\n" + + "}\n" + + "@JsonType public class GenericDispatchModel\n" + + " extends GenericDispatchBase {\n" + + " public String value;\n" + + " public int overloadSetterCalls;\n" + + " public int overloadAnySetterCalls;\n" + + " public void setText(String value) { overloadSetterCalls++; }\n" + + " public void putExtra(String name, String value) { overloadAnySetterCalls++; }\n" + + " @Override public String getVirtualValue() { return \"child\"; }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + ClassLoader loader = result.classLoader(); + Class modelType = loader.loadClass("test.GenericDispatchModel"); + Class baseType = loader.loadClass("test.GenericDispatchBase"); + GeneratedJsonCodec codec = generatedCodec(loader, "test.GenericDispatchModel_ForyJsonCodec"); + Object model = modelType.getConstructor().newInstance(); + JsonFieldAccessor baseField = fieldAccessor(codec.fieldAccessors(), baseType, "value"); + JsonFieldAccessor childField = fieldAccessor(codec.fieldAccessors(), modelType, "value"); + baseField.putObject(model, "base-field"); + childField.putObject(model, "child-field"); + assertEquals(baseField.getObject(model), "base-field"); + assertEquals(childField.getObject(model), "child-field"); + JsonFieldAccessor setter = + methodAccessor(codec.fieldAccessors(), baseType, "setText", CharSequence.class); + JsonFieldAccessor getter = methodAccessor(codec.fieldAccessors(), baseType, "getValue"); + setter.putObject(model, "base-setter"); + assertEquals(getter.getObject(model), "base-setter"); + Field baseSetterCalls = baseType.getField("baseSetterCalls"); + baseSetterCalls.setAccessible(true); + assertEquals(baseSetterCalls.getInt(model), 1); + assertEquals(modelType.getField("overloadSetterCalls").getInt(model), 0); + + JsonAnySetterAccessor anySetter = codec.anySetterAccessor(); + anySetter.put(model, "extra", "base-any"); + assertEquals(getter.getObject(model), "base-any"); + Field baseAnySetterCalls = baseType.getField("baseAnySetterCalls"); + baseAnySetterCalls.setAccessible(true); + assertEquals(baseAnySetterCalls.getInt(model), 1); + assertEquals(modelType.getField("overloadAnySetterCalls").getInt(model), 0); + + JsonFieldAccessor virtual = + methodAccessor(codec.fieldAccessors(), modelType, "getVirtualValue"); + assertEquals(virtual.getObject(model), "child"); + } + + @Test + public void generatedCreator() throws Exception { + CompilationResult result = + compile( + "test.CreatorModel", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public final class CreatorModel {\n" + + " public final int id;\n" + + " public final String name;\n" + + " @JsonCreator({\"id\", \"name\"})\n" + + " public CreatorModel(int id, String name) { this.id = id; this.name = name; }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + ClassLoader loader = result.classLoader(); + GeneratedJsonCodec codec = generatedCodec(loader, "test.CreatorModel_ForyJsonCodec"); + assertEquals(codec.creatorParameterNames(), new String[] {"id", "name"}); + assertEquals(codec.creatorParameterTypes(), new Class[] {int.class, String.class}); + assertNotSame(codec.creatorParameterNames(), codec.creatorParameterNames()); + assertNotSame(codec.creatorParameterTypes(), codec.creatorParameterTypes()); + Object model = codec.newInstance(new Object[] {7, "json"}); + assertEquals(model.getClass().getField("id").getInt(model), 7); + assertEquals(model.getClass().getField("name").get(model), "json"); + } + + @Test + public void runtimePipeline() throws Exception { + CompilationResult result = + compile( + "test.RuntimeModel", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public final class RuntimeModel {\n" + + " public final int id;\n" + + " private final String name;\n" + + " @JsonCreator({\"id\", \"name\"})\n" + + " public RuntimeModel(int id, String name) { this.id = id; this.name = name; }\n" + + " public String getName() { return name; }\n" + + " @Override public boolean equals(Object value) {\n" + + " if (!(value instanceof RuntimeModel)) { return false; }\n" + + " RuntimeModel other = (RuntimeModel) value;\n" + + " return id == other.id && name.equals(other.name);\n" + + " }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertRoundTrips( + result, + "test.RuntimeModel", + new Class[] {int.class, String.class}, + new Object[] {7, "json"}); + } + + @Test + public void mutableRuntimePipeline() throws Exception { + CompilationResult result = + compile( + "test.MutableModel", + "package test;\n" + + "import java.util.*;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public final class MutableModel {\n" + + " public int id;\n" + + " private String name;\n" + + " private final Map extra = new LinkedHashMap<>();\n" + + " public MutableModel() {}\n" + + " public MutableModel(int id, String name, Map extra) {\n" + + " this.id = id; this.name = name; this.extra.putAll(extra);\n" + + " }\n" + + " public String getName() { return name; }\n" + + " public void setName(String name) { this.name = name; }\n" + + " @JsonAnyGetter public Map getExtra() { return extra; }\n" + + " @JsonAnySetter public void putExtra(String key, Object value) {\n" + + " extra.put(key, value);\n" + + " }\n" + + " @Override public boolean equals(Object value) {\n" + + " if (!(value instanceof MutableModel)) { return false; }\n" + + " MutableModel other = (MutableModel) value;\n" + + " return id == other.id && Objects.equals(name, other.name)\n" + + " && extra.equals(other.extra);\n" + + " }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + Map extra = new LinkedHashMap<>(); + extra.put("enabled", Boolean.TRUE); + assertRoundTrips( + result, + "test.MutableModel", + new Class[] {int.class, String.class, Map.class}, + new Object[] {5, "mutable", extra}); + } + + @Test + public void encodedRecordPipeline() throws Exception { + assumeJava16Source(); + CompilationResult result = + compile( + "test.EncodedRecord", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public record EncodedRecord(\n" + + " @JsonRawValue String raw, @JsonBase64 byte[] bytes) {}\n"); + assertTrue(result.success, result.diagnostics()); + ClassLoader loader = result.classLoader(); + Class type = loader.loadClass("test.EncodedRecord"); + Object value = + type.getConstructor(String.class, byte[].class) + .newInstance("{\"id\":1}", new byte[] {1, 2, 3}); + for (ForyJson json : jsonRuntimes(loader)) { + assertEquals(json.toJson(value), "{\"raw\":{\"id\":1},\"bytes\":\"AQID\"}"); + Object decoded = json.fromJson("{\"raw\":\"text\",\"bytes\":\"AQI=\"}", type); + assertEquals(type.getMethod("raw").invoke(decoded), "text"); + assertTrue( + Arrays.equals((byte[]) type.getMethod("bytes").invoke(decoded), new byte[] {1, 2})); + } + } + + @Test + public void encodedCreatorPipeline() throws Exception { + CompilationResult result = + compile( + "test.EncodedCreator", + "package test;\n" + + "import java.util.Arrays;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public final class EncodedCreator {\n" + + " @JsonBase64 public final byte[] bytes;\n" + + " @JsonCreator({\"bytes\"}) public EncodedCreator(byte[] bytes) {\n" + + " this.bytes = bytes;\n" + + " }\n" + + " @Override public boolean equals(Object value) {\n" + + " return value instanceof EncodedCreator\n" + + " && Arrays.equals(bytes, ((EncodedCreator) value).bytes);\n" + + " }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertRoundTrips( + result, + "test.EncodedCreator", + new Class[] {byte[].class}, + new Object[] {new byte[] {1, 2, 3}}); + } + + @Test + public void missingCompanionFails() throws Exception { + CompilationResult result = + compile( + "test.MissingModel", + "package test;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "@JsonType public final class MissingModel {\n" + + " public int id;\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertTrue( + Files.deleteIfExists(result.classRoot.resolve("test/MissingModel_ForyJsonCodec.class"))); + ClassLoader loader = result.classLoader(); + Class type = loader.loadClass("test.MissingModel"); + Object value = type.getConstructor().newInstance(); + try { + ForyJson.builder().withCodegen(false).withClassLoader(loader).build().toJson(value); + throw new AssertionError("Missing generated JSON companion was accepted"); + } catch (ForyJsonException e) { + assertTrue( + e.getMessage().contains("Missing generated JSON codec test.MissingModel_ForyJsonCodec"), + e.getMessage()); + } + } + + @Test + public void unannotatedRuntimePipeline() throws Exception { + CompilationResult result = + compile( + "test.RuntimePlain", + "package test;\n" + + "public final class RuntimePlain {\n" + + " public int id;\n" + + " public String name;\n" + + " public RuntimePlain() {}\n" + + " public RuntimePlain(int id, String name) { this.id = id; this.name = name; }\n" + + " @Override public boolean equals(Object value) {\n" + + " if (!(value instanceof RuntimePlain)) { return false; }\n" + + " RuntimePlain other = (RuntimePlain) value;\n" + + " return id == other.id && name.equals(other.name);\n" + + " }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertFalse(result.hasGeneratedSource("test/RuntimePlain_ForyJsonCodec.java")); + assertRoundTrips( + result, + "test.RuntimePlain", + new Class[] {int.class, String.class}, + new Object[] {5, "plain"}); + } + + @Test + public void staleCompanionIgnored() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "test.StaleModel", + "package test;\n" + + "public final class StaleModel {\n" + + " public int id;\n" + + " public StaleModel() {}\n" + + "}\n"); + sources.put( + "test.StaleModel_ForyJsonCodec", + "package test;\n" + + "public final class StaleModel_ForyJsonCodec\n" + + " extends org.apache.fory.json.codec.GeneratedJsonCodec {\n" + + " public Class type() {\n" + + " throw new AssertionError(\"stale companion loaded\");\n" + + " }\n" + + " public org.apache.fory.json.meta.JsonFieldAccessor[] fieldAccessors() {\n" + + " return new org.apache.fory.json.meta.JsonFieldAccessor[0];\n" + + " }\n" + + "}\n"); + CompilationResult result = compile(sources); + assertTrue(result.success, result.diagnostics()); + ClassLoader loader = result.classLoader(); + Class type = loader.loadClass("test.StaleModel"); + Object value = type.getConstructor().newInstance(); + type.getField("id").setInt(value, 7); + for (ForyJson json : jsonRuntimes(loader)) { + assertEquals(json.toJson(value), "{\"id\":7}"); + } + } + + @Test + public void recordRuntimePipeline() throws Exception { + assumeJava16Source(); + CompilationResult result = + compile( + "test.RuntimeRecord", + "package test;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "@JsonType public record RuntimeRecord(int id, String name) {}\n"); + assertTrue(result.success, result.diagnostics()); + assertRoundTrips( + result, + "test.RuntimeRecord", + new Class[] {int.class, String.class}, + new Object[] {11, "record"}); + } + + @Test + public void recordValuePipeline() throws Exception { + assumeJava16Source(); + CompilationResult result = + compile( + "test.RuntimeValueRecord", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public record RuntimeValueRecord(@JsonValue String value) {\n" + + " @JsonCreator public RuntimeValueRecord {}\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertTrue(result.hasGeneratedSource("test/RuntimeValueRecord_ForyJsonCodec.java")); + assertRoundTrips( + result, + "test.RuntimeValueRecord", + new Class[] {String.class}, + new Object[] {"record-value"}); + } + + @Test + public void escapedCreatorNames() throws Exception { + CompilationResult result = + compile( + "test.EscapedCreator", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public final class EscapedCreator {\n" + + " public final int value;\n" + + " @JsonCreator({\"line\\nfeed\\001\"})\n" + + " public EscapedCreator(int value) { this.value = value; }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + GeneratedJsonCodec codec = + generatedCodec(result.classLoader(), "test.EscapedCreator_ForyJsonCodec"); + assertEquals(codec.creatorParameterNames(), new String[] {"line\nfeed\u0001"}); + } + + @Test + public void atomicSubtypeCompanion() throws Exception { + CompilationResult result = + compile( + "test.AtomicModel", + "package test;\n" + + "import java.util.concurrent.atomic.AtomicReference;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "@JsonType public class AtomicModel extends AtomicReference {\n" + + " public int tag;\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertTrue(result.hasGeneratedSource("test/AtomicModel_ForyJsonCodec.java")); + } + + @Test + public void rejectedRuntimeFamily() throws Exception { + CompilationResult result = + compile( + "test.SocketModel", + "package test;\n" + + "import java.net.InetSocketAddress;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "@JsonType public class SocketModel extends InetSocketAddress {\n" + + " public SocketModel() { super(0); }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertFalse(result.hasGeneratedSource("test/SocketModel_ForyJsonCodec.java")); + } + + @Test + public void packageMethodsSkipped() throws Exception { + CompilationResult result = + compile( + "test.PackageMethods", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public class PackageMethods {\n" + + " @JsonProperty String getHidden() { return null; }\n" + + " @JsonProperty void setHidden(String value) {}\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + GeneratedJsonCodec codec = + generatedCodec(result.classLoader(), "test.PackageMethods_ForyJsonCodec"); + assertEquals(codec.fieldAccessors().length, 0); + } + + @Test + public void inaccessibleOwners() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "hidden.VisibleBase", + "package hidden;\n" + + "class HiddenBase {\n" + + " public int id;\n" + + "}\n" + + "public class VisibleBase extends HiddenBase { public VisibleBase() {} }\n"); + sources.put( + "test.InaccessibleModel", + "package test;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "@JsonType public final class InaccessibleModel extends hidden.VisibleBase {\n" + + " public InaccessibleModel() {}\n" + + "}\n"); + CompilationResult result = compile(sources); + assertTrue(result.success, result.diagnostics()); + ClassLoader loader = result.classLoader(); + Class type = loader.loadClass("test.InaccessibleModel"); + GeneratedJsonCodec codec = generatedCodec(loader, "test.InaccessibleModel_ForyJsonCodec"); + assertEquals(codec.fieldAccessors().length, 0); + Field id = type.getField("id"); + id.setAccessible(true); + for (ForyJson json : jsonRuntimes(loader)) { + byte[] bytes = "{\"id\":3}".getBytes(StandardCharsets.UTF_8); + for (Object value : + Arrays.asList( + json.fromJson(new String(bytes, StandardCharsets.UTF_8), type), + json.fromJson(bytes, type))) { + assertEquals(id.getInt(value), 3); + assertEquals(json.toJson(value), "{\"id\":3}"); + } + } + } + + @Test + public void inaccessibleAnySetterType() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "hidden.AnyBase", + "package hidden;\n" + + "import org.apache.fory.json.annotation.JsonAnySetter;\n" + + "class HiddenValue { public int value; }\n" + + "public class AnyBase {\n" + + " private Object extraValue;\n" + + " @JsonAnySetter public void putExtra(String name, HiddenValue value) {\n" + + " extraValue = value;\n" + + " }\n" + + " public Object getExtraValue() { return extraValue; }\n" + + "}\n"); + sources.put( + "test.InaccessibleAnyModel", + "package test;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "@JsonType public final class InaccessibleAnyModel extends hidden.AnyBase {}\n"); + CompilationResult result = compile(sources); + assertTrue(result.success, result.diagnostics()); + ClassLoader loader = result.classLoader(); + Class type = loader.loadClass("test.InaccessibleAnyModel"); + GeneratedJsonCodec codec = generatedCodec(loader, "test.InaccessibleAnyModel_ForyJsonCodec"); + assertEquals(codec.anySetterAccessor(), null); + Method getExtraValue = type.getMethod("getExtraValue"); + for (ForyJson json : jsonRuntimes(loader)) { + byte[] bytes = "{\"unknown\":{\"value\":4}}".getBytes(StandardCharsets.UTF_8); + for (Object value : + Arrays.asList( + json.fromJson(new String(bytes, StandardCharsets.UTF_8), type), + json.fromJson(bytes, type))) { + Object extra = getExtraValue.invoke(value); + Field field = extra.getClass().getField("value"); + field.setAccessible(true); + assertEquals(field.getInt(extra), 4); + } + } } @Test @@ -121,6 +696,151 @@ public void hierarchyRules() throws Exception { assertFalse(rules.contains("class test.Hierarchy$SuppressedCodec { public (); }"), rules); assertTrue(rules.contains("class test.Base"), rules); assertTrue(rules.contains("class test.Contract"), rules); + assertFalse(result.hasGeneratedSource("test/Hierarchy_ForyJsonCodec.java")); + } + + @Test + public void customTypeCodecSkipsCompanion() throws Exception { + CompilationResult result = + compile( + "test.CustomModel", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType @JsonCodec(CustomModel.Codec.class) public class CustomModel {\n" + + valueCodec("Codec") + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertFalse(result.hasGeneratedSource("test/CustomModel_ForyJsonCodec.java")); + } + + @Test + public void nestedCompanionName() throws Exception { + CompilationResult result = + compile( + "test.Outer", + "package test;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "public class Outer {\n" + + " @JsonType public static class Inner_Name {\n" + + " public int id;\n" + + " }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertTrue(result.hasGeneratedSource("test/Outer_d_Inner_u_Name_ForyJsonCodec.java")); + String rules = result.generatedResource(RULE_PREFIX + "test.Outer$Inner_Name.pro"); + assertTrue(rules.contains("class test.Outer_d_Inner_u_Name_ForyJsonCodec {"), rules); + } + + @Test + public void companionNamesDoNotCollide() throws Exception { + Map sources = new LinkedHashMap<>(); + sources.put( + "test.A", + "package test;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "public class A { @JsonType public static class u_X { public int id; } }\n"); + sources.put( + "test.A_u", + "package test;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "public class A_u { @JsonType public static class X { public int id; } }\n"); + CompilationResult result = compile(sources); + assertTrue(result.success, result.diagnostics()); + assertTrue(result.hasGeneratedSource("test/A_d_u_u_X_ForyJsonCodec.java")); + assertTrue(result.hasGeneratedSource("test/A_u_u_d_X_ForyJsonCodec.java")); + } + + @Test + public void inaccessibleModelDiagnostic() throws Exception { + CompilationResult result = + compile( + "test.PrivateModelOwner", + "package test;\n" + + "import org.apache.fory.json.annotation.JsonType;\n" + + "public class PrivateModelOwner {\n" + + " @JsonType private static class PrivateModel {}\n" + + "}\n"); + assertFalse(result.success); + assertTrue( + result.diagnostics().contains("@JsonType model is not accessible to generated JSON code"), + result.diagnostics()); + } + + @Test + public void creatorDiagnostic() throws Exception { + CompilationResult result = + compile( + "test.InvalidCreator", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public class InvalidCreator {\n" + + " @JsonCreator private InvalidCreator(@JsonProperty(\"id\") int id) {}\n" + + "}\n"); + assertFalse(result.success); + assertTrue(result.diagnostics().contains("@JsonCreator must be public"), result.diagnostics()); + } + + @Test + public void memberCreatorConstructorDiagnostic() throws Exception { + CompilationResult result = + compile( + "test.MemberCreatorOwner", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "public class MemberCreatorOwner {\n" + + " @JsonType public class Value {\n" + + " @JsonCreator public Value(@JsonProperty(\"id\") int id) {}\n" + + " }\n" + + "}\n"); + assertFalse(result.success); + assertTrue( + result + .diagnostics() + .contains("A non-static member class must use a static @JsonCreator factory"), + result.diagnostics()); + } + + @Test + public void memberCreatorFactory() throws Exception { + assumeJava16Source(); + CompilationResult result = + compile( + "test.MemberFactoryOwner", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "public class MemberFactoryOwner {\n" + + " @JsonType public class Value {\n" + + " public final int id;\n" + + " private Value(int id) { this.id = id; }\n" + + " @JsonCreator public static Value create(@JsonProperty(\"id\") int id) {\n" + + " return new MemberFactoryOwner().new Value(id);\n" + + " }\n" + + " }\n" + + "}\n"); + assertTrue(result.success, result.diagnostics()); + assertTrue(result.hasGeneratedSource("test/MemberFactoryOwner_d_Value_ForyJsonCodec.java")); + } + + @Test + public void creatorPropertyListDiagnostic() throws Exception { + CompilationResult result = + compile( + "test.InvalidPropertyListCreator", + "package test;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public class InvalidPropertyListCreator {\n" + + " public final int id;\n" + + " @JsonCreator({\"id\"})\n" + + " public InvalidPropertyListCreator(@JsonProperty(\"id\") int id) {\n" + + " this.id = id;\n" + + " }\n" + + "}\n"); + assertFalse(result.success); + assertTrue( + result + .diagnostics() + .contains("Property-list @JsonCreator parameters cannot declare @JsonProperty"), + result.diagnostics()); } @Test @@ -220,7 +940,7 @@ public void validationRules() throws Exception { @Test public void recordRules() throws Exception { - assumeRecordSupport(); + assumeJava16Source(); CompilationResult result = compile( "test.CodecRecord", @@ -228,15 +948,58 @@ public void recordRules() throws Exception { + "import java.util.List;\n" + "import org.apache.fory.json.annotation.*;\n" + "@JsonType public record CodecRecord(\n" + + " int id, String name,\n" + " @JsonCodec(elementCodec = ElementCodec.class) List values) {\n" + valueCodec("ElementCodec") + "}\n"); assertTrue(result.success, result.diagnostics()); String rules = result.generatedResource(RULE_PREFIX + "test.CodecRecord.pro"); assertTrue(rules.contains("java.util.List values;"), rules); + assertTrue(rules.contains("int id();"), rules); + assertTrue(rules.contains("java.lang.String name();"), rules); assertTrue(rules.contains("java.util.List values();"), rules); - assertTrue(rules.contains("(java.util.List);"), rules); + assertTrue(rules.contains("(int,java.lang.String,java.util.List);"), rules); assertTrue(rules.contains("class test.CodecRecord$ElementCodec { public (); }"), rules); + assertTrue(result.hasGeneratedSource("test/CodecRecord_ForyJsonCodec.java")); + GeneratedJsonCodec codec = + generatedCodec(result.classLoader(), "test.CodecRecord_ForyJsonCodec"); + assertTrue(codec.isRecord()); + assertEquals(codec.creatorParameterNames(), new String[] {"id", "name", "values"}); + Object record = + codec.newInstance(new Object[] {7, "record", Collections.singletonList("fory")}); + assertEquals(record.getClass().getMethod("id").invoke(record), 7); + assertEquals(record.getClass().getMethod("name").invoke(record), "record"); + assertEquals( + record.getClass().getMethod("values").invoke(record), Collections.singletonList("fory")); + } + + @Test + public void ignoredRecordComponent() throws Exception { + assumeJava16Source(); + CompilationResult result = + compile( + "test.IgnoredRecord", + "package test;\n" + + "import java.util.Map;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "@JsonType public record IgnoredRecord(\n" + + " @JsonIgnore(ignoreWrite = false) int id,\n" + + " @JsonAnyProperty Map extra) {}\n"); + assertTrue(result.success, result.diagnostics()); + ClassLoader loader = result.classLoader(); + Class type = loader.loadClass("test.IgnoredRecord"); + byte[] bytes = "{\"id\":9,\"unknown\":1}".getBytes(StandardCharsets.UTF_8); + for (ForyJson json : jsonRuntimes(loader)) { + for (Object value : + Arrays.asList( + json.fromJson(new String(bytes, StandardCharsets.UTF_8), type), + json.fromJson(bytes, type))) { + assertEquals(type.getMethod("id").invoke(value), 0); + Map extra = (Map) type.getMethod("extra").invoke(value); + assertEquals(extra.size(), 1); + assertTrue(extra.containsKey("unknown")); + } + } } @Test @@ -254,6 +1017,8 @@ public void subtypeRules() throws Exception { assertTrue(result.success, result.diagnostics()); assertTrue(result.hasGeneratedResource(RULE_PREFIX + "test.Base.pro")); assertTrue(result.hasGeneratedResource(RULE_PREFIX + "test.Child.pro")); + assertFalse(result.hasGeneratedSource("test/Base_ForyJsonCodec.java")); + assertFalse(result.hasGeneratedSource("test/Child_ForyJsonCodec.java")); String rules = result.generatedResource(RULE_PREFIX + "test.Base.pro"); assertTrue(rules.contains("-keep,allowoptimization class external.HiddenChild"), rules); } @@ -284,6 +1049,7 @@ public void independentProcessors() throws Exception { + "@ForyStruct @JsonType public class Both { public int id; public Both() {} }\n"); assertTrue(result.success, result.diagnostics()); assertTrue(result.hasGeneratedSource("test/Both_ForySerializer.java")); + assertTrue(result.hasGeneratedSource("test/Both_ForyJsonCodec.java")); assertTrue( result.hasGeneratedResource("META-INF/proguard/fory-static-generated-test.Both.pro")); assertTrue(result.hasGeneratedResource(RULE_PREFIX + "test.Both.pro")); @@ -326,6 +1092,7 @@ public void valueRawAndBase64Rules() throws Exception { assertTrue(result.success, result.diagnostics()); String valueRules = result.generatedResource(RULE_PREFIX + "test.ValueModel.pro"); + assertFalse(result.hasGeneratedSource("test/ValueModel_ForyJsonCodec.java")); assertTrue(valueRules.contains("java.lang.String value();"), valueRules); assertTrue(valueRules.contains("(java.lang.String);"), valueRules); assertTrue( @@ -334,6 +1101,7 @@ public void valueRawAndBase64Rules() throws Exception { valueRules.contains("@interface org.apache.fory.json.annotation.JsonRawValue"), valueRules); String rawRules = result.generatedResource(RULE_PREFIX + "test.RawModel.pro"); + assertTrue(result.hasGeneratedSource("test/RawModel_ForyJsonCodec.java")); assertTrue(rawRules.contains("java.lang.String body;"), rawRules); assertTrue(rawRules.contains("byte[] bytes;"), rawRules); assertTrue(rawRules.contains("java.lang.String getOther();"), rawRules); @@ -364,6 +1132,7 @@ public void valueOverrideSuppression() throws Exception { + "}\n"); assertTrue(result.success, result.diagnostics()); String rules = result.generatedResource(RULE_PREFIX + "test.ValueChild.pro"); + assertTrue(result.hasGeneratedSource("test/ValueChild_ForyJsonCodec.java")); assertFalse(rules.contains("JsonValue"), rules); assertFalse(rules.contains("value();"), rules); } @@ -507,7 +1276,84 @@ private static CompilationResult compile(Map sources) throws IOE } } - private static void assumeRecordSupport() { + private static GeneratedJsonCodec generatedCodec(ClassLoader loader, String name) + throws Exception { + return (GeneratedJsonCodec) loader.loadClass(name).getConstructor().newInstance(); + } + + private static void assertRoundTrips( + CompilationResult result, String typeName, Class[] parameterTypes, Object[] arguments) + throws Exception { + ClassLoader loader = result.classLoader(); + Class type = loader.loadClass(typeName); + Object value = type.getConstructor(parameterTypes).newInstance(arguments); + for (ForyJson json : jsonRuntimes(loader)) { + assertRoundTrip(json, type, value); + } + } + + private static ForyJson[] jsonRuntimes(ClassLoader loader) { + return new ForyJson[] { + ForyJson.builder().withCodegen(false).withClassLoader(loader).build(), + ForyJson.builder() + .withCodegen(true) + .withAsyncCompilation(false) + .withClassLoader(loader) + .build() + }; + } + + private static void assertRoundTrip(ForyJson json, Class type, Object value) { + String text = json.toJson(value); + assertEquals(json.fromJson(text, type), value); + } + + private static JsonFieldAccessor fieldAccessor(JsonFieldAccessor[] accessors, String name) { + for (JsonFieldAccessor accessor : accessors) { + if (accessor.field() != null && accessor.field().getName().equals(name)) { + return accessor; + } + } + throw new AssertionError("Missing generated field accessor " + name); + } + + private static JsonFieldAccessor fieldAccessor( + JsonFieldAccessor[] accessors, Class owner, String name) { + for (JsonFieldAccessor accessor : accessors) { + if (accessor.field() != null + && accessor.field().getDeclaringClass() == owner + && accessor.field().getName().equals(name)) { + return accessor; + } + } + throw new AssertionError("Missing generated field accessor " + owner.getName() + "." + name); + } + + private static JsonFieldAccessor methodAccessor(JsonFieldAccessor[] accessors, String name) { + for (JsonFieldAccessor accessor : accessors) { + Method method = accessor.getter() != null ? accessor.getter() : accessor.setter(); + if (method != null && method.getName().equals(name)) { + return accessor; + } + } + throw new AssertionError("Missing generated method accessor " + name); + } + + private static JsonFieldAccessor methodAccessor( + JsonFieldAccessor[] accessors, Class owner, String name, Class... parameterTypes) { + for (JsonFieldAccessor accessor : accessors) { + Method method = accessor.getter() != null ? accessor.getter() : accessor.setter(); + if (method != null + && method.getDeclaringClass() == owner + && method.getName().equals(name) + && Arrays.equals(method.getParameterTypes(), parameterTypes)) { + return accessor; + } + } + throw new AssertionError("Missing generated method accessor " + owner.getName() + "." + name); + } + + private static void assumeJava16Source() { String version = System.getProperty("java.specification.version"); if (version.startsWith("1.")) { version = version.substring(2); @@ -517,7 +1363,7 @@ private static void assumeRecordSupport() { version = version.substring(0, dotIndex); } if (Integer.parseInt(version) < 16) { - throw new SkipException("Record source tests require JDK 16 or newer"); + throw new SkipException("Source test requires JDK 16 or newer"); } } @@ -551,6 +1397,11 @@ String generatedResource(String relativePath) throws IOException { Files.readAllBytes(classRoot.resolve(relativePath)), StandardCharsets.UTF_8); } + ClassLoader classLoader() throws IOException { + return new URLClassLoader( + new URL[] {classRoot.toUri().toURL()}, JsonTypeProcessorTest.class.getClassLoader()); + } + String diagnostics() { StringBuilder builder = new StringBuilder(); for (Diagnostic diagnostic : diagnostics) { diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/GeneratedClassNames.java b/java/fory-core/src/main/java/org/apache/fory/codegen/GeneratedClassNames.java new file mode 100644 index 0000000000..09f5dfb1cc --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/GeneratedClassNames.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.codegen; + +import org.apache.fory.annotation.Internal; + +/** Collision-free Java names derived from model binary names. */ +@Internal +public final class GeneratedClassNames { + private GeneratedClassNames() {} + + /** Appends a generated-class suffix after escaping the binary simple name. */ + public static String withSuffix(String binaryName, String suffix) { + int packageEnd = binaryName.lastIndexOf('.'); + String binarySimpleName = packageEnd < 0 ? binaryName : binaryName.substring(packageEnd + 1); + String generatedSimpleName = escapeBinarySimpleName(binarySimpleName) + suffix; + return packageEnd < 0 + ? generatedSimpleName + : binaryName.substring(0, packageEnd + 1) + generatedSimpleName; + } + + /** Escapes one binary simple name into a collision-free Java identifier. */ + public static String escapeBinarySimpleName(String binarySimpleName) { + return escape(binarySimpleName, false); + } + + /** Escapes every segment of a binary name while preserving package separators. */ + public static String escapeBinaryName(String binaryName) { + return escape(binaryName, true); + } + + private static String escape(String value, boolean preserveDots) { + StringBuilder builder = new StringBuilder(value.length() + 32); + for (int i = 0; i < value.length(); ) { + int codePoint = value.codePointAt(i); + if (codePoint == '$') { + builder.append("_d_"); + } else if (codePoint == '_') { + builder.append("_u_"); + } else if (preserveDots && codePoint == '.') { + builder.append('.'); + } else if (Character.isJavaIdentifierPart(codePoint)) { + builder.appendCodePoint(codePoint); + } else { + builder.append("_x").append(Integer.toHexString(codePoint)).append('_'); + } + i += Character.charCount(codePoint); + } + return builder.toString(); + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/StaticGeneratedSerializerRegistry.java b/java/fory-core/src/main/java/org/apache/fory/resolver/StaticGeneratedSerializerRegistry.java index d24464b1ff..e38f1a33a7 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/StaticGeneratedSerializerRegistry.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/StaticGeneratedSerializerRegistry.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; import org.apache.fory.annotation.Internal; +import org.apache.fory.codegen.GeneratedClassNames; import org.apache.fory.exception.ForyException; import org.apache.fory.meta.TypeDef; import org.apache.fory.reflect.ReflectionUtils; @@ -196,38 +197,14 @@ static String generatedSerializerBinaryName(Class targetType, Mode mode) { } static String generatedSerializerBinaryName(String targetBinaryName, Mode mode) { - int packageEnd = targetBinaryName.lastIndexOf('.'); - if (packageEnd < 0) { - return generatedSerializerSimpleName(targetBinaryName, mode); - } - return targetBinaryName.substring(0, packageEnd) - + "." - + generatedSerializerSimpleName(targetBinaryName.substring(packageEnd + 1), mode); + return GeneratedClassNames.withSuffix(targetBinaryName, suffix(mode)); } static String generatedSerializerSimpleName(String targetBinarySimpleName, Mode mode) { - return escapeBinarySimpleName(targetBinarySimpleName) + suffix(mode); + return GeneratedClassNames.escapeBinarySimpleName(targetBinarySimpleName) + suffix(mode); } private static String suffix(Mode mode) { return mode == Mode.XLANG ? XLANG_SUFFIX : NATIVE_SUFFIX; } - - private static String escapeBinarySimpleName(String binarySimpleName) { - StringBuilder builder = new StringBuilder(binarySimpleName.length() + 32); - for (int i = 0; i < binarySimpleName.length(); ) { - int codePoint = binarySimpleName.codePointAt(i); - if (codePoint == '$') { - builder.append('_'); - } else if (codePoint == '_') { - builder.append("_u_"); - } else if (Character.isJavaIdentifierPart(codePoint)) { - builder.appendCodePoint(codePoint); - } else { - builder.append("_x").append(Integer.toHexString(codePoint)).append('_'); - } - i += Character.charCount(codePoint); - } - return builder.toString(); - } } diff --git a/java/fory-core/src/test/java/org/apache/fory/resolver/StaticGeneratedSerializerRegistryTest.java b/java/fory-core/src/test/java/org/apache/fory/resolver/StaticGeneratedSerializerRegistryTest.java index 47a1e0d3a0..9b1538298e 100644 --- a/java/fory-core/src/test/java/org/apache/fory/resolver/StaticGeneratedSerializerRegistryTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/resolver/StaticGeneratedSerializerRegistryTest.java @@ -19,6 +19,7 @@ package org.apache.fory.resolver; +import org.apache.fory.codegen.GeneratedClassNames; import org.testng.Assert; import org.testng.annotations.Test; @@ -36,7 +37,7 @@ public void testGoldenGeneratedSerializerNames() { Assert.assertEquals( StaticGeneratedSerializerRegistry.generatedSerializerBinaryName( "com.example.Outer$Inner", StaticGeneratedSerializerRegistry.Mode.XLANG), - "com.example.Outer_Inner_ForySerializer"); + "com.example.Outer_d_Inner_ForySerializer"); Assert.assertEquals( StaticGeneratedSerializerRegistry.generatedSerializerBinaryName( "com.example.Outer_Inner", StaticGeneratedSerializerRegistry.Mode.XLANG), @@ -49,5 +50,13 @@ public void testGoldenGeneratedSerializerNames() { StaticGeneratedSerializerRegistry.generatedSerializerBinaryName( "com.example.Outer-Inner", StaticGeneratedSerializerRegistry.Mode.XLANG), "com.example.Outer_x2d_Inner_ForySerializer"); + Assert.assertNotEquals( + StaticGeneratedSerializerRegistry.generatedSerializerBinaryName( + "com.example.A$u_X", StaticGeneratedSerializerRegistry.Mode.XLANG), + StaticGeneratedSerializerRegistry.generatedSerializerBinaryName( + "com.example.A_u$X", StaticGeneratedSerializerRegistry.Mode.XLANG)); + Assert.assertEquals( + GeneratedClassNames.escapeBinaryName("com_example.Outer$Inner"), + "com_u_example.Outer_d_Inner"); } } diff --git a/java/fory-core/src/test/java/org/apache/fory/xlang/XlangTestBase.java b/java/fory-core/src/test/java/org/apache/fory/xlang/XlangTestBase.java index 5f4bfec2ff..5aa076cb5e 100644 --- a/java/fory-core/src/test/java/org/apache/fory/xlang/XlangTestBase.java +++ b/java/fory-core/src/test/java/org/apache/fory/xlang/XlangTestBase.java @@ -50,6 +50,7 @@ import org.apache.fory.annotation.UInt32Type; import org.apache.fory.annotation.UInt64Type; import org.apache.fory.annotation.UInt8Type; +import org.apache.fory.codegen.GeneratedClassNames; import org.apache.fory.collection.BFloat16List; import org.apache.fory.collection.Float16List; import org.apache.fory.collection.Int32List; @@ -312,7 +313,7 @@ protected static void assertStructEvolvingOverride(Fory fory) { fixedSerializer.getClass().getName()); Assert.assertEquals( fixedSerializer.getClass().getName(), - XlangTestBase.class.getName() + "_FixedOverrideStruct_ForySerializer"); + GeneratedClassNames.withSuffix(FixedOverrideStruct.class.getName(), "_ForySerializer")); } EvolvingOverrideStruct evolving = newEvolvingOverrideStruct(); diff --git a/java/fory-json/README.md b/java/fory-json/README.md index 334ac9c8ac..875234fa94 100644 --- a/java/fory-json/README.md +++ b/java/fory-json/README.md @@ -362,12 +362,17 @@ automatically disabled. Every other builder option keeps the behavior described ## JSON annotations Fory JSON defines thirteen annotations in `org.apache.fory.json.annotation`, including `JsonCodec` for -complete-value codec selection and `JsonType` for GraalVM Native Image and Android build metadata. +complete-value codec selection and `JsonType` for generated model execution and retention. They are Fory JSON APIs, not Jackson, Gson, or Fory binary-protocol compatibility annotations. -`JsonType` has no effect on ordinary JVM JSON behavior and is not inherited. Add it to every -reachable object model used by a native executable. On Android it enables processor-generated R8 -rules. See the [GraalVM guide](../../docs/guide/java/graalvm-support.md) and +`JsonType` asks the annotation processor to generate direct property and creator operations plus +exact retention rules. It is not inherited, so annotate each eligible concrete model that needs a +generated companion. A directly annotated `JsonValue` Record also receives a companion for its +value accessor and canonical constructor. Ordinary unannotated classes may still use reflection; on +Android they need application-authored exact R8 rules. Android-desugared Records require `JsonType` +and the processor. A directly annotated model that uses the default object codec fails during codec +creation if its generated companion is missing. +See the [GraalVM guide](../../docs/guide/java/graalvm-support.md) and [Android guide](../../docs/guide/java/android-support.md) for the platform workflows. ### `JsonProperty` @@ -1128,8 +1133,9 @@ type-declaration codec is used for a more specific target, every decoded value m assignable to that target. The annotation has the same FIELD, METHOD, and PARAMETER behavior on the JVM, Android, and GraalVM -Native Image. Android applications may use `JsonType` for generated exact R8 rules or provide the -equivalent rules themselves. GraalVM object models follow the `JsonType` workflow in the +Native Image. Ordinary Android classes may omit `JsonType` and provide equivalent exact rules; +Android-desugared Records, including `JsonValue` Records, require `JsonType` and the processor. +GraalVM object models follow the `JsonType` workflow in the [GraalVM guide](../../docs/guide/java/graalvm-support.md). ## Type validation and untrusted input diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java index e43373c6af..14f39756ef 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java @@ -26,16 +26,22 @@ import java.lang.annotation.Target; /** - * Marks a reachable JSON model for build-time reflection configuration. + * Marks a JSON model for build-time generated execution and retention metadata. * - *

When the Fory annotation processor is enabled, it generates exact R8 rules for the model and - * the codec classes selected by its {@link JsonCodec} declarations. A class-literal subtype listed - * by an annotated {@link JsonSubTypes} base is processed automatically. The annotation is also used - * for reflection metadata registration in GraalVM native images. + *

The Fory annotation processor generates a type-owned JSON companion for an eligible concrete + * object model or a Record with an effective {@link JsonValue}, together with exact R8 rules for + * the model, companion, and codec classes selected by its {@link JsonCodec} declarations. The + * companion provides direct member access and creator invocation on the JVM, Android, and GraalVM + * Native Image. A concrete subtype listed only by a class-literal {@link JsonSubTypes} entry + * receives retention metadata but needs its own direct {@code JsonType} annotation to receive a + * companion. A directly annotated model that reaches the default object codec fails during codec + * creation when its generated companion is missing. * - *

This annotation does not change JSON codec semantics and is intentionally not inherited. It is - * not required for Android runtime use: applications may supply equivalent exact R8 rules - * themselves. + *

This annotation does not change the JSON schema and is intentionally not inherited. An + * ordinary mutable class may omit it and use reflection, with application-authored exact R8 rules + * on Android. Android-desugared Records, including Records represented by {@link JsonValue}, + * require this annotation and the processor because Android does not expose the Java Record + * reflection APIs. */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodec.java new file mode 100644 index 0000000000..571b0de8fc --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodec.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.json.codec; + +import java.lang.reflect.Executable; +import java.lang.reflect.Member; +import java.util.Collections; +import java.util.Map; +import org.apache.fory.annotation.Internal; +import org.apache.fory.json.meta.JsonAnySetterAccessor; +import org.apache.fory.json.meta.JsonFieldAccessor; + +/** Source-generated execution operations for one JSON model. */ +@Internal +public abstract class GeneratedJsonCodec { + private Map validatedAccessors; + private JsonAnySetterAccessor validatedAnySetterAccessor; + private Member validatedAnySetterMember; + private String[] validatedCreatorParameterNames; + private Class[] validatedCreatorParameterTypes; + private String validatedCreatorFactoryName; + private Executable validatedCreator; + private boolean validatedRecord; + private boolean initialized; + + protected GeneratedJsonCodec() {} + + /** Returns the exact model class owned by this companion. */ + public abstract Class type(); + + /** Returns fresh generated member-access candidates for cold schema binding. */ + public abstract JsonFieldAccessor[] fieldAccessors(); + + /** Returns the generated dynamic any-setter operation, or {@code null}. */ + public JsonAnySetterAccessor anySetterAccessor() { + return null; + } + + /** Returns creator parameter names in executable order, or {@code null} for mutable objects. */ + public String[] creatorParameterNames() { + return null; + } + + /** Returns exact erased creator parameter types, or {@code null} for mutable objects. */ + public Class[] creatorParameterTypes() { + return null; + } + + /** Returns the static creator factory name, or {@code null} for a constructor. */ + public String creatorFactoryName() { + return null; + } + + /** Returns whether the source model is a Java Record. */ + public boolean isRecord() { + return false; + } + + /** Constructs a creator-backed object from arguments in creator parameter order. */ + public T newInstance(Object[] arguments) { + throw new UnsupportedOperationException(); + } + + /** Publishes registry-validated hook results exactly once. */ + public final synchronized void initializeValidated( + Map accessors, + JsonAnySetterAccessor anySetterAccessor, + Member anySetterMember, + String[] creatorParameterNames, + Class[] creatorParameterTypes, + String creatorFactoryName, + Executable creator, + boolean record) { + if (initialized) { + throw new IllegalStateException("Generated JSON codec is already initialized"); + } + validatedAccessors = Collections.unmodifiableMap(accessors); + validatedAnySetterAccessor = anySetterAccessor; + validatedAnySetterMember = anySetterMember; + validatedCreatorParameterNames = creatorParameterNames; + validatedCreatorParameterTypes = creatorParameterTypes; + validatedCreatorFactoryName = creatorFactoryName; + validatedCreator = creator; + validatedRecord = record; + initialized = true; + } + + /** Returns the validated generated accessor for the exact selected member. */ + public final JsonFieldAccessor validatedAccessor(Member member) { + requireInitialized(); + return validatedAccessors.get(member); + } + + /** Returns the generated any-setter operation for the exact selected method. */ + final JsonAnySetterAccessor anySetter(Member member) { + requireInitialized(); + return member != null && member.equals(validatedAnySetterMember) + ? validatedAnySetterAccessor + : null; + } + + /** Returns whether the generated companion declared an any-setter operation. */ + final boolean hasAnySetter() { + requireInitialized(); + return validatedAnySetterAccessor != null; + } + + /** Returns the registry-owned creator parameter names. */ + final String[] validatedCreatorParameterNames() { + requireInitialized(); + return validatedCreatorParameterNames; + } + + /** Returns the registry-owned creator parameter types. */ + final Class[] validatedCreatorParameterTypes() { + requireInitialized(); + return validatedCreatorParameterTypes; + } + + /** Returns the validated static creator factory name. */ + final String validatedCreatorFactoryName() { + requireInitialized(); + return validatedCreatorFactoryName; + } + + /** Returns the exact registry-validated creator executable, or {@code null}. */ + final Executable validatedCreator() { + requireInitialized(); + return validatedCreator; + } + + /** Returns validated source-level Record identity. */ + public final boolean validatedRecord() { + requireInitialized(); + return validatedRecord; + } + + /** Returns whether the generated construction operation targets the exact executable. */ + public final boolean matchesCreator(Executable creator) { + requireInitialized(); + return creator != null && creator.equals(validatedCreator); + } + + /** Rethrows a checked creator failure without widening the generated ABI. */ + protected static RuntimeException creatorFailure(Throwable throwable) { + GeneratedJsonCodec.throwUnchecked(throwable); + throw new AssertionError(); + } + + @SuppressWarnings("unchecked") + private static void throwUnchecked(Throwable throwable) throws E { + throw (E) throwable; + } + + private void requireInitialized() { + if (!initialized) { + throw new IllegalStateException("Generated JSON codec has not been initialized"); + } + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java new file mode 100644 index 0000000000..35a0eb15ca --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.json.codec; + +import org.apache.fory.annotation.Internal; + +/** Stateless GraalVM runtime factory for a generated JSON companion. */ +@Internal +public interface GeneratedJsonCodecFactory { + GeneratedJsonCodec create(); +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java index 6d2e098e2c..e8776c11ee 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java @@ -25,12 +25,12 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; -import java.util.Arrays; import java.util.Map; import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.PropertyNamingStrategy; import org.apache.fory.json.annotation.JsonCodec; +import org.apache.fory.json.meta.JsonAnySetterAccessor; import org.apache.fory.json.meta.JsonCreatorFieldInfo; import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; @@ -48,8 +48,6 @@ import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.ObjectInstantiator; import org.apache.fory.reflect.TypeRef; -import org.apache.fory.util.record.RecordInfo; -import org.apache.fory.util.record.RecordUtils; /** * Reflection-backed semantic codec and metadata owner for one Java object type. @@ -58,36 +56,28 @@ * field/getter/setter group into one logical property, applies its name, inclusion, serialization * order, and directional ignore rules, resolves generic member types against the owner {@link * TypeRef}, and builds separate read and write field arrays. Class-valued fields and properties are - * never JSON members. Records retain constructor metadata and field defaults; ordinary objects - * retain an allocation strategy plus field or accessor sinks. + * never JSON members. Records and explicit creators retain ordered creator metadata; mutable + * objects retain an allocation strategy plus field or accessor sinks. * *

This codec is the interpreted implementation and the semantic fallback. Only an exact * raw-class instance of this class is eligible for generated capability replacement. Parameterized * object codecs retain binding-specific member types and remain the owner of all five slots. * Generated code may replace paths independently, but it is built from this codec's immutable field - * metadata and preserves the same null, unknown-field, record, and member-discovery semantics. + * metadata and preserves the same null, unknown-field, creator, and member-discovery semantics. */ public class ObjectCodec implements JsonValueCodec { - private static final int MUTABLE = 0; - private static final int RECORD = 1; - private static final int CREATOR = 2; - protected final Class type; protected final JsonFieldInfo[] writeFields; protected final JsonFieldInfo[] readFields; protected final JsonFieldTable readTable; protected final ObjectInstantiator instantiator; - private final int creationKind; private final JsonCreatorInfo creatorInfo; private final AnyInfo anyInfo; - private final RecordInfo recordInfo; - private final Object[] recordFieldDefaults; private ObjectCodec( Class type, JsonFieldInfo[] writeFields, JsonFieldInfo[] readFields, - String[] readJavaNames, JsonCreatorInfo creatorInfo, AnyInfo anyInfo, String[] skippedNames, @@ -102,14 +92,6 @@ private ObjectCodec( : new JsonFieldTable(readFields, skippedNames); this.instantiator = instantiator; this.creatorInfo = creatorInfo; - creationKind = RecordUtils.isRecord(type) ? RECORD : creatorInfo == null ? MUTABLE : CREATOR; - if (creationKind == RECORD) { - recordInfo = new RecordInfo(type, Arrays.asList(readJavaNames)); - recordFieldDefaults = recordFieldDefaults(type, readJavaNames, recordInfo); - } else { - recordInfo = null; - recordFieldDefaults = null; - } } @Internal @@ -117,16 +99,20 @@ public static ObjectCodec build( TypeRef ownerType, boolean propertyDiscoveryEnabled, PropertyNamingStrategy propertyNamingStrategy, - boolean writeNullFields) { + boolean writeNullFields, + GeneratedJsonCodec generatedCodec) { return ObjectCodecBuilder.build( - ownerType, propertyDiscoveryEnabled, propertyNamingStrategy, writeNullFields); + ownerType, + propertyDiscoveryEnabled, + propertyNamingStrategy, + writeNullFields, + generatedCodec); } static ObjectCodec createCodec( TypeRef ownerType, JsonFieldInfo[] writeFields, JsonFieldInfo[] readFields, - String[] readJavaNames, JsonCreatorInfo creatorInfo, AnyInfo anyInfo, String[] skippedNames, @@ -134,24 +120,10 @@ static ObjectCodec createCodec( Class type = ownerType.getRawType(); if (ownerType.getType() instanceof Class) { return new ObjectCodec<>( - type, - writeFields, - readFields, - readJavaNames, - creatorInfo, - anyInfo, - skippedNames, - instantiator); + type, writeFields, readFields, creatorInfo, anyInfo, skippedNames, instantiator); } return new ParameterizedObjectCodec<>( - type, - writeFields, - readFields, - readJavaNames, - creatorInfo, - anyInfo, - skippedNames, - instantiator); + type, writeFields, readFields, creatorInfo, anyInfo, skippedNames, instantiator); } public final Class type() { @@ -170,10 +142,6 @@ public final JsonFieldTable readTable() { return readTable; } - public final boolean isRecord() { - return creationKind == RECORD; - } - public final JsonCreatorInfo creatorInfo() { return creatorInfo; } @@ -219,20 +187,6 @@ public final T newInstance() { return (T) instantiator.newInstance(); } - @Internal - public final Object[] newRecordFieldValues() { - return Arrays.copyOf(recordFieldDefaults, recordFieldDefaults.length); - } - - @Internal - @SuppressWarnings("unchecked") - public final T newRecord(Object[] values) { - Object[] arguments = RecordUtils.remapping(recordInfo, values); - Object object = instantiator.newInstanceWithArguments(arguments); - Arrays.fill(recordInfo.getRecordComponents(), null); - return (T) object; - } - @Internal public final Map newAnyMap() { return anyInfo.mapCodec.newMap(); @@ -384,15 +338,10 @@ final T readLatin1Object(Latin1JsonReader reader, JsonFieldTable table) { private T readLatin1FixedObject(Latin1JsonReader reader) { reader.enterDepth(); - if (creationKind != MUTABLE) { - if (creationKind == CREATOR) { - Object[] arguments = readLatin1CreatorArguments(reader); - reader.exitDepth(); - return create(arguments); - } - T object = readLatin1Record(reader); + if (creatorInfo != null) { + Object[] arguments = readLatin1CreatorArguments(reader); reader.exitDepth(); - return object; + return create(arguments); } T object = newInstance(); reader.expect('{'); @@ -427,15 +376,10 @@ final T readUtf16Object(Utf16JsonReader reader, JsonFieldTable table) { private T readUtf16FixedObject(Utf16JsonReader reader) { reader.enterDepth(); - if (creationKind != MUTABLE) { - if (creationKind == CREATOR) { - Object[] arguments = readUtf16CreatorArguments(reader); - reader.exitDepth(); - return create(arguments); - } - T object = readUtf16Record(reader); + if (creatorInfo != null) { + Object[] arguments = readUtf16CreatorArguments(reader); reader.exitDepth(); - return object; + return create(arguments); } T object = newInstance(); reader.expect('{'); @@ -470,15 +414,10 @@ final T readUtf8Object(Utf8JsonReader reader, JsonFieldTable table) { private T readUtf8FixedObject(Utf8JsonReader reader) { reader.enterDepth(); - if (creationKind != MUTABLE) { - if (creationKind == CREATOR) { - Object[] arguments = readUtf8CreatorArguments(reader); - reader.exitDepth(); - return create(arguments); - } - T object = readUtf8Record(reader); + if (creatorInfo != null) { + Object[] arguments = readUtf8CreatorArguments(reader); reader.exitDepth(); - return object; + return create(arguments); } T object = newInstance(); reader.expect('{'); @@ -539,10 +478,8 @@ public T readUtf8(Utf8JsonReader reader) { private T readLatin1AnyObject(Latin1JsonReader reader, JsonFieldTable table) { reader.enterDepth(); T object; - if (creationKind == CREATOR) { + if (creatorInfo != null) { object = create(readLatin1AnyCreatorArguments(reader, table)); - } else if (creationKind == RECORD) { - object = readLatin1AnyRecord(reader, table); } else { object = readLatin1AnyMutable(reader, table); } @@ -553,10 +490,8 @@ private T readLatin1AnyObject(Latin1JsonReader reader, JsonFieldTable table) { private T readUtf16AnyObject(Utf16JsonReader reader, JsonFieldTable table) { reader.enterDepth(); T object; - if (creationKind == CREATOR) { + if (creatorInfo != null) { object = create(readUtf16AnyCreatorArguments(reader, table)); - } else if (creationKind == RECORD) { - object = readUtf16AnyRecord(reader, table); } else { object = readUtf16AnyMutable(reader, table); } @@ -567,10 +502,8 @@ private T readUtf16AnyObject(Utf16JsonReader reader, JsonFieldTable table) { private T readUtf8AnyObject(Utf8JsonReader reader, JsonFieldTable table) { reader.enterDepth(); T object; - if (creationKind == CREATOR) { + if (creatorInfo != null) { object = create(readUtf8AnyCreatorArguments(reader, table)); - } else if (creationKind == RECORD) { - object = readUtf8AnyRecord(reader, table); } else { object = readUtf8AnyMutable(reader, table); } @@ -710,105 +643,6 @@ private T readUtf8AnyMutable(Utf8JsonReader reader, JsonFieldTable table) { return object; } - private T readLatin1AnyRecord(Latin1JsonReader reader, JsonFieldTable table) { - Object[] values = newRecordFieldValues(); - Map anyMap = null; - Latin1ReaderCodec anyReader = anyInfo.valueTypeInfo.latin1Reader(); - reader.expect('{'); - if (!reader.consume('}')) { - do { - int start = reader.position(); - long hash = reader.readFieldNameHash(); - int match = table.match(hash); - reader.expect(':'); - if (match >= 0) { - JsonFieldInfo field = readFields[match]; - values[field.readIndex()] = field.readLatin1Value(reader); - } else if (match == JsonFieldTable.SKIP) { - reader.skipValue(); - } else { - String name = reader.materializeFieldName(start); - Object value = anyReader.readLatin1(reader); - if (anyMap == null) { - anyMap = newAnyMap(); - } - putAnyMap(anyMap, name, value); - } - } while (reader.consume(',')); - reader.expect('}'); - } - if (anyMap != null) { - values[anyInfo.constructionIndex] = finishAnyMap(anyMap); - } - return newRecord(values); - } - - private T readUtf16AnyRecord(Utf16JsonReader reader, JsonFieldTable table) { - Object[] values = newRecordFieldValues(); - Map anyMap = null; - Utf16ReaderCodec anyReader = anyInfo.valueTypeInfo.utf16Reader(); - reader.expect('{'); - if (!reader.consume('}')) { - do { - int start = reader.position(); - long hash = reader.readFieldNameHash(); - int match = table.match(hash); - reader.expect(':'); - if (match >= 0) { - JsonFieldInfo field = readFields[match]; - values[field.readIndex()] = field.readUtf16Value(reader); - } else if (match == JsonFieldTable.SKIP) { - reader.skipValue(); - } else { - String name = reader.materializeFieldName(start); - Object value = anyReader.readUtf16(reader); - if (anyMap == null) { - anyMap = newAnyMap(); - } - putAnyMap(anyMap, name, value); - } - } while (reader.consume(',')); - reader.expect('}'); - } - if (anyMap != null) { - values[anyInfo.constructionIndex] = finishAnyMap(anyMap); - } - return newRecord(values); - } - - private T readUtf8AnyRecord(Utf8JsonReader reader, JsonFieldTable table) { - Object[] values = newRecordFieldValues(); - Map anyMap = null; - Utf8ReaderCodec anyReader = anyInfo.valueTypeInfo.utf8Reader(); - reader.expect('{'); - if (!reader.consume('}')) { - do { - int start = reader.position(); - long hash = reader.readFieldNameHash(); - int match = table.match(hash); - reader.expect(':'); - if (match >= 0) { - JsonFieldInfo field = readFields[match]; - values[field.readIndex()] = field.readUtf8Value(reader); - } else if (match == JsonFieldTable.SKIP) { - reader.skipValue(); - } else { - String name = reader.materializeFieldName(start); - Object value = anyReader.readUtf8(reader); - if (anyMap == null) { - anyMap = newAnyMap(); - } - putAnyMap(anyMap, name, value); - } - } while (reader.consume(',')); - reader.expect('}'); - } - if (anyMap != null) { - values[anyInfo.constructionIndex] = finishAnyMap(anyMap); - } - return newRecord(values); - } - private Object[] readLatin1AnyCreatorArguments(Latin1JsonReader reader, JsonFieldTable table) { Object[] arguments = creatorInfo.newArguments(); JsonCreatorFieldInfo[] fields = creatorInfo.fields(); @@ -920,60 +754,6 @@ private Object[] readUtf8AnyCreatorArguments(Utf8JsonReader reader, JsonFieldTab return arguments; } - private T readLatin1Record(Latin1JsonReader reader) { - Object[] values = newRecordFieldValues(); - reader.expect('{'); - if (!reader.consume('}')) { - do { - JsonFieldInfo field = reader.readField(readTable); - reader.expect(':'); - if (field == null) { - reader.skipValue(); - } else { - values[field.readIndex()] = field.readLatin1Value(reader); - } - } while (reader.consume(',')); - reader.expect('}'); - } - return newRecord(values); - } - - private T readUtf16Record(Utf16JsonReader reader) { - Object[] values = newRecordFieldValues(); - reader.expect('{'); - if (!reader.consume('}')) { - do { - JsonFieldInfo field = reader.readField(readTable); - reader.expect(':'); - if (field == null) { - reader.skipValue(); - } else { - values[field.readIndex()] = field.readUtf16Value(reader); - } - } while (reader.consume(',')); - reader.expect('}'); - } - return newRecord(values); - } - - private T readUtf8Record(Utf8JsonReader reader) { - Object[] values = newRecordFieldValues(); - reader.expect('{'); - if (!reader.consume('}')) { - do { - JsonFieldInfo field = reader.readField(readTable); - reader.expect(':'); - if (field == null) { - reader.skipValue(); - } else { - values[field.readIndex()] = field.readUtf8Value(reader); - } - } while (reader.consume(',')); - reader.expect('}'); - } - return newRecord(values); - } - private Object[] readLatin1CreatorArguments(Latin1JsonReader reader) { Object[] arguments = creatorInfo.newArguments(); JsonCreatorFieldInfo[] fields = creatorInfo.fields(); @@ -1158,18 +938,6 @@ private void writeAnyMembers(Utf8JsonWriter writer, T value, int written) { } } - private static Object[] recordFieldDefaults( - Class type, String[] readJavaNames, RecordInfo recordInfo) { - Object[] defaults = new Object[readJavaNames.length]; - Object[] componentDefaults = recordInfo.getRecordComponentsDefaultValues(); - Map componentIndexes = RecordUtils.buildFieldToComponentMapping(type); - for (int i = 0; i < readJavaNames.length; i++) { - Integer componentIndex = componentIndexes.get(readJavaNames[i]); - defaults[i] = componentIndex == null ? null : componentDefaults[componentIndex.intValue()]; - } - return defaults; - } - @Internal public static final class AnyInfo { private final Field writeField; @@ -1179,6 +947,7 @@ public static final class AnyInfo { private final Class setterValueRawType; private final JsonFieldAccessor writeAccessor; private final JsonFieldAccessor readAccessor; + private final JsonAnySetterAccessor generatedSetter; private final MethodHandle setterHandle; private final Type mapType; private final Class mapRawType; @@ -1196,6 +965,9 @@ public static final class AnyInfo { Method writeGetter, Field readField, Method readSetter, + JsonFieldAccessor writeAccessor, + JsonFieldAccessor readAccessor, + JsonAnySetterAccessor generatedSetter, Type mapType, Class mapRawType, Type valueType, @@ -1209,14 +981,14 @@ public static final class AnyInfo { this.readField = readField; this.readSetter = readSetter; setterValueRawType = readSetter == null ? null : readSetter.getParameterTypes()[1]; - writeAccessor = - writeGetter != null - ? JsonFieldAccessor.forGetter(writeGetter) - : writeField == null ? null : JsonFieldAccessor.forField(writeField); - readAccessor = readField == null ? null : JsonFieldAccessor.forField(readField); + this.writeAccessor = writeAccessor; + this.readAccessor = readAccessor; + this.generatedSetter = generatedSetter; setterHandle = - readSetter == null || AndroidSupport.IS_ANDROID ? null : methodHandle(readSetter); - if (readSetter != null && AndroidSupport.IS_ANDROID) { + readSetter == null || generatedSetter != null || AndroidSupport.IS_ANDROID + ? null + : methodHandle(readSetter); + if (readSetter != null && generatedSetter == null && AndroidSupport.IS_ANDROID) { readSetter.setAccessible(true); } this.mapType = mapType; @@ -1284,7 +1056,7 @@ private boolean writeEnabled() { } private boolean readEnabled() { - return readAccessor != null || readSetter != null; + return constructionIndex >= 0 || readAccessor != null || readSetter != null; } private boolean fieldRead() { @@ -1321,6 +1093,10 @@ private void put(Object target, String name, Object value) { throw new ForyJsonException( "Cannot read null into primitive @JsonAnySetter parameter " + setterValueRawType); } + if (generatedSetter != null) { + generatedSetter.put(target, name, value); + return; + } try { if (AndroidSupport.IS_ANDROID) { readSetter.invoke(target, name, value); @@ -1352,20 +1128,11 @@ private ParameterizedObjectCodec( Class type, JsonFieldInfo[] writeFields, JsonFieldInfo[] readFields, - String[] readJavaNames, JsonCreatorInfo creatorInfo, AnyInfo anyInfo, String[] skippedNames, ObjectInstantiator instantiator) { - super( - type, - writeFields, - readFields, - readJavaNames, - creatorInfo, - anyInfo, - skippedNames, - instantiator); + super(type, writeFields, readFields, creatorInfo, anyInfo, skippedNames, instantiator); } @Override diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java index ddc2174d9c..0f03b04f4d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java @@ -24,6 +24,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; +import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; @@ -50,6 +51,7 @@ import org.apache.fory.json.annotation.JsonRawValue; import org.apache.fory.json.annotation.JsonValue; import org.apache.fory.json.codec.ObjectCodec.AnyInfo; +import org.apache.fory.json.meta.JsonAnySetterAccessor; import org.apache.fory.json.meta.JsonCreatorDeclaration; import org.apache.fory.json.meta.JsonCreatorFieldInfo; import org.apache.fory.json.meta.JsonCreatorInfo; @@ -70,23 +72,24 @@ static ObjectCodec build( TypeRef ownerType, boolean propertyDiscoveryEnabled, PropertyNamingStrategy propertyNamingStrategy, - boolean writeNullFields) { + boolean writeNullFields, + GeneratedJsonCodec generatedCodec) { Class type = ownerType.getRawType(); - if (type.isInterface() - || Modifier.isAbstract(type.getModifiers()) - || type.isPrimitive() - || type.isArray() - || type.isEnum()) { - throw new ForyJsonException("Unsupported JSON object type " + type); - } - boolean record = RecordUtils.isRecord(type); - boolean hasAnyField = validateMemberAnnotations(type, propertyDiscoveryEnabled, record); + boolean record = + generatedCodec == null ? RecordUtils.isRecord(type) : generatedCodec.validatedRecord(); + boolean hasAnyField = + validateMemberAnnotations(type, propertyDiscoveryEnabled, record, generatedCodec); LinkedHashMap builders = new LinkedHashMap<>(); addFields(type, record, propertyDiscoveryEnabled, hasAnyField, builders); if (record) { - addRecordAccessors(type, builders); + addRecordAccessors(type, builders, generatedCodec); + } + Method anySetter = + addJsonMethods(type, propertyDiscoveryEnabled, record, builders, generatedCodec); + if (generatedCodec != null && generatedCodec.hasAnySetter() && anySetter == null) { + throw new ForyJsonException( + "Generated JSON Any setter does not match runtime annotations on " + type.getName()); } - Method anySetter = addJsonMethods(type, propertyDiscoveryEnabled, record, builders); FieldBuilder anyBuilder = findAnyBuilder(type, builders); if (anyBuilder != null && anyBuilder.anyField != null) { if (anyBuilder.anyGetter != null || anySetter != null) { @@ -104,8 +107,9 @@ boolean record = RecordUtils.isRecord(type); } JsonCreatorInfo creatorInfo = record - ? rejectRecordCreator(type) - : buildCreatorInfo(type, ownerType, builders, propertyNamingStrategy); + ? buildRecordCreatorInfo( + type, ownerType, builders, propertyNamingStrategy, generatedCodec) + : buildCreatorInfo(type, ownerType, builders, propertyNamingStrategy, generatedCodec); if (anySetter != null && (record || creatorInfo != null)) { throw new ForyJsonException( "@JsonAnySetter is not supported on constructor-backed type " + type.getName()); @@ -124,7 +128,6 @@ boolean record = RecordUtils.isRecord(type); List writes = new ArrayList<>(); List writeBuilders = orderWrites ? new ArrayList<>(builders.size()) : null; List reads = new ArrayList<>(); - List readJavaNames = record ? new ArrayList<>() : null; List skippedNames = hasAny ? new ArrayList<>() : null; Map canonicalNames = new LinkedHashMap<>(); Map canonicalHashes = new LinkedHashMap<>(); @@ -193,7 +196,7 @@ boolean record = RecordUtils.isRecord(type); continue; } JsonFieldInfo field = - builder.build(record, ownerType, propertyNamingStrategy, writeNullFields); + builder.build(record, ownerType, propertyNamingStrategy, writeNullFields, generatedCodec); if (!hasAny) { FieldBuilder priorProperty = canonicalNames.put(field.name(), builder); if (priorProperty != null) { @@ -226,9 +229,6 @@ boolean record = RecordUtils.isRecord(type); } } reads.add(field); - if (record) { - readJavaNames.add(builder.name); - } } } if (hasAny && creatorInfo != null) { @@ -259,22 +259,20 @@ boolean record = RecordUtils.isRecord(type); } int constructionIndex = -1; if (anyBuilder != null && anyBuilder.anyReadEnabled()) { - if (record) { - constructionIndex = readArray.length; - readJavaNames.add(anyBuilder.name); - } else if (creatorInfo != null) { + if (creatorInfo != null) { constructionIndex = anyBuilder.creatorArgumentIndex; } } AnyInfo anyInfo = hasAny - ? buildAnyInfo(ownerType, anyBuilder, anySetter, anyWriteIndex, constructionIndex) + ? buildAnyInfo( + ownerType, anyBuilder, anySetter, anyWriteIndex, constructionIndex, generatedCodec) : null; - ObjectInstantiator instantiator = ObjectInstantiators.createObjectInstantiator(type); - String[] recordNames = record ? readJavaNames.toArray(new String[0]) : null; + ObjectInstantiator instantiator = + creatorInfo == null ? ObjectInstantiators.createObjectInstantiator(type) : null; String[] skipped = hasAny ? skippedNames.toArray(new String[0]) : null; return ObjectCodec.createCodec( - ownerType, writeArray, readArray, recordNames, creatorInfo, anyInfo, skipped, instantiator); + ownerType, writeArray, readArray, creatorInfo, anyInfo, skipped, instantiator); } private static JsonPropertyOrder findPropertyOrder(Class type) { @@ -588,7 +586,8 @@ private static Method addJsonMethods( Class type, boolean propertyDiscoveryEnabled, boolean record, - LinkedHashMap builders) { + LinkedHashMap builders, + GeneratedJsonCodec generatedCodec) { Method anyGetter = null; Method anySetter = null; for (Method method : type.getMethods()) { @@ -599,7 +598,7 @@ private static Method addJsonMethods( } if (method.getDeclaringClass().isInterface() && method.isAnnotationPresent(JsonProperty.class)) { - validatePropertyMethod(type, method, propertyDiscoveryEnabled, record); + validatePropertyMethod(type, method, propertyDiscoveryEnabled, record, generatedCodec); } JsonAnyGetter getter = method.getAnnotation(JsonAnyGetter.class); JsonAnySetter setter = method.getAnnotation(JsonAnySetter.class); @@ -717,21 +716,44 @@ private static FieldBuilder findAnyBuilder( } private static void addRecordAccessors( - Class type, LinkedHashMap builders) { + Class type, + LinkedHashMap builders, + GeneratedJsonCodec generatedCodec) { + if (generatedCodec != null) { + String[] names = generatedCodec.validatedCreatorParameterNames(); + Class[] parameterTypes = generatedCodec.validatedCreatorParameterTypes(); + for (int i = 0; i < names.length; i++) { + Method accessor; + try { + accessor = type.getDeclaredMethod(names[i]); + } catch (NoSuchMethodException e) { + throw new ForyJsonException( + "Missing generated JSON record accessor " + names[i] + " on " + type.getName(), e); + } + if (accessor.getParameterCount() != 0 || accessor.getReturnType() != parameterTypes[i]) { + throw new ForyJsonException("Invalid JSON record accessor " + accessor); + } + FieldBuilder builder = builders.get(names[i]); + if (builder == null) { + throw new ForyJsonException("Missing JSON record field " + names[i]); + } + builder.setWriteGetter(type, accessor); + } + return; + } RecordComponent[] components = RecordUtils.getRecordComponents(type); for (RecordComponent component : components) { FieldBuilder builder = builders.get(component.getName()); if (builder == null) { throw new ForyJsonException("Missing JSON record field " + component.getName()); } - // Record accessors carry component annotations in Java 16+, but field access remains the - // optimized read/write owner. The accessor participates only in logical-property annotation - // merging and is discarded before hot metadata is published. - builder.mergeAnnotation(type, component.getAccessor()); + // Component accessors are the Record value source on every platform. This preserves an + // explicitly implemented accessor and keeps native and Android-desugared Records identical. + builder.setWriteGetter(type, component.getAccessor()); } } - private static JsonCreatorInfo rejectRecordCreator(Class type) { + private static void rejectRecordCreator(Class type) { for (Constructor constructor : type.getDeclaredConstructors()) { if (constructor.isAnnotationPresent(JsonCreator.class)) { throw new ForyJsonException("@JsonCreator is not supported on record " + type.getName()); @@ -742,20 +764,85 @@ private static JsonCreatorInfo rejectRecordCreator(Class type) { throw new ForyJsonException("@JsonCreator is not supported on record " + type.getName()); } } - return null; + } + + private static JsonCreatorInfo buildRecordCreatorInfo( + Class type, + TypeRef ownerType, + LinkedHashMap builders, + PropertyNamingStrategy namingStrategy, + GeneratedJsonCodec generatedCodec) { + rejectRecordCreator(type); + String[] names; + Class[] rawTypes; + Constructor constructor; + if (generatedCodec == null) { + RecordComponent[] components = RecordUtils.getRecordComponents(type); + names = new String[components.length]; + rawTypes = new Class[components.length]; + for (int i = 0; i < components.length; i++) { + names[i] = components[i].getName(); + rawTypes[i] = components[i].getType(); + } + constructor = RecordUtils.getRecordConstructor(type).f0; + } else { + names = generatedCodec.validatedCreatorParameterNames(); + rawTypes = generatedCodec.validatedCreatorParameterTypes(); + if (names == null || !generatedCodec.validatedRecord()) { + throw new ForyJsonException( + "Generated JSON record creator metadata is missing for " + type); + } + constructor = (Constructor) generatedCodec.validatedCreator(); + } + Type[] parameterTypes = constructor.getGenericParameterTypes(); + Parameter[] parameters = constructor.getParameters(); + List fields = new ArrayList<>(names.length); + for (int i = 0; i < names.length; i++) { + FieldBuilder builder = builders.get(names[i]); + if (builder == null || !builder.hasLogicalMember()) { + throw new ForyJsonException("Unknown JSON record component " + names[i]); + } + builder.mergeAnnotation(type, parameters[i]); + if (!builder.creatorReadAllowed()) { + continue; + } + bindCreatorType(ownerType, constructor, i, parameterTypes[i], builder); + if (builder.isAny()) { + continue; + } + Type resolved = ownerType.resolveType(parameterTypes[i]).getType(); + fields.add( + new JsonCreatorFieldInfo( + builder.jsonName(namingStrategy), + i, + resolved, + rawTypes[i], + builder.codecAnnotation(), + builder.valueCodecClass())); + } + JsonCreatorFieldInfo[] fieldArray = fields.toArray(new JsonCreatorFieldInfo[0]); + rejectCreatorHashCollisions(fieldArray); + return new JsonCreatorInfo( + type, constructor, fieldArray, creatorDefaults(rawTypes), generatedCodec); } private static JsonCreatorInfo buildCreatorInfo( Class type, TypeRef ownerType, LinkedHashMap builders, - PropertyNamingStrategy namingStrategy) { + PropertyNamingStrategy namingStrategy, + GeneratedJsonCodec generatedCodec) { JsonCreatorDeclaration declaration = JsonCreatorDeclaration.find(type); if (declaration == null) { + if (generatedCodec != null && generatedCodec.validatedCreatorParameterNames() != null) { + throw new ForyJsonException( + "Generated JSON creator does not match runtime annotations on " + type.getName()); + } return null; } Executable creator = declaration.executable(); JsonCreator annotation = declaration.annotation(); + validateGeneratedCreator(type, creator, annotation, generatedCodec); Map jsonProperties = new LinkedHashMap<>(); for (FieldBuilder builder : builders.values()) { @@ -884,7 +971,42 @@ private static JsonCreatorInfo buildCreatorInfo( } JsonCreatorFieldInfo[] fieldArray = fields.toArray(new JsonCreatorFieldInfo[0]); rejectCreatorHashCollisions(fieldArray); - return new JsonCreatorInfo(type, creator, fieldArray, creatorDefaults(rawTypes)); + return new JsonCreatorInfo( + type, creator, fieldArray, creatorDefaults(rawTypes), generatedCodec); + } + + private static void validateGeneratedCreator( + Class type, + Executable creator, + JsonCreator annotation, + GeneratedJsonCodec generatedCodec) { + if (generatedCodec == null) { + return; + } + String[] names = generatedCodec.validatedCreatorParameterNames(); + Class[] parameterTypes = generatedCodec.validatedCreatorParameterTypes(); + String factoryName = generatedCodec.validatedCreatorFactoryName(); + if (names == null + || !creator.equals(generatedCodec.validatedCreator()) + || !Arrays.equals(parameterTypes, creator.getParameterTypes()) + || creator instanceof Method != (factoryName != null) + || creator instanceof Method && !creator.getName().equals(factoryName)) { + throw new ForyJsonException( + "Generated JSON creator signature does not match " + creator + " on " + type.getName()); + } + String[] runtimeNames = annotation.value(); + if (runtimeNames.length == 0) { + runtimeNames = new String[creator.getParameterCount()]; + Parameter[] parameters = creator.getParameters(); + for (int i = 0; i < parameters.length; i++) { + JsonProperty property = parameters[i].getAnnotation(JsonProperty.class); + runtimeNames[i] = property == null ? null : property.value(); + } + } + if (!Arrays.equals(names, runtimeNames)) { + throw new ForyJsonException( + "Generated JSON creator names do not match " + creator + " on " + type.getName()); + } } private static void validatePropertyIndex( @@ -964,7 +1086,10 @@ private static Object[] creatorDefaults(Class[] types) { } private static boolean validateMemberAnnotations( - Class type, boolean propertyDiscoveryEnabled, boolean record) { + Class type, + boolean propertyDiscoveryEnabled, + boolean record, + GeneratedJsonCodec generatedCodec) { boolean hasAnyField = false; for (Class current = type; current != null && current != Object.class; @@ -1007,17 +1132,17 @@ private static boolean validateMemberAnnotations( continue; } if (method.isAnnotationPresent(JsonCodec.class)) { - validateCodecMethod(type, method, propertyDiscoveryEnabled, record); + validateCodecMethod(type, method, propertyDiscoveryEnabled, record, generatedCodec); } validateCodecParameters(method, propertyDiscoveryEnabled, record); if (method.isAnnotationPresent(JsonRawValue.class)) { - validateRawMethod(type, method, propertyDiscoveryEnabled, record); + validateRawMethod(type, method, propertyDiscoveryEnabled, record, generatedCodec); } if (method.isAnnotationPresent(JsonBase64.class)) { - validateBase64Method(type, method, propertyDiscoveryEnabled, record); + validateBase64Method(type, method, propertyDiscoveryEnabled, record, generatedCodec); } if (method.isAnnotationPresent(JsonProperty.class)) { - validatePropertyMethod(type, method, propertyDiscoveryEnabled, record); + validatePropertyMethod(type, method, propertyDiscoveryEnabled, record, generatedCodec); } if (method.isAnnotationPresent(JsonAnyGetter.class)) { if (!propertyDiscoveryEnabled) { @@ -1036,7 +1161,7 @@ private static boolean validateMemberAnnotations( } } for (Constructor constructor : type.getDeclaredConstructors()) { - validateCodecParameters(type, constructor, record); + validateCodecParameters(type, constructor, record, generatedCodec); } for (Method method : type.getMethods()) { if (!method.getDeclaringClass().isInterface()) { @@ -1045,21 +1170,25 @@ private static boolean validateMemberAnnotations( if (method.isAnnotationPresent(JsonCodec.class)) { // getMethods exposes only the effective inherited declaration. A class or child-interface // override therefore suppresses an annotation from the overridden interface method. - validateCodecMethod(type, method, propertyDiscoveryEnabled, record); + validateCodecMethod(type, method, propertyDiscoveryEnabled, record, generatedCodec); } validateCodecParameters(method, propertyDiscoveryEnabled, record); if (method.isAnnotationPresent(JsonRawValue.class)) { - validateRawMethod(type, method, propertyDiscoveryEnabled, record); + validateRawMethod(type, method, propertyDiscoveryEnabled, record, generatedCodec); } if (method.isAnnotationPresent(JsonBase64.class)) { - validateBase64Method(type, method, propertyDiscoveryEnabled, record); + validateBase64Method(type, method, propertyDiscoveryEnabled, record, generatedCodec); } } return hasAnyField; } private static void validateCodecMethod( - Class type, Method method, boolean propertyDiscoveryEnabled, boolean record) { + Class type, + Method method, + boolean propertyDiscoveryEnabled, + boolean record, + GeneratedJsonCodec generatedCodec) { if (method.isAnnotationPresent(JsonAnyGetter.class)) { if (!propertyDiscoveryEnabled) { throw new ForyJsonException( @@ -1069,9 +1198,7 @@ private static void validateCodecMethod( return; } if (record) { - // javac copies a record-component annotation to every applicable generated member. The - // component/backing field remains the existing codec owner; tolerate its identical copies. - if (isPropagatedRecordCodec(type, method)) { + if (isRecordAccessor(type, method, generatedCodec)) { return; } throw new ForyJsonException( @@ -1115,14 +1242,17 @@ && setterPropertyName(method) != null } private static void validateCodecParameters( - Class type, Constructor constructor, boolean record) { + Class type, + Constructor constructor, + boolean record, + GeneratedJsonCodec generatedCodec) { Parameter[] parameters = constructor.getParameters(); for (int i = 0; i < parameters.length; i++) { JsonCodec annotation = parameters[i].getAnnotation(JsonCodec.class); if (annotation == null || constructor.isAnnotationPresent(JsonCreator.class)) { continue; } - if (record && isPropagatedRecordCodec(type, constructor, i, annotation)) { + if (record && isRecordConstructor(type, constructor, generatedCodec)) { continue; } throw new ForyJsonException("@JsonCodec parameter requires a @JsonCreator: " + constructor); @@ -1145,9 +1275,14 @@ private static void validateRawField(Field field) { } private static void validateRawMethod( - Class type, Method method, boolean propertyDiscoveryEnabled, boolean record) { + Class type, + Method method, + boolean propertyDiscoveryEnabled, + boolean record, + GeneratedJsonCodec generatedCodec) { if ((!propertyDiscoveryEnabled - && !(record && isPropagatedRecordAnnotation(type, method, JsonRawValue.class))) + && !(record + && isPropagatedRecordAnnotation(type, method, JsonRawValue.class, generatedCodec))) || !isEligibleAccessor(method) || method.isVarArgs() || method.getTypeParameters().length != 0 @@ -1155,7 +1290,7 @@ private static void validateRawMethod( || method.getReturnType() != String.class || (!method.isAnnotationPresent(JsonValue.class) && ((!record && getterPropertyName(method) == null) - || (record && !isRecordAccessor(type, method))))) { + || (record && !isRecordAccessor(type, method, generatedCodec))))) { throw new ForyJsonException("Invalid @JsonRawValue method " + method); } if (method.isAnnotationPresent(JsonCodec.class) @@ -1181,16 +1316,21 @@ private static void validateBase64Field(Field field) { } private static void validateBase64Method( - Class type, Method method, boolean propertyDiscoveryEnabled, boolean record) { + Class type, + Method method, + boolean propertyDiscoveryEnabled, + boolean record, + GeneratedJsonCodec generatedCodec) { if ((!propertyDiscoveryEnabled - && !(record && isPropagatedRecordAnnotation(type, method, JsonBase64.class))) + && !(record + && isPropagatedRecordAnnotation(type, method, JsonBase64.class, generatedCodec))) || !isEligibleAccessor(method) || method.isVarArgs() || method.getTypeParameters().length != 0 || method.getParameterCount() != 0 || method.getReturnType() != byte[].class || ((!record && getterPropertyName(method) == null) - || (record && !isRecordAccessor(type, method)))) { + || (record && !isRecordAccessor(type, method, generatedCodec)))) { throw new ForyJsonException("Invalid @JsonBase64 method " + method); } if (method.isAnnotationPresent(JsonCodec.class) @@ -1200,43 +1340,27 @@ private static void validateBase64Method( } } - private static boolean isPropagatedRecordCodec(Class type, Method method) { - if (!isRecordAccessor(type, method)) { - return false; - } - try { - JsonCodec fieldCodec = type.getDeclaredField(method.getName()).getAnnotation(JsonCodec.class); - JsonCodec methodCodec = method.getAnnotation(JsonCodec.class); - return fieldCodec != null && fieldCodec.equals(methodCodec); - } catch (NoSuchFieldException e) { - return false; - } catch (RuntimeException | LinkageError e) { - throw new ForyJsonException("Cannot read record-component @JsonCodec for " + method, e); - } - } - - private static boolean isPropagatedRecordCodec( - Class type, Constructor constructor, int parameterIndex, JsonCodec annotation) { - RecordComponent[] components = RecordUtils.getRecordComponents(type); - if (components.length != constructor.getParameterCount() - || parameterIndex >= components.length - || components[parameterIndex].getType() - != constructor.getParameterTypes()[parameterIndex]) { - return false; - } - try { - JsonCodec fieldCodec = - type.getDeclaredField(components[parameterIndex].getName()) - .getAnnotation(JsonCodec.class); - return annotation.equals(fieldCodec); - } catch (NoSuchFieldException e) { - return false; + private static boolean isRecordConstructor( + Class type, Constructor constructor, GeneratedJsonCodec generatedCodec) { + Class[] componentTypes; + if (generatedCodec == null) { + RecordComponent[] components = RecordUtils.getRecordComponents(type); + componentTypes = new Class[components.length]; + for (int i = 0; i < components.length; i++) { + componentTypes[i] = components[i].getType(); + } + } else { + componentTypes = generatedCodec.validatedCreatorParameterTypes(); } + return Arrays.equals(componentTypes, constructor.getParameterTypes()); } private static boolean isPropagatedRecordAnnotation( - Class type, Method method, Class annotationType) { - if (!isRecordAccessor(type, method)) { + Class type, + Method method, + Class annotationType, + GeneratedJsonCodec generatedCodec) { + if (!isRecordAccessor(type, method, generatedCodec)) { return false; } try { @@ -1294,10 +1418,14 @@ private static void validateAnySetter(Method method) { } private static void validatePropertyMethod( - Class type, Method method, boolean propertyDiscoveryEnabled, boolean record) { + Class type, + Method method, + boolean propertyDiscoveryEnabled, + boolean record, + GeneratedJsonCodec generatedCodec) { if (!propertyDiscoveryEnabled || !isEligibleAccessor(method) - || record && !isRecordAccessor(type, method)) { + || record && !isRecordAccessor(type, method, generatedCodec)) { throw new ForyJsonException("@JsonProperty is not supported on JSON method: " + method); } if (!record && getterPropertyName(method) == null && setterPropertyName(method) == null) { @@ -1305,7 +1433,20 @@ private static void validatePropertyMethod( } } - private static boolean isRecordAccessor(Class type, Method method) { + private static boolean isRecordAccessor( + Class type, Method method, GeneratedJsonCodec generatedCodec) { + if (generatedCodec != null) { + String[] names = generatedCodec.validatedCreatorParameterNames(); + Class[] types = generatedCodec.validatedCreatorParameterTypes(); + for (int i = 0; i < names.length; i++) { + if (method.getName().equals(names[i]) + && method.getParameterCount() == 0 + && method.getReturnType() == types[i]) { + return true; + } + } + return false; + } RecordComponent[] components = RecordUtils.getRecordComponents(type); for (RecordComponent component : components) { if (component.getAccessor().equals(method)) { @@ -1315,6 +1456,10 @@ private static boolean isRecordAccessor(Class type, Method method) { return false; } + private static boolean isRecordAccessor(Class type, Method method) { + return isRecordAccessor(type, method, null); + } + private static boolean isEligibleField(Field field) { int modifiers = field.getModifiers(); return !Modifier.isStatic(modifiers) @@ -1379,11 +1524,12 @@ private static AnyInfo buildAnyInfo( FieldBuilder builder, Method anySetter, int writeIndex, - int constructionIndex) { + int constructionIndex, + GeneratedJsonCodec generatedCodec) { Field anyField = builder == null ? null : builder.anyField; - Method anyGetter = builder == null ? null : builder.anyGetter; - Field writeField = anyField != null && builder.anyWriteEnabled() ? anyField : null; - Method writeGetter = anyGetter; + Method writeGetter = builder == null || !builder.anyWriteEnabled() ? null : builder.writeGetter; + Field writeField = + writeGetter == null && anyField != null && builder.anyWriteEnabled() ? anyField : null; Field readField = anyField != null && builder.anyReadEnabled() ? anyField : null; Type mapType = null; Class mapRawType = null; @@ -1391,12 +1537,13 @@ private static AnyInfo buildAnyInfo( Class valueRawType = null; Class> valueCodecClass = null; JsonCodec valueCodecAnnotation = null; - if (anyField != null || anyGetter != null) { + if (anyField != null || writeGetter != null) { Type declaredMapType = - anyGetter == null ? anyField.getGenericType() : anyGetter.getGenericReturnType(); + writeGetter == null ? anyField.getGenericType() : writeGetter.getGenericReturnType(); mapType = ownerType.resolveType(declaredMapType).getType(); mapRawType = CodecUtils.rawType(mapType, null); - valueType = anyMapValueType(mapType, mapRawType, anyField != null ? anyField : anyGetter); + valueType = + anyMapValueType(mapType, mapRawType, writeGetter == null ? anyField : writeGetter); valueRawType = CodecUtils.rawType(valueType, Object.class); validateAnyLogicalTypes(ownerType, builder, mapType); if (builder.valueCodecClass() != null) { @@ -1437,11 +1584,20 @@ private static AnyInfo buildAnyInfo( valueCodecClass = null; } } + JsonAnySetterAccessor generatedAnySetter = + anySetter == null || generatedCodec == null ? null : generatedCodec.anySetter(anySetter); return new AnyInfo( writeField, writeGetter, readField, anySetter, + writeGetter != null + ? getterAccessor(generatedCodec, writeGetter) + : writeField == null ? null : fieldAccessor(generatedCodec, writeField), + readField == null || constructionIndex >= 0 + ? null + : fieldAccessor(generatedCodec, readField), + generatedAnySetter, mapType, mapRawType, valueType, @@ -1452,6 +1608,29 @@ private static AnyInfo buildAnyInfo( constructionIndex); } + private static JsonFieldAccessor generatedAccessor( + GeneratedJsonCodec generatedCodec, Member member) { + return generatedCodec == null ? null : generatedCodec.validatedAccessor(member); + } + + private static JsonFieldAccessor fieldAccessor( + GeneratedJsonCodec generatedCodec, Field field) { + JsonFieldAccessor accessor = generatedAccessor(generatedCodec, field); + return accessor == null ? JsonFieldAccessor.forField(field) : accessor; + } + + private static JsonFieldAccessor getterAccessor( + GeneratedJsonCodec generatedCodec, Method getter) { + JsonFieldAccessor accessor = generatedAccessor(generatedCodec, getter); + return accessor == null ? JsonFieldAccessor.forGetter(getter) : accessor; + } + + private static JsonFieldAccessor setterAccessor( + GeneratedJsonCodec generatedCodec, Method setter) { + JsonFieldAccessor accessor = generatedAccessor(generatedCodec, setter); + return accessor == null ? JsonFieldAccessor.forSetter(setter) : accessor; + } + private static Class> anyValueCodec( JsonCodec annotation, String source) { if (annotation.value() != JsonCodec.NoJsonValueCodec.class @@ -1724,7 +1903,8 @@ private JsonFieldInfo build( boolean record, TypeRef ownerType, PropertyNamingStrategy propertyNamingStrategy, - boolean defaultWriteNull) { + boolean defaultWriteNull, + GeneratedJsonCodec generatedCodec) { validateTypes(ownerType); if (explicitInclude != JsonProperty.Include.DEFAULT && !hasWriteSource()) { throw new ForyJsonException( @@ -1740,14 +1920,16 @@ private JsonFieldInfo build( && (rawWriteType.isPrimitive() || explicitInclude == JsonProperty.Include.ALWAYS || explicitInclude == JsonProperty.Include.DEFAULT && defaultWriteNull); - writeAccessor = - writeGetter != null - ? JsonFieldAccessor.forGetter(writeGetter) - : (writeField == null ? null : JsonFieldAccessor.forField(writeField)); - readAccessor = - readSetter != null - ? JsonFieldAccessor.forSetter(readSetter) - : (readField == null || record ? null : JsonFieldAccessor.forField(readField)); + if (writeGetter != null) { + writeAccessor = getterAccessor(generatedCodec, writeGetter); + } else if (writeField != null) { + writeAccessor = fieldAccessor(generatedCodec, writeField); + } + if (readSetter != null) { + readAccessor = setterAccessor(generatedCodec, readSetter); + } else if (readField != null && !record) { + readAccessor = fieldAccessor(generatedCodec, readField); + } boolean rawValue = rawValueSource != null; if (rawValue) { if (rawWriteType != null && rawWriteType != String.class) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index 78f94542df..a77f3a301b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -37,6 +37,7 @@ import org.apache.fory.json.codec.Utf16ReaderCodec; import org.apache.fory.json.codec.Utf8ReaderCodec; import org.apache.fory.json.codec.Utf8WriterCodec; +import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; import org.apache.fory.json.resolver.JsonTypeInfo; @@ -206,11 +207,9 @@ private Class buildLatin1Reader(ObjectCodec codec) { String code = any == null || any.readField() == null && any.readSetter() == null ? new Latin1ReaderCodegen(this) - .genReaderCode( - builder, type, codec.readFields(), codec.isRecord(), codec.creatorInfo()) + .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) : new Latin1ReaderCodegen(this) - .genAnyReaderCode( - builder, type, codec.readFields(), codec.isRecord(), codec.creatorInfo(), any); + .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); return compileCodecClass(generatedPackage, className, code); } @@ -224,11 +223,9 @@ private Class buildUtf16Reader(ObjectCodec codec) { String code = any == null || any.readField() == null && any.readSetter() == null ? new Utf16ReaderCodegen(this) - .genReaderCode( - builder, type, codec.readFields(), codec.isRecord(), codec.creatorInfo()) + .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) : new Utf16ReaderCodegen(this) - .genAnyReaderCode( - builder, type, codec.readFields(), codec.isRecord(), codec.creatorInfo(), any); + .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); return compileCodecClass(generatedPackage, className, code); } @@ -242,11 +239,9 @@ private Class buildUtf8Reader(ObjectCodec codec) { String code = any == null || any.readField() == null && any.readSetter() == null ? new Utf8ReaderCodegen(this) - .genReaderCode( - builder, type, codec.readFields(), codec.isRecord(), codec.creatorInfo()) + .genReaderCode(builder, type, codec.readFields(), codec.creatorInfo()) : new Utf8ReaderCodegen(this) - .genAnyReaderCode( - builder, type, codec.readFields(), codec.isRecord(), codec.creatorInfo(), any); + .genAnyReaderCode(builder, type, codec.readFields(), codec.creatorInfo(), any); return compileCodecClass(generatedPackage, className, code); } @@ -280,15 +275,22 @@ public boolean canCompileReader(ObjectCodec codec) { if (!canCompileType(codec.type())) { return false; } - boolean record = codec.isRecord(); + JsonCreatorInfo creator = codec.creatorInfo(); + if (creator != null) { + for (Class parameterType : creator.executable().getParameterTypes()) { + if (!canCompileType(parameterType)) { + return false; + } + } + } JsonFieldInfo[] properties = codec.readFields(); for (int i = 0; i < properties.length; i++) { - if (!canCompileRead(properties[i], record)) { + if (!canCompileRead(properties[i])) { return false; } } AnyInfo any = codec.anyInfo(); - return any == null || canCompileAnyRead(any, record, codec.creatorInfo() != null); + return any == null || canCompileAnyRead(any, codec.creatorInfo() != null); } private boolean canCompileAnyWrite(AnyInfo any) { @@ -300,11 +302,14 @@ private boolean canCompileAnyWrite(AnyInfo any) { if (getter != null && !canCall(getter)) { return false; } + if (field != null && !canCompileField(field)) { + return false; + } Class mapType = getter == null ? field.getType() : getter.getReturnType(); return isVisible(mapType) && isVisible(any.valueRawType()); } - private boolean canCompileAnyRead(AnyInfo any, boolean record, boolean creator) { + private boolean canCompileAnyRead(AnyInfo any, boolean creator) { Field field = any.readField(); Method setter = any.readSetter(); if (field == null && setter == null) { @@ -318,7 +323,10 @@ private boolean canCompileAnyRead(AnyInfo any, boolean record, boolean creator) if (field != null && !isVisible(field.getType())) { return false; } - if (setter != null && (record || creator)) { + if (field != null && !canCompileField(field)) { + return false; + } + if (setter != null && creator) { return false; } return isVisible(any.valueRawType()); @@ -496,6 +504,9 @@ private boolean canCompileWrite(JsonFieldInfo property) { if (property.writeGetter() != null && !canCall(property.writeGetter())) { return false; } + if (field != null && !canCompileField(field)) { + return false; + } Class rawType = property.writeRawType(); if (rawType != null && !rawType.isPrimitive() && !isVisible(rawType)) { return false; @@ -503,16 +514,19 @@ private boolean canCompileWrite(JsonFieldInfo property) { return true; } - private boolean canCompileRead(JsonFieldInfo property, boolean record) { - if (!record && property.readAccessor() == null) { + private boolean canCompileRead(JsonFieldInfo property) { + if (property.readAccessor() == null) { + return false; + } + if (property.readSetter() != null && !canCall(property.readSetter())) { return false; } - if (!record && property.readSetter() != null && !canCall(property.readSetter())) { + if (property.readField() != null && !canCompileField(property.readField())) { return false; } - if (!record - && property.readSetter() == null - && property.readAccessor().coreAccessor() == null) { + // Generated field accessors deliberately have no Fory core FieldAccessor. The selected Field + // remains the runtime-codegen owner, so exact field metadata is sufficient for direct codegen. + if (property.readSetter() == null && property.readField() == null) { return false; } Class rawType = property.readRawType(); @@ -526,6 +540,13 @@ private boolean canCompileType(Class type) { return isPublicSourceType(type) && isVisible(type); } + private boolean canCompileField(Field field) { + // Descriptor emits public fields as direct Java member access. A public field inherited from + // an inaccessible declaring class is reflectively visible but cannot be resolved by Janino. + // Non-public fields use the existing generated accessor path and do not spell their owner. + return !Modifier.isPublic(field.getModifiers()) || canCompileType(field.getDeclaringClass()); + } + private boolean canCall(Method method) { return Modifier.isPublic(method.getModifiers()) && isPublicSourceType(method.getDeclaringClass()); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java index cd7a4e3e0c..acc84ee657 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java @@ -26,11 +26,9 @@ import org.apache.fory.builder.CodecBuilder; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.Expression; -import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.reflect.TypeRef; import org.apache.fory.type.Descriptor; -import org.apache.fory.util.record.RecordUtils; /** * Common field-access and source-class builder for one concrete generated JSON capability. @@ -88,7 +86,7 @@ private Descriptor writeDescriptor(JsonFieldInfo property) { } private Descriptor writeDescriptor(Field field) { - return new Descriptor(field, TypeRef.of(field.getGenericType()), recordReadMethod(field), null); + return new Descriptor(field, TypeRef.of(field.getGenericType()), null, null); } private Descriptor readDescriptor(JsonFieldInfo property) { @@ -99,17 +97,6 @@ private Descriptor readDescriptor(Field field) { return new Descriptor(field, TypeRef.of(field.getGenericType()), null, null); } - private Method recordReadMethod(Field field) { - if (!RecordUtils.isRecord(field.getDeclaringClass())) { - return null; - } - try { - return field.getDeclaringClass().getMethod(field.getName()); - } catch (NoSuchMethodException e) { - throw new ForyJsonException("Cannot resolve record accessor for field " + field, e); - } - } - Expression fieldValue(JsonFieldInfo property, Expression object) { Method getter = property.writeGetter(); if (getter != null) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 3c54c8099d..d19c817ecd 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -96,8 +96,6 @@ final String readFieldMethod(String readMethod, int start) { return readMethod + "Field" + start; } - abstract String readFieldValueMethod(); - abstract boolean isDirectName(String name, boolean tokenValueRead); abstract Expression tryReadNextFieldNameColon(JsonFieldInfo property, boolean tokenValueRead); @@ -119,7 +117,6 @@ String genReaderCode( JsonGeneratedCodecBuilder builder, Class type, JsonFieldInfo[] properties, - boolean record, JsonCreatorInfo creatorInfo) { ownerType = type; if (creatorInfo != null) { @@ -131,12 +128,9 @@ String genReaderCode( CodegenContext ctx = builder.context(); ctx.addImports(ObjectCodec.class, readerType, JsonFieldTable.class); ctx.implementsInterfaces(JsonCodegen.generatedCodecType(ctx, readerCapabilityType())); - // Generated readers retain immutable lookup metadata directly. Only record readers keep the - // ObjectCodec because record allocation and construction remain owned by it. + // Generated mutable readers retain immutable lookup metadata directly. Creator-backed readers + // are selected above and consume the exact executable owned by JsonCreatorInfo. ctx.addField(JsonFieldTable.class, "readTable"); - if (record) { - ctx.addField(ObjectCodec.class, "owner"); - } ctx.addField(long[].class, "fieldHashes"); for (int i = 0; i < properties.length; i++) { if (usesReadInfo(properties[i])) { @@ -151,7 +145,7 @@ String genReaderCode( } addGeneratedConstructor( ctx, - readerConstructorExpression(type, properties, record), + readerConstructorExpression(type, properties), ObjectCodec.class, "owner", JsonFieldInfo[].class, @@ -160,7 +154,7 @@ String genReaderCode( "codecs"); ctx.clearExprState(); Code.ExprCode body = - fastReadExpression(builder, readMethod, slowMethod, type, properties, record).genCode(ctx); + fastReadExpression(builder, readMethod, slowMethod, type, properties).genCode(ctx); String bodyCode = body.code(); bodyCode = bodyCode == null ? "" : ctx.optimizeMethodCode(bodyCode); ctx.addMethod( @@ -170,10 +164,9 @@ String genReaderCode( Object.class, readerType, "reader"); - addFastReadGroupMethods( - ctx, builder, readMethod, slowMethod, readerType, type, properties, record); - addReadFieldMethods(ctx, builder, readMethod, readerType, type, properties, record); - addSlowReadMethods(ctx, builder, slowMethod, readerType, type, properties, record); + addFastReadGroupMethods(ctx, builder, readMethod, slowMethod, readerType, type, properties); + addReadFieldMethods(ctx, builder, readMethod, readerType, type, properties); + addSlowReadMethods(ctx, builder, slowMethod, readerType, type, properties); return ctx.genCode(); } @@ -181,7 +174,6 @@ String genAnyReaderCode( JsonGeneratedCodecBuilder builder, Class type, JsonFieldInfo[] properties, - boolean record, JsonCreatorInfo creatorInfo, AnyInfo any) { this.any = any; @@ -215,7 +207,7 @@ String genAnyReaderCode( if (storesAnyReader) { addGeneratedConstructor( ctx, - readerConstructorExpression(type, properties, record), + readerConstructorExpression(type, properties), ObjectCodec.class, "owner", JsonFieldTable.class, @@ -229,7 +221,7 @@ String genAnyReaderCode( } else { addGeneratedConstructor( ctx, - readerConstructorExpression(type, properties, record), + readerConstructorExpression(type, properties), ObjectCodec.class, "owner", JsonFieldTable.class, @@ -243,7 +235,7 @@ String genAnyReaderCode( ctx, "private final", anyReadMethod, - fastReadExpression(builder, readMethod, slowMethod, type, properties, record), + fastReadExpression(builder, readMethod, slowMethod, type, properties), Object.class, concreteReaderType, "reader"); @@ -258,9 +250,9 @@ String genAnyReaderCode( concreteReaderType, "reader"); addFastReadGroupMethods( - ctx, builder, readMethod, slowMethod, concreteReaderType, type, properties, record); - addReadFieldMethods(ctx, builder, readMethod, concreteReaderType, type, properties, record); - addSlowReadMethods(ctx, builder, slowMethod, concreteReaderType, type, properties, record); + ctx, builder, readMethod, slowMethod, concreteReaderType, type, properties); + addReadFieldMethods(ctx, builder, readMethod, concreteReaderType, type, properties); + addSlowReadMethods(ctx, builder, slowMethod, concreteReaderType, type, properties); return ctx.genCode(); } @@ -353,7 +345,7 @@ private String genCreatorReaderCode( "properties", JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), "codecs"); - addCreatorMethod(ctx, type, creatorInfo.executable(), fields); + addCreatorMethod(ctx, type, creatorInfo.executable()); ctx.clearExprState(); Code.ExprCode body = creatorReadExpression(type, creatorInfo).genCode(ctx); String bodyCode = body.code(); @@ -422,7 +414,7 @@ private String genAnyCreatorReaderCode( JsonCodegen.generatedCodecArrayType(ctx, readerArrayType()), "codecs"); } - addAnyCreatorMethod(ctx, type, creatorInfo.executable()); + addCreatorMethod(ctx, type, creatorInfo.executable()); addGeneratedMethod( ctx, "private final", @@ -449,11 +441,7 @@ private Expression anyCreatorReadExpression(Class type, JsonCreatorInfo creat Class[] parameterTypes = creatorInfo.executable().getParameterTypes(); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(new Expression.Invoke(readerRef(), "enterDepth")); - Expression[] arguments = new Expression[parameterTypes.length]; - for (int i = 0; i < parameterTypes.length; i++) { - arguments[i] = new Expression.Variable("a" + i, creatorDefault(parameterTypes[i])); - expressions.add(arguments[i]); - } + Expression[] arguments = creatorArguments(creatorInfo.executable(), expressions); Expression anyMap = new Expression.Variable("anyMap", new Expression.Null(TypeRef.of(Map.class), false)); expressions.add(anyMap); @@ -561,7 +549,7 @@ private Expression readUnknownCreator(Expression fieldStart, Expression hash, Ex eq(match, Expression.Literal.ofInt(JsonFieldTable.UNKNOWN)), read, skip))); } - private void addAnyCreatorMethod(CodegenContext ctx, Class type, Executable executable) { + private void addCreatorMethod(CodegenContext ctx, Class type, Executable executable) { Class[] parameterTypes = executable.getParameterTypes(); Object[] parameters = new Object[parameterTypes.length << 1]; StringBuilder invocation = new StringBuilder(); @@ -630,11 +618,7 @@ private Expression creatorReadExpression(Class type, JsonCreatorInfo creatorI JsonCreatorFieldInfo[] fields = creatorInfo.fields(); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(new Expression.Invoke(readerRef(), "enterDepth")); - Expression[] arguments = new Expression[fields.length]; - for (int i = 0; i < fields.length; i++) { - arguments[i] = new Expression.Variable("a" + i, creatorDefault(fields[i].rawType())); - expressions.add(arguments[i]); - } + Expression[] arguments = creatorArguments(creatorInfo.executable(), expressions); expressions.add(expectExpr('{')); expressions.add( new Expression.If( @@ -664,7 +648,8 @@ private Expression creatorReadExpression(Class type, JsonCreatorInfo creatorI new Expression.Switch.Case( i, new Expression.ListExpression( - new Expression.Assign(arguments[i], readCreatorValue(fields[i], i)), + new Expression.Assign( + arguments[fields[i].argumentIndex()], readCreatorValue(fields[i], i)), new Expression.Break())); } loop.add( @@ -679,35 +664,17 @@ private Expression creatorReadExpression(Class type, JsonCreatorInfo creatorI return expressions; } - private void addCreatorMethod( - CodegenContext ctx, Class type, Executable executable, JsonCreatorFieldInfo[] fields) { - Object[] parameters = new Object[fields.length << 1]; - StringBuilder invocation = new StringBuilder(); - for (int i = 0; i < fields.length; i++) { - parameters[i << 1] = fields[i].rawType(); - parameters[(i << 1) + 1] = "a" + i; - if (i != 0) { - invocation.append(", "); - } - invocation.append('a').append(i); - } - String typeName = ctx.type(type); - String expression = - executable instanceof Constructor - ? "new " + typeName + "(" + invocation + ")" - : typeName + "." + ((Method) executable).getName() + "(" + invocation + ")"; - StringBuilder body = new StringBuilder(); - body.append(typeName) - .append(" value;\ntry {\n value = ") - .append(expression) - .append(";\n") - .append("} catch (Throwable e) {\n throw owner.creatorFailure(e);\n}\n"); - if (executable instanceof Method) { - body.append("return (").append(typeName).append(") owner.requireCreatorResult(value);"); - } else { - body.append("return value;"); + private Expression[] creatorArguments( + Executable executable, Expression.ListExpression expressions) { + // Creator fields cover only read-enabled JSON members. The executable owns the full argument + // shape, including ignored or getter-only parameters which must still receive typed defaults. + Class[] parameterTypes = executable.getParameterTypes(); + Expression[] arguments = new Expression[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + arguments[i] = new Expression.Variable("a" + i, creatorDefault(parameterTypes[i])); + expressions.add(arguments[i]); } - ctx.addMethod("private final", "createValue", body.toString(), type, parameters); + return arguments; } private Expression createValue(Class type, Expression[] arguments) { @@ -818,12 +785,10 @@ private void addFastReadGroupMethods( String slowMethod, Class readerType, Class type, - JsonFieldInfo[] properties, - boolean record) { + JsonFieldInfo[] properties) { if (!shouldSplitFastRead(properties)) { return; } - Class objectType = record ? Object[].class : type; for (int start = 0; start < properties.length; ) { int end = readGroupEnd(properties, start); if (any == null) { @@ -831,11 +796,11 @@ private void addFastReadGroupMethods( ctx, "final", readGroupMethod(readMethod, start), - fastReadGroupExpression(builder, slowMethod, type, properties, start, end, record), + fastReadGroupExpression(builder, slowMethod, type, properties, start, end), boolean.class, readerType, "reader", - objectType, + type, "object", long[].class, "fieldHashes"); @@ -844,11 +809,11 @@ private void addFastReadGroupMethods( ctx, "final", readGroupMethod(readMethod, start), - fastReadGroupExpression(builder, slowMethod, type, properties, start, end, record), + fastReadGroupExpression(builder, slowMethod, type, properties, start, end), boolean.class, readerType, "reader", - objectType, + type, "object", long[].class, "fieldHashes", @@ -865,12 +830,10 @@ private void addReadFieldMethods( String readMethod, Class readerType, Class type, - JsonFieldInfo[] properties, - boolean record) { + JsonFieldInfo[] properties) { if (!shouldSplitFieldSwitch(properties)) { return; } - Class objectType = record ? Object[].class : type; for (int start = 0; start < properties.length; start += READ_FIELD_SWITCH_SIZE) { int end = Math.min(start + READ_FIELD_SWITCH_SIZE, properties.length); addGeneratedMethod( @@ -883,13 +846,12 @@ private void addReadFieldMethods( properties, start, end, - objectParam(type, record), - new Reference("fieldIndex", TypeRef.of(int.class)), - record), + objectParam(type), + new Reference("fieldIndex", TypeRef.of(int.class))), void.class, readerType, "reader", - objectType, + type, "object", int.class, "fieldIndex"); @@ -902,9 +864,7 @@ private void addSlowReadMethods( String methodName, Class readerType, Class type, - JsonFieldInfo[] properties, - boolean record) { - Class objectType = record ? Object[].class : type; + JsonFieldInfo[] properties) { // An Any slow invocation consumes the entire remaining object and finishes its map before // returning. This makes the Map parameter its local accumulator without a holder or return // carrier; callers must not resume member consumption after the slow call. @@ -913,11 +873,11 @@ private void addSlowReadMethods( ctx, "final", methodName, - slowReadExpression(builder, type, properties, record), + slowReadExpression(builder, type, properties), void.class, readerType, "reader", - objectType, + type, "object", int.class, "expectedIndex"); @@ -925,11 +885,11 @@ private void addSlowReadMethods( ctx, "final", methodName, - slowReadFromFirstExpression(builder, type, properties, record), + slowReadFromFirstExpression(builder, type, properties), void.class, readerType, "reader", - objectType, + type, "object", int.class, "expectedIndex", @@ -941,11 +901,11 @@ private void addSlowReadMethods( ctx, "final", methodName, - slowReadExpression(builder, type, properties, record), + slowReadExpression(builder, type, properties), void.class, readerType, "reader", - objectType, + type, "object", int.class, "expectedIndex", @@ -955,11 +915,11 @@ private void addSlowReadMethods( ctx, "final", methodName, - slowReadFromFirstExpression(builder, type, properties, record), + slowReadFromFirstExpression(builder, type, properties), void.class, readerType, "reader", - objectType, + type, "object", int.class, "expectedIndex", @@ -996,8 +956,7 @@ private void addGeneratedConstructor( ctx.addConstructor(code, params); } - private Expression readerConstructorExpression( - Class type, JsonFieldInfo[] properties, boolean record) { + private Expression readerConstructorExpression(Class type, JsonFieldInfo[] properties) { Expression.ListExpression expressions = new Expression.ListExpression(); Reference propertiesRef = new Reference("properties", TypeRef.of(JsonFieldInfo[].class)); Reference codecsRef = new Reference("codecs", TypeRef.of(readerArrayType())); @@ -1009,7 +968,7 @@ private Expression readerConstructorExpression( expressions.add( new Expression.Assign( new Reference("this.readTable", TypeRef.of(JsonFieldTable.class)), table)); - if (record || any != null) { + if (any != null) { expressions.add( new Expression.Assign(new Reference("this.owner", TypeRef.of(ObjectCodec.class)), owner)); } @@ -1065,24 +1024,23 @@ private Expression fastReadExpression( String readMethod, String slowMethod, Class type, - JsonFieldInfo[] properties, - boolean record) { + JsonFieldInfo[] properties) { if (shouldSplitFastRead(properties)) { - return splitFastReadExpression(builder, readMethod, slowMethod, type, properties, record); + return splitFastReadExpression(builder, readMethod, slowMethod, type, properties); } - Expression object = objectExpression(builder, record); + Expression object = objectExpression(builder); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(new Expression.Invoke(readerRef(), "enterDepth")); expressions.add(object); - Expression anyMap = anyMap(builder, object, record); + Expression anyMap = anyMap(builder, object); if (anyMap != null) { expressions.add(anyMap); } expressions.add(expectExpr('{')); - expressions.add(new Expression.If(consumeExpr('}'), returnObject(object, record))); + expressions.add(new Expression.If(consumeExpr('}'), returnObject(object))); if (properties.length == 0) { expressions.add(slowCall(slowMethod, object, Expression.Literal.ofInt(0))); - expressions.add(returnObject(object, record)); + expressions.add(returnObject(object)); return expressions; } Expression hashes = fieldRef("fieldHashes", long[].class); @@ -1097,10 +1055,10 @@ private Expression fastReadExpression( } for (int i = 0; i < properties.length; i++) { Expression read = - fastReadField(builder, slowMethod, type, properties, i, object, hashes, skips, record); + fastReadField(builder, slowMethod, type, properties, i, object, hashes, skips); expressions.add(i == 0 ? read : new Expression.If(not(skips[i]), read)); } - expressions.add(returnObject(object, record)); + expressions.add(returnObject(object)); return expressions; } @@ -1109,28 +1067,27 @@ private Expression splitFastReadExpression( String readMethod, String slowMethod, Class type, - JsonFieldInfo[] properties, - boolean record) { - Expression object = objectExpression(builder, record); + JsonFieldInfo[] properties) { + Expression object = objectExpression(builder); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(new Expression.Invoke(readerRef(), "enterDepth")); expressions.add(object); - Expression anyMap = anyMap(builder, object, record); + Expression anyMap = anyMap(builder, object); if (anyMap != null) { expressions.add(anyMap); } expressions.add(expectExpr('{')); - expressions.add(new Expression.If(consumeExpr('}'), returnObject(object, record))); + expressions.add(new Expression.If(consumeExpr('}'), returnObject(object))); Expression hashes = new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); expressions.add(hashes); for (int start = 0; start < properties.length; ) { int end = readGroupEnd(properties, start); Expression groupCall = inline(readGroupCall(readMethod, start, object, hashes)); - expressions.add(new Expression.If(not(groupCall), returnObject(object, record))); + expressions.add(new Expression.If(not(groupCall), returnObject(object))); start = end; } - expressions.add(returnObject(object, record)); + expressions.add(returnObject(object)); return expressions; } @@ -1140,9 +1097,8 @@ private Expression fastReadGroupExpression( Class type, JsonFieldInfo[] properties, int start, - int end, - boolean record) { - Expression object = objectParam(type, record); + int end) { + Expression object = objectParam(type); Expression hashes = new Reference("fieldHashes", TypeRef.of(long[].class)); Expression[] skips = new Expression[properties.length]; Expression.ListExpression expressions = new Expression.ListExpression(); @@ -1152,8 +1108,7 @@ private Expression fastReadGroupExpression( } for (int i = start; i < end; i++) { Expression read = - fastReadField( - builder, slowMethod, type, properties, i, end, true, object, hashes, skips, record); + fastReadField(builder, slowMethod, type, properties, i, end, true, object, hashes, skips); expressions.add(i == start ? read : new Expression.If(not(skips[i]), read)); } if (end < properties.length) { @@ -1191,15 +1146,13 @@ private Expression readGroupCall( anyMapRef()); } - private Expression anyMap(JsonGeneratedCodecBuilder builder, Expression object, boolean record) { + private Expression anyMap(JsonGeneratedCodecBuilder builder, Expression object) { if (any == null || any.readField() == null) { return null; } Expression initial = - record - ? new Expression.Null(TypeRef.of(Map.class), false) - : new Expression.Cast( - inline(builder.anyValue(any.readField(), object)), TypeRef.of(Map.class)); + new Expression.Cast( + inline(builder.anyValue(any.readField(), object)), TypeRef.of(Map.class)); return new Expression.Variable("anyMap", initial); } @@ -1211,8 +1164,7 @@ private Expression fastReadField( int index, Expression object, Expression hashes, - Expression[] skips, - boolean record) { + Expression[] skips) { return fastReadField( builder, slowMethod, @@ -1223,8 +1175,7 @@ private Expression fastReadField( false, object, hashes, - skips, - record); + skips); } private Expression fastReadField( @@ -1237,15 +1188,13 @@ private Expression fastReadField( boolean groupHelper, Expression object, Expression hashes, - Expression[] skips, - boolean record) { + Expression[] skips) { if (isDirectName(properties[index].name(), true)) { return statementIf( tryReadNextFieldNameColon(properties[index], true), new Expression.ListExpression( - readField(builder, type, properties[index], index, object, record, true), - fieldEnd( - slowMethod, properties.length, groupEnd, groupHelper, index, object, record)), + readField(builder, type, properties[index], index, object, true), + fieldEnd(slowMethod, properties.length, groupEnd, groupHelper, index, object)), nextDirectFallback( builder, slowMethod, @@ -1256,22 +1205,11 @@ private Expression fastReadField( groupHelper, object, hashes, - skips, - record), + skips), groupHelper); } return nextDirectFallback( - builder, - slowMethod, - type, - properties, - index, - groupEnd, - groupHelper, - object, - hashes, - skips, - record); + builder, slowMethod, type, properties, index, groupEnd, groupHelper, object, hashes, skips); } private Expression nextDirectFallback( @@ -1284,17 +1222,15 @@ private Expression nextDirectFallback( boolean groupHelper, Expression object, Expression hashes, - Expression[] skips, - boolean record) { + Expression[] skips) { int nextIndex = index + 1; if (nextIndex < groupEnd && isDirectName(properties[nextIndex].name(), true)) { return statementIf( tryReadNextFieldNameColon(properties[nextIndex], true), new Expression.ListExpression( - readField(builder, type, properties[nextIndex], nextIndex, object, record, true), + readField(builder, type, properties[nextIndex], nextIndex, object, true), new Expression.Assign(skips[nextIndex], Expression.Literal.True), - fieldEnd( - slowMethod, properties.length, groupEnd, groupHelper, nextIndex, object, record)), + fieldEnd(slowMethod, properties.length, groupEnd, groupHelper, nextIndex, object)), hashFallback( builder, slowMethod, @@ -1305,22 +1241,11 @@ private Expression nextDirectFallback( groupHelper, object, hashes, - skips, - record), + skips), groupHelper); } return hashFallback( - builder, - slowMethod, - type, - properties, - index, - groupEnd, - groupHelper, - object, - hashes, - skips, - record); + builder, slowMethod, type, properties, index, groupEnd, groupHelper, object, hashes, skips); } private Expression hashFallback( @@ -1333,8 +1258,7 @@ private Expression hashFallback( boolean groupHelper, Expression object, Expression hashes, - Expression[] skips, - boolean record) { + Expression[] skips) { Expression fieldStart = any == null ? null @@ -1360,8 +1284,7 @@ private Expression hashFallback( hashes, skips, fieldHash, - fieldStart, - record)); + fieldStart)); return expressions; } @@ -1377,8 +1300,7 @@ private Expression fastReadFieldFromHash( Expression hashes, Expression[] skips, Expression fieldHash, - Expression fieldStart, - boolean record) { + Expression fieldStart) { Expression fallback; if (index + 1 < groupEnd) { fallback = @@ -1386,16 +1308,10 @@ private Expression fastReadFieldFromHash( eq(fieldHash, arrayValue(hashes, index + 1)), new Expression.ListExpression( expectExpr(':'), - readField(builder, type, properties[index + 1], index + 1, object, record, false), + readField(builder, type, properties[index + 1], index + 1, object, false), new Expression.Assign(skips[index + 1], Expression.Literal.True), fieldEnd( - slowMethod, - properties.length, - groupEnd, - groupHelper, - index + 1, - object, - record)), + slowMethod, properties.length, groupEnd, groupHelper, index + 1, object)), slowConsumedReturn( slowMethod, index, @@ -1403,7 +1319,6 @@ private Expression fastReadFieldFromHash( fieldHash, fieldStart, object, - record, groupHelper), groupHelper); } else { @@ -1415,7 +1330,6 @@ private Expression fastReadFieldFromHash( fieldHash, fieldStart, object, - record, groupHelper); } return statementIf( @@ -1423,8 +1337,8 @@ private Expression fastReadFieldFromHash( fallback, new Expression.ListExpression( expectExpr(':'), - readField(builder, type, properties[index], index, object, record, false), - fieldEnd(slowMethod, properties.length, groupEnd, groupHelper, index, object, record)), + readField(builder, type, properties[index], index, object, false), + fieldEnd(slowMethod, properties.length, groupEnd, groupHelper, index, object)), groupHelper); } @@ -1506,27 +1420,12 @@ final boolean shouldSplitFieldSwitch(JsonFieldInfo[] properties) { return properties.length > READ_FIELD_SWITCH_SIZE; } - private Expression objectExpression(JsonGeneratedCodecBuilder builder, boolean record) { - if (record) { - return new Expression.Variable( - "object", - inline( - new Expression.Invoke( - ownerRef(), "newRecordFieldValues", TypeRef.of(Object[].class), false))); - } + private Expression objectExpression(JsonGeneratedCodecBuilder builder) { return builder.newObject(); } - private Expression returnObject(Expression object, boolean record) { + private Expression returnObject(Expression object) { Expression exitDepth = new Expression.Invoke(readerRef(), "exitDepth"); - if (record) { - return new Expression.ListExpression( - exitDepth, - new Expression.Return( - inline( - new Expression.Invoke( - ownerRef(), "newRecord", TypeRef.of(Object.class), object)))); - } return new Expression.ListExpression(exitDepth, new Expression.Return(object)); } @@ -1547,10 +1446,10 @@ final Expression statementIf( } private Expression slowConsumedReturn( - String slowMethod, int index, Expression firstFieldIndex, Expression object, boolean record) { + String slowMethod, int index, Expression firstFieldIndex, Expression object) { return new Expression.ListExpression( slowCall(slowMethod, object, Expression.Literal.ofInt(index), firstFieldIndex), - returnObject(object, record)); + returnObject(object)); } private Expression slowConsumedReturn( @@ -1558,10 +1457,9 @@ private Expression slowConsumedReturn( int index, Expression firstFieldIndex, Expression object, - boolean record, boolean groupHelper) { if (!groupHelper) { - return slowConsumedReturn(slowMethod, index, firstFieldIndex, object, record); + return slowConsumedReturn(slowMethod, index, firstFieldIndex, object); } return new Expression.ListExpression( slowCall(slowMethod, object, Expression.Literal.ofInt(index), firstFieldIndex), @@ -1575,10 +1473,9 @@ private Expression slowConsumedReturn( Expression firstFieldHash, Expression firstFieldStart, Expression object, - boolean record, boolean groupHelper) { if (any == null) { - return slowConsumedReturn(slowMethod, index, firstFieldIndex, object, record, groupHelper); + return slowConsumedReturn(slowMethod, index, firstFieldIndex, object, groupHelper); } if (!groupHelper) { return new Expression.ListExpression( @@ -1589,7 +1486,7 @@ private Expression slowConsumedReturn( firstFieldIndex, firstFieldHash, firstFieldStart), - returnObject(object, record)); + returnObject(object)); } return new Expression.ListExpression( slowCall( @@ -1608,18 +1505,16 @@ private Expression fieldEnd( int groupEnd, boolean groupHelper, int index, - Expression object, - boolean record) { + Expression object) { if (!groupHelper) { - return fieldEnd(slowMethod, propertyCount, index, object, record); + return fieldEnd(slowMethod, propertyCount, index, object); } return fastReadGroupEnd(slowMethod, propertyCount, index, object); } - private Expression fieldEnd( - String slowMethod, int propertyCount, int index, Expression object, boolean record) { + private Expression fieldEnd(String slowMethod, int propertyCount, int index, Expression object) { if (index + 1 < propertyCount) { - return new Expression.If(not(consumeCommaOrEndObjectExpr()), returnObject(object, record)); + return new Expression.If(not(consumeCommaOrEndObjectExpr()), returnObject(object)); } return new Expression.If( consumeCommaOrEndObjectExpr(), @@ -1638,11 +1533,8 @@ private Expression fastReadGroupEnd( } private Expression slowReadExpression( - JsonGeneratedCodecBuilder builder, - Class type, - JsonFieldInfo[] properties, - boolean record) { - Expression object = objectParam(type, record); + JsonGeneratedCodecBuilder builder, Class type, JsonFieldInfo[] properties) { + Expression object = objectParam(type); Expression hashes = new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); Reference expectedIndex = new Reference("expectedIndex", TypeRef.of(int.class)); @@ -1652,10 +1544,10 @@ private Expression slowReadExpression( Expression.ListExpression loop = new Expression.ListExpression(); loop.add( readNextHashedField( - builder, type, properties, object, hashes, expectedIndex, anyMapCreated, record)); + builder, type, properties, object, hashes, expectedIndex, anyMapCreated)); loop.add(new Expression.If(not(consumeCommaOrEndObjectExpr()), new Expression.Break())); expressions.add(new Expression.While(Expression.Literal.True, loop)); - finishAnyRead(builder, expressions, object, anyMapCreated, record); + finishAnyRead(builder, expressions, object, anyMapCreated); return expressions; } @@ -1672,19 +1564,15 @@ private void finishAnyRead( JsonGeneratedCodecBuilder builder, Expression.ListExpression expressions, Expression object, - Expression anyMapCreated, - boolean record) { - Expression finish = finishAnyReadExpression(builder, object, anyMapCreated, record); + Expression anyMapCreated) { + Expression finish = finishAnyReadExpression(builder, object, anyMapCreated); if (finish != null) { expressions.add(finish); } } private Expression finishAnyReadExpression( - JsonGeneratedCodecBuilder builder, - Expression object, - Expression anyMapCreated, - boolean record) { + JsonGeneratedCodecBuilder builder, Expression object, Expression anyMapCreated) { if (any == null || any.readField() == null) { return null; } @@ -1696,11 +1584,7 @@ private Expression finishAnyReadExpression( new Expression.Invoke(ownerRef(), "finishAnyMap", TypeRef.of(Map.class), map), TypeRef.of(Map.class))); Expression store; - if (record) { - store = - new Expression.AssignArrayElem( - object, finished, Expression.Literal.ofInt(any.constructionIndex())); - } else if (Modifier.isFinal(any.readField().getModifiers())) { + if (Modifier.isFinal(any.readField().getModifiers())) { store = new Expression.Empty(); } else { store = builder.setAnyField(any.readField(), object, finished); @@ -1709,22 +1593,16 @@ private Expression finishAnyReadExpression( } private Expression finishAnyReadAndReturn( - JsonGeneratedCodecBuilder builder, - Expression object, - Expression anyMapCreated, - boolean record) { - Expression finish = finishAnyReadExpression(builder, object, anyMapCreated, record); + JsonGeneratedCodecBuilder builder, Expression object, Expression anyMapCreated) { + Expression finish = finishAnyReadExpression(builder, object, anyMapCreated); return finish == null ? new Expression.Return() : new Expression.ListExpression(finish, new Expression.Return()); } private Expression slowReadFromFirstExpression( - JsonGeneratedCodecBuilder builder, - Class type, - JsonFieldInfo[] properties, - boolean record) { - Expression object = objectParam(type, record); + JsonGeneratedCodecBuilder builder, Class type, JsonFieldInfo[] properties) { + Expression object = objectParam(type); Expression hashes = new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); Reference expectedIndex = new Reference("expectedIndex", TypeRef.of(int.class)); @@ -1746,15 +1624,14 @@ private Expression slowReadFromFirstExpression( fieldIndex, new Reference("firstFieldHash", TypeRef.of(long.class)), any == null ? null : new Reference("firstFieldStart", TypeRef.of(int.class)), - anyMapCreated, - record)); + anyMapCreated)); loop.add(updateExpectedIndex(expectedIndex, fieldIndex)); loop.add( new Expression.If( not(consumeCommaOrEndObjectExpr()), any == null ? new Expression.Return() - : finishAnyReadAndReturn(builder, object, anyMapCreated, record))); + : finishAnyReadAndReturn(builder, object, anyMapCreated))); Expression fieldStart = any == null ? null @@ -1786,8 +1663,7 @@ private Expression readNextHashedField( Expression object, Expression hashes, Expression expectedIndex, - Expression anyMapCreated, - boolean record) { + Expression anyMapCreated) { Expression fieldStart = any == null ? null @@ -1806,15 +1682,7 @@ private Expression readNextHashedField( fieldIndex, expectExpr(':'), fieldSwitch( - builder, - type, - properties, - object, - fieldIndex, - fieldHash, - fieldStart, - anyMapCreated, - record), + builder, type, properties, object, fieldIndex, fieldHash, fieldStart, anyMapCreated), updateExpectedIndex(expectedIndex, fieldIndex)); return expressions; } @@ -1824,9 +1692,8 @@ private Expression fieldSwitch( Class type, JsonFieldInfo[] properties, Expression object, - Expression fieldIndex, - boolean record) { - return fieldSwitch(builder, type, properties, object, fieldIndex, null, null, null, record); + Expression fieldIndex) { + return fieldSwitch(builder, type, properties, object, fieldIndex, null, null, null); } private Expression fieldSwitch( @@ -1837,8 +1704,7 @@ private Expression fieldSwitch( Expression fieldIndex, Expression fieldHash, Expression fieldStart, - Expression anyMapCreated, - boolean record) { + Expression anyMapCreated) { if (shouldSplitFieldSwitch(properties)) { int chunks = (properties.length + READ_FIELD_SWITCH_SIZE - 1) / READ_FIELD_SWITCH_SIZE; Expression.Switch.Case[] cases = new Expression.Switch.Case[chunks]; @@ -1869,7 +1735,7 @@ private Expression fieldSwitch( : new Expression.If( ge(fieldIndex, Expression.Literal.ofInt(0)), known, - readUnknown(object, fieldIndex, fieldHash, fieldStart, anyMapCreated, record)); + readUnknown(object, fieldIndex, fieldHash, fieldStart, anyMapCreated)); } return fieldSwitchRange( builder, @@ -1881,8 +1747,7 @@ private Expression fieldSwitch( fieldIndex, fieldHash, fieldStart, - anyMapCreated, - record); + anyMapCreated); } private Expression fieldSwitchRange( @@ -1892,8 +1757,7 @@ private Expression fieldSwitchRange( int start, int end, Expression object, - Expression fieldIndex, - boolean record) { + Expression fieldIndex) { return fieldSwitchRange( builder, type, @@ -1902,8 +1766,7 @@ private Expression fieldSwitchRange( end, object, fieldIndex, - new Expression.Invoke(readerRef(), "skipValue"), - record); + new Expression.Invoke(readerRef(), "skipValue")); } private Expression fieldSwitchRange( @@ -1916,14 +1779,12 @@ private Expression fieldSwitchRange( Expression fieldIndex, Expression fieldHash, Expression fieldStart, - Expression anyMapCreated, - boolean record) { + Expression anyMapCreated) { Expression unknown = any == null ? new Expression.Invoke(readerRef(), "skipValue") - : readUnknown(object, fieldIndex, fieldHash, fieldStart, anyMapCreated, record); - return fieldSwitchRange( - builder, type, properties, start, end, object, fieldIndex, unknown, record); + : readUnknown(object, fieldIndex, fieldHash, fieldStart, anyMapCreated); + return fieldSwitchRange(builder, type, properties, start, end, object, fieldIndex, unknown); } private Expression fieldSwitchRange( @@ -1934,15 +1795,14 @@ private Expression fieldSwitchRange( int end, Expression object, Expression fieldIndex, - Expression unknown, - boolean record) { + Expression unknown) { Expression.Switch.Case[] cases = new Expression.Switch.Case[end - start]; for (int i = start; i < end; i++) { cases[i - start] = new Expression.Switch.Case( i, new Expression.ListExpression( - readField(builder, type, properties[i], i, object, record, false), + readField(builder, type, properties[i], i, object, false), new Expression.Break())); } return new Expression.Switch(fieldIndex, cases, unknown); @@ -1953,8 +1813,7 @@ private Expression readUnknown( Expression fieldIndex, Expression fieldHash, Expression fieldStart, - Expression anyMapCreated, - boolean record) { + Expression anyMapCreated) { Expression skip = new Expression.Invoke(readerRef(), "skipValue"); Expression reserved = eq(fieldIndex, Expression.Literal.ofInt(JsonFieldTable.SKIP)); Expression name = @@ -1984,29 +1843,21 @@ private Expression readUnknown( } else { Reference map = new Reference("anyMap", TypeRef.of(Map.class)); Expression create = - record - ? new Expression.ListExpression( + Modifier.isFinal(any.readField().getModifiers()) + ? new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + "requireAnyMap", + "", + TypeRef.of(Map.class), + false, + false, + map) + : new Expression.ListExpression( new Expression.Assign( map, new Expression.Invoke(ownerRef(), "newAnyMap", TypeRef.of(Map.class), false) .inline()), - new Expression.Assign(anyMapCreated, Expression.Literal.True)) - : Modifier.isFinal(any.readField().getModifiers()) - ? new Expression.Invoke( - new Reference("this", TypeRef.of(Object.class)), - "requireAnyMap", - "", - TypeRef.of(Map.class), - false, - false, - map) - : new Expression.ListExpression( - new Expression.Assign( - map, - new Expression.Invoke( - ownerRef(), "newAnyMap", TypeRef.of(Map.class), false) - .inline()), - new Expression.Assign(anyMapCreated, Expression.Literal.True)); + new Expression.Assign(anyMapCreated, Expression.Literal.True)); Expression put = new Expression.Invoke(ownerRef(), "putAnyMap", map, name, value); write = new Expression.ListExpression( @@ -2041,10 +1892,8 @@ private Expression fieldIndexValue( TypeRef.of(int.class)); } - final Expression objectParam(Class type, boolean record) { - return record - ? new Reference("object", TypeRef.of(Object[].class)) - : new Reference("object", TypeRef.of(type)); + final Expression objectParam(Class type) { + return new Reference("object", TypeRef.of(type)); } final Reference ownerRef() { @@ -2504,11 +2353,7 @@ private Expression readField( JsonFieldInfo property, int id, Expression object, - boolean record, boolean tokenValueRead) { - if (record) { - return readRecordField(type, property, id, object, tokenValueRead); - } Class rawType = property.readRawType(); switch (property.readKind()) { case BOOLEAN: @@ -2538,122 +2383,6 @@ private Expression readField( } } - private Expression readRecordField( - Class type, JsonFieldInfo property, int id, Expression object, boolean tokenValueRead) { - Class rawType = property.readRawType(); - switch (property.readKind()) { - case BOOLEAN: - return readRecordBoolean(rawType, id, object, tokenValueRead); - case INT: - return readRecordInt(rawType, id, object, tokenValueRead); - case LONG: - return readRecordLong(rawType, id, object, tokenValueRead); - case FLOAT: - return readRecordFloat(rawType, id, object); - case DOUBLE: - return readRecordDouble(rawType, id, object); - case STRING: - return assignRecord(object, id, readStringExpr(tokenValueRead)); - case ENUM: - return readRecordEnum(id, object, tokenValueRead); - case COLLECTION: - return readRecordCollection(property, id, object); - case ARRAY: - case MAP: - return assignRecord(object, id, readResolvedValue(property, id)); - case OBJECT: - return readRecordObject(type, property, id, object); - default: - return assignRecord( - object, - id, - new Expression.Invoke( - fieldRef("rp" + id, JsonFieldInfo.class), - readFieldValueMethod(), - TypeRef.of(Object.class), - true, - readerRef())); - } - } - - final Expression readRecordBoolean( - Class rawType, int id, Expression object, boolean tokenValueRead) { - Expression value = box(Boolean.class, readBooleanExpr(tokenValueRead)); - if (rawType.isPrimitive()) { - return assignRecord(object, id, value); - } - return new Expression.If( - tryReadNullExpr(), - assignRecord(object, id, new Expression.Null(TypeRef.of(Boolean.class), false)), - assignRecord(object, id, value)); - } - - final Expression readRecordInt( - Class rawType, int id, Expression object, boolean tokenValueRead) { - Expression value = box(Integer.class, readIntExpr(tokenValueRead)); - if (rawType.isPrimitive()) { - return assignRecord(object, id, value); - } - return new Expression.If( - tryReadNullExpr(), - assignRecord(object, id, new Expression.Null(TypeRef.of(Integer.class), false)), - assignRecord(object, id, value)); - } - - final Expression readRecordLong( - Class rawType, int id, Expression object, boolean tokenValueRead) { - Expression value = box(Long.class, readLongExpr(tokenValueRead)); - if (rawType.isPrimitive()) { - return assignRecord(object, id, value); - } - return new Expression.If( - tryReadNullExpr(), - assignRecord(object, id, new Expression.Null(TypeRef.of(Long.class), false)), - assignRecord(object, id, value)); - } - - final Expression readRecordFloat(Class rawType, int id, Expression object) { - Expression value = box(Float.class, readFloatExpr()); - if (rawType.isPrimitive()) { - return assignRecord(object, id, value); - } - return new Expression.If( - tryReadNullExpr(), - assignRecord(object, id, new Expression.Null(TypeRef.of(Float.class), false)), - assignRecord(object, id, value)); - } - - final Expression readRecordDouble(Class rawType, int id, Expression object) { - Expression value = box(Double.class, readDoubleExpr()); - if (rawType.isPrimitive()) { - return assignRecord(object, id, value); - } - return new Expression.If( - tryReadNullExpr(), - assignRecord(object, id, new Expression.Null(TypeRef.of(Double.class), false)), - assignRecord(object, id, value)); - } - - final Expression readRecordEnum(int id, Expression object, boolean tokenValueRead) { - return new Expression.If( - tryReadNullExpr(), - assignRecord(object, id, new Expression.Null(TypeRef.of(Object.class), false)), - assignRecord(object, id, readEnumValue(Object.class, id, tokenValueRead))); - } - - final Expression readRecordObject( - Class type, JsonFieldInfo property, int id, Expression object) { - if (property.readRawType() == Object.class - || !property.readTypeInfo().usesDefaultObjectCodec()) { - return assignRecord(object, id, readResolvedValue(property, id)); - } - return assignRecord(object, id, readObjectValue(type, property, id)); - } - - final Expression readRecordCollection(JsonFieldInfo property, int id, Expression object) { - return assignRecord(object, id, readCollectionValue(property, id)); - } - final Expression readBoolean( JsonGeneratedCodecBuilder builder, JsonFieldInfo property, @@ -2798,10 +2527,6 @@ final Expression readObject( return builder.setField(property, object, readObjectValue(type, property, id)); } - final Expression assignRecord(Expression object, int id, Expression value) { - return new Expression.AssignArrayElem(object, inline(value), Expression.Literal.ofInt(id)); - } - final Expression box(Class boxedType, Expression value) { return new Expression.StaticInvoke( boxedType, "valueOf", "", TypeRef.of(boxedType), false, true, false, value); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java index a32eecc550..10d0346e6b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Latin1ReaderCodegen.java @@ -74,11 +74,6 @@ String readFieldMethod() { return "readLatin1"; } - @Override - String readFieldValueMethod() { - return "readLatin1Value"; - } - @Override boolean isDirectName(String name, boolean tokenValueRead) { return JsonAsciiToken.isLongPackable(fieldNameToken(name)); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java index 1bb68b7a77..b0588bf4a5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf16ReaderCodegen.java @@ -71,11 +71,6 @@ String readFieldMethod() { return "readUtf16"; } - @Override - String readFieldValueMethod() { - return "readUtf16Value"; - } - @Override boolean isDirectName(String name, boolean tokenValueRead) { return tokenValueRead ? isUtf16FieldNameToken(name) : isPackedName(name); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java index 072afb8ee8..ce01fa5810 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8ReaderCodegen.java @@ -74,11 +74,6 @@ String readFieldMethod() { return "readUtf8"; } - @Override - String readFieldValueMethod() { - return "readUtf8Value"; - } - @Override boolean isDirectName(String name, boolean tokenValueRead) { return JsonAsciiToken.isLongPackable(fieldNameToken(name)); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAnySetterAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAnySetterAccessor.java new file mode 100644 index 0000000000..93ce74e53b --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAnySetterAccessor.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.json.meta; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.apache.fory.annotation.Internal; +import org.apache.fory.json.ForyJsonException; + +/** Generated direct invocation for a two-argument {@code JsonAnySetter} method. */ +@Internal +public abstract class JsonAnySetterAccessor { + protected JsonAnySetterAccessor() {} + + /** Returns the exact setter method represented by this operation. */ + public abstract Method setter(); + + /** Invokes the setter with one dynamic JSON property. */ + public abstract void put(Object target, String name, Object value); + + /** Preserves ordinary any-setter failure semantics for generated direct calls. */ + protected static ForyJsonException accessException(Method method, Throwable throwable) { + Throwable cause = + throwable instanceof InvocationTargetException + ? ((InvocationTargetException) throwable).getCause() + : throwable; + if (cause instanceof Error) { + throw (Error) cause; + } + return new ForyJsonException("Cannot invoke @JsonAnySetter " + method, cause); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java index 926782db57..7e2a5cb110 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java @@ -28,15 +28,19 @@ import java.util.Arrays; import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.internal._JDKAccess; /** - * Immutable construction metadata for one property-based {@code JsonCreator}. + * Immutable ordered construction metadata for one JSON object codec. * - *

The interpreted path allocates exactly one fixed-size argument array per object. Generated - * readers consume the same field metadata but invoke the executable directly with typed locals. + *

Record canonical constructors, property-based {@code JsonCreator} constructors, and + * property-based {@code JsonCreator} factories share this owner. The separate complete-string + * {@code JsonValue} representation owns its value creator in its value codec. The interpreted path + * allocates exactly one fixed-size argument array per object. Generated JIT readers consume the + * same field metadata and executable but invoke it directly with typed locals. */ @Internal public final class JsonCreatorInfo { @@ -46,14 +50,23 @@ public final class JsonCreatorInfo { private final Object[] defaults; private final long[] hashes; private final MethodHandle invoker; + private final GeneratedJsonCodec generatedCodec; public JsonCreatorInfo( - Class ownerType, Executable executable, JsonCreatorFieldInfo[] fields, Object[] defaults) { + Class ownerType, + Executable executable, + JsonCreatorFieldInfo[] fields, + Object[] defaults, + GeneratedJsonCodec generatedCodec) { this.ownerType = ownerType; this.executable = executable; this.fields = fields; this.defaults = defaults; - invoker = buildInvoker(ownerType, executable, executable.getParameterCount()); + this.generatedCodec = generatedCodec; + invoker = + generatedCodec == null + ? buildInvoker(ownerType, executable, executable.getParameterCount()) + : null; hashes = new long[fields.length]; for (int i = 0; i < fields.length; i++) { hashes[i] = fields[i].nameHash(); @@ -90,6 +103,16 @@ public void resolveTypes(JsonTypeResolver resolver) { } public Object create(Object[] arguments) { + if (generatedCodec != null) { + try { + return requireResult(generatedCodec.newInstance(arguments)); + } catch (Throwable cause) { + if (cause instanceof Error) { + throw (Error) cause; + } + throw new ForyJsonException("JSON creator failed for " + ownerType.getName(), cause); + } + } if (invoker != null) { return invoke(arguments); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java index 1a38c0abdc..a6cd06ee17 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java @@ -318,7 +318,8 @@ private static MethodHandle methodHandle(Method method) { } } - private static ForyJsonException accessException(Method method, Throwable e) { + /** Preserves ordinary property-method failure semantics for generated direct calls. */ + protected static ForyJsonException accessException(Method method, Throwable e) { Throwable cause = e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e; return new ForyJsonException("Cannot access JSON property method " + method, cause); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java new file mode 100644 index 0000000000..5720ca7d14 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.fory.json.resolver; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import org.apache.fory.annotation.Internal; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.GeneratedJsonCodecFactory; + +/** Frozen GraalVM runtime reconstruction table for generated JSON companions. */ +@Internal +public final class GeneratedJsonCodecFactories { + private static Map, GeneratedJsonCodecFactory> factories = new IdentityHashMap<>(); + private static boolean frozen; + + private GeneratedJsonCodecFactories() {} + + /** Publishes one hosted-analysis factory. */ + public static synchronized void register(Class type, GeneratedJsonCodecFactory factory) { + if (frozen) { + throw new ForyJsonException("Generated JSON codec factory table is already frozen"); + } + GeneratedJsonCodecFactory previous = factories.get(type); + if (previous == null) { + factories.put(type, factory); + } else if (previous.getClass() != factory.getClass()) { + throw new ForyJsonException( + "Conflicting generated JSON codec factories for " + + type.getName() + + ": " + + previous.getClass().getName() + + " and " + + factory.getClass().getName()); + } + } + + /** Freezes the hosted table into the native image heap. */ + public static synchronized void freeze() { + if (!frozen) { + factories = Collections.unmodifiableMap(new IdentityHashMap<>(factories)); + frozen = true; + } + } + + /** Returns the exact runtime factory, or {@code null} when no companion was generated. */ + public static synchronized GeneratedJsonCodecFactory get(Class type) { + return factories.get(type); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index 192445f331..3c1da0bfc4 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -20,8 +20,13 @@ package org.apache.fory.json.resolver; import java.io.File; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; @@ -60,6 +65,7 @@ import java.util.Currency; import java.util.Date; import java.util.Deque; +import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; @@ -82,6 +88,8 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Pattern; +import org.apache.fory.annotation.Internal; +import org.apache.fory.codegen.GeneratedClassNames; import org.apache.fory.exception.InsecureException; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.JsonConfig; @@ -91,9 +99,12 @@ import org.apache.fory.json.annotation.JsonCodec; import org.apache.fory.json.annotation.JsonSubTypes; import org.apache.fory.json.annotation.JsonSubTypes.Inclusion; +import org.apache.fory.json.annotation.JsonType; import org.apache.fory.json.codec.ArrayCodec; import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; +import org.apache.fory.json.codec.GeneratedJsonCodec; +import org.apache.fory.json.codec.GeneratedJsonCodecFactory; import org.apache.fory.json.codec.GuavaCodecs; import org.apache.fory.json.codec.JsonSubTypesInfo; import org.apache.fory.json.codec.JsonValueCodec; @@ -103,13 +114,18 @@ import org.apache.fory.json.codec.SqlJsonCodecs; import org.apache.fory.json.codegen.JsonCodegen; import org.apache.fory.json.codegen.JsonJITContext; +import org.apache.fory.json.meta.JsonAnySetterAccessor; +import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; import org.apache.fory.resolver.DisallowedList; import org.apache.fory.type.BFloat16; import org.apache.fory.type.Float16; import org.apache.fory.type.TypeUtils; +import org.apache.fory.util.record.RecordUtils; /** * Shared codec definitions and cold caches for all local resolvers of one {@code ForyJson}. @@ -122,9 +138,9 @@ * *

Accepted type-check results are cached by class name up to a bounded 8192-entry shared cache. * Once full, new names are checked on every resolution rather than growing attacker-controlled - * state. Generated classes are shared through one {@link JsonCodegen}; generated instances, - * ordinary type bindings, JIT locks, and callbacks remain resolver-local. A fresh generic {@link - * JsonJITContext} is therefore created for every pooled JSON state. + * state. Source-generated model companions and JIT-generated classes are shared here; concrete JIT + * codec instances, ordinary type bindings, JIT locks, and callbacks remain resolver-local. A fresh + * generic {@link JsonJITContext} is therefore created for every pooled JSON state. */ public final class JsonSharedRegistry { private static final int TYPE_CHECK_CACHE_LIMIT = 8192; @@ -161,6 +177,8 @@ public int compare(DeclarationCandidate left, DeclarationCandidate right) { private final ConcurrentHashMap>, JsonValueCodec> annotationCodecs; private final ConcurrentHashMap, MapKeyCodec> mapKeyCodecs; + private final ConcurrentHashMap, GeneratedJsonCodec> generatedCodecs; + private final Set> typesWithoutGeneratedCodec; public JsonSharedRegistry(JsonConfig config) { this(config, null); @@ -186,6 +204,8 @@ public JsonSharedRegistry(JsonConfig config) { Collections.newSetFromMap(new IdentityHashMap, Boolean>()); annotationCodecs = new ConcurrentHashMap<>(); mapKeyCodecs = new ConcurrentHashMap<>(); + generatedCodecs = new ConcurrentHashMap<>(); + typesWithoutGeneratedCodec = ConcurrentHashMap.newKeySet(); boolean codegenEnabled = config.codegenEnabled(); codegen = codegenEnabled ? new JsonCodegen(config.getCodegenHash(), classLoader) : null; asyncCompilationEnabled = codegenEnabled && config.asyncCompilationEnabled(); @@ -193,6 +213,345 @@ public JsonSharedRegistry(JsonConfig config) { registerExactCodecs(); } + GeneratedJsonCodec generatedCodec(Class type) { + if (type.getDeclaredAnnotation(JsonType.class) == null) { + return null; + } + GeneratedJsonCodec codec = generatedCodecIfPresent(type); + if (codec == null) { + throw new ForyJsonException( + "Missing generated JSON codec " + + generatedCodecBinaryName(type) + + " for @JsonType object model " + + type.getName() + + "; enable the Fory annotation processor and preserve its generated R8 rules"); + } + return codec; + } + + private GeneratedJsonCodec generatedCodecIfPresent(Class type) { + GeneratedJsonCodec codec = generatedCodecs.get(type); + if (codec != null) { + return codec; + } + if (!typesWithoutGeneratedCodec.contains(type)) { + synchronized (generatedCodecs) { + codec = generatedCodecs.get(type); + if (codec == null && !typesWithoutGeneratedCodec.contains(type)) { + codec = loadGeneratedCodec(type); + if (codec == null) { + typesWithoutGeneratedCodec.add(type); + } else { + generatedCodecs.put(type, codec); + } + } + } + } + return codec; + } + + private GeneratedJsonCodec loadGeneratedCodec(Class type) { + if (GraalvmSupport.isGraalRuntime()) { + GeneratedJsonCodecFactory factory = GeneratedJsonCodecFactories.get(type); + return factory == null ? null : validateGeneratedCodec(type, factory.create()); + } + String generatedName = generatedCodecBinaryName(type); + Class generatedClass; + try { + generatedClass = Class.forName(generatedName, false, type.getClassLoader()); + } catch (ClassNotFoundException e) { + return null; + } catch (LinkageError e) { + throw new ForyJsonException("Cannot load generated JSON codec " + generatedName, e); + } + if (!GeneratedJsonCodec.class.isAssignableFrom(generatedClass)) { + throw new ForyJsonException( + "Generated JSON codec " + + generatedName + + " does not extend " + + GeneratedJsonCodec.class.getName()); + } + @SuppressWarnings("unchecked") + Class> codecClass = + (Class>) + generatedClass.asSubclass(GeneratedJsonCodec.class); + return validateGeneratedCodec(type, newGeneratedCodec(codecClass)); + } + + private static GeneratedJsonCodec newGeneratedCodec( + Class> codecClass) { + Constructor> publicConstructor; + try { + publicConstructor = codecClass.getConstructor(); + } catch (NoSuchMethodException e) { + throw new ForyJsonException( + "Generated JSON codec must declare a public no-argument constructor: " + + codecClass.getName(), + e); + } + if (!Modifier.isPublic(codecClass.getModifiers())) { + throw new ForyJsonException("Generated JSON codec must be public: " + codecClass.getName()); + } + if (AndroidSupport.IS_ANDROID) { + try { + return publicConstructor.newInstance(); + } catch (ReflectiveOperationException e) { + throw new ForyJsonException( + "Cannot construct generated JSON codec " + codecClass.getName(), unwrap(e)); + } + } + try { + // Generated companions may live in named application modules that are not opened to Fory. + // The trusted handle is used only once while the shared registry publishes the companion. + MethodHandle constructor = ReflectionUtils.getCtrHandle(codecClass); + return (GeneratedJsonCodec) constructor.invoke(); + } catch (Throwable e) { + throw new ForyJsonException( + "Cannot construct generated JSON codec " + codecClass.getName(), unwrap(e)); + } + } + + /** Validates and initializes a source-generated codec before shared publication. */ + @Internal + public static GeneratedJsonCodec validateGeneratedCodec( + Class type, GeneratedJsonCodec codec) { + if (codec == null) { + throw new ForyJsonException( + "Generated JSON codec factory returned null for " + type.getName()); + } + Class declaredType; + JsonFieldAccessor[] accessors; + JsonAnySetterAccessor anySetter; + String[] creatorNames; + Class[] creatorTypes; + String creatorFactory; + boolean record; + try { + declaredType = codec.type(); + accessors = codec.fieldAccessors(); + anySetter = codec.anySetterAccessor(); + creatorNames = codec.creatorParameterNames(); + creatorTypes = codec.creatorParameterTypes(); + creatorFactory = codec.creatorFactoryName(); + record = codec.isRecord(); + } catch (RuntimeException | LinkageError e) { + throw new ForyJsonException( + "Cannot read generated JSON codec metadata for " + type.getName(), e); + } + if (declaredType != type) { + throw invalidGeneratedCodec(type, "type() must return the exact model class"); + } + if (accessors == null) { + throw invalidGeneratedCodec(type, "fieldAccessors() returned null"); + } + Map memberAccessors = new HashMap<>(); + for (JsonFieldAccessor accessor : accessors) { + validateGeneratedAccessor(type, accessor, memberAccessors); + } + Method anySetterMethod = anySetter == null ? null : validateGeneratedAnySetter(type, anySetter); + Executable creator = + validateGeneratedCreator( + type, memberAccessors, creatorNames, creatorTypes, creatorFactory, record); + if (!AndroidSupport.IS_ANDROID + && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && RecordUtils.isRecord(type) != record) { + throw invalidGeneratedCodec(type, "isRecord() does not match the runtime model class"); + } + codec.initializeValidated( + memberAccessors, + anySetter, + anySetterMethod, + creatorNames, + creatorTypes, + creatorFactory, + creator, + record); + return codec; + } + + private static void validateGeneratedAccessor( + Class type, JsonFieldAccessor accessor, Map memberAccessors) { + if (accessor == null) { + throw invalidGeneratedCodec(type, "fieldAccessors() contains null"); + } + Field field = accessor.field(); + Method getter = accessor.getter(); + Method setter = accessor.setter(); + int memberCount = (field == null ? 0 : 1) + (getter == null ? 0 : 1) + (setter == null ? 0 : 1); + if (memberCount != 1) { + throw invalidGeneratedCodec(type, "each field accessor must identify exactly one member"); + } + Member member = field != null ? field : getter != null ? getter : setter; + if (!member.getDeclaringClass().isAssignableFrom(type) + || Modifier.isStatic(member.getModifiers())) { + throw invalidGeneratedCodec(type, "field accessor does not belong to the model hierarchy"); + } + if (field != null + && (field.isSynthetic() + || Modifier.isTransient(field.getModifiers()) + || field.getType() == Class.class)) { + throw invalidGeneratedCodec(type, "generated field is not an eligible JSON member"); + } + if (member instanceof Method + && (((Method) member).isSynthetic() || ((Method) member).isBridge())) { + throw invalidGeneratedCodec(type, "generated method is not an eligible JSON member"); + } + if (getter != null + && (getter.getParameterCount() != 0 || getter.getReturnType() == void.class)) { + throw invalidGeneratedCodec(type, "generated getter has an invalid signature"); + } + if (setter != null + && (setter.getParameterCount() != 1 || setter.getReturnType() != void.class)) { + throw invalidGeneratedCodec(type, "generated setter has an invalid signature"); + } + if (memberAccessors.put(member, accessor) != null) { + throw invalidGeneratedCodec(type, "duplicate generated accessor for " + member); + } + } + + private static Method validateGeneratedAnySetter(Class type, JsonAnySetterAccessor accessor) { + Method method = accessor.setter(); + if (method == null + || !method.getDeclaringClass().isAssignableFrom(type) + || !Modifier.isPublic(method.getModifiers()) + || Modifier.isStatic(method.getModifiers()) + || method.isSynthetic() + || method.isBridge() + || method.isVarArgs() + || method.getTypeParameters().length != 0 + || method.getReturnType() != void.class + || method.getParameterCount() != 2 + || method.getParameterTypes()[0] != String.class) { + throw invalidGeneratedCodec(type, "generated any setter has an invalid signature"); + } + return method; + } + + private static Executable validateGeneratedCreator( + Class type, + Map memberAccessors, + String[] creatorNames, + Class[] creatorTypes, + String creatorFactory, + boolean record) { + if ((creatorNames == null) != (creatorTypes == null)) { + throw invalidGeneratedCodec( + type, "creator names and parameter types must both be null or non-null"); + } + if (creatorNames == null) { + if (creatorFactory != null || record) { + throw invalidGeneratedCodec(type, "creator metadata is incomplete"); + } + return null; + } + if (record && creatorFactory != null) { + throw invalidGeneratedCodec(type, "a Record creator must be a constructor"); + } + if (creatorNames.length != creatorTypes.length) { + throw invalidGeneratedCodec(type, "creator name and parameter type counts differ"); + } + Set names = new HashSet<>(); + for (int i = 0; i < creatorNames.length; i++) { + if (creatorNames[i] == null + || creatorNames[i].isEmpty() + || !names.add(creatorNames[i]) + || creatorTypes[i] == null + || creatorTypes[i] == void.class) { + throw invalidGeneratedCodec(type, "invalid generated creator parameter at index " + i); + } + } + if (creatorFactory != null && creatorFactory.isEmpty()) { + throw invalidGeneratedCodec(type, "creator factory name must not be empty"); + } + Executable creator; + if (creatorFactory == null) { + Constructor constructor; + try { + constructor = type.getDeclaredConstructor(creatorTypes); + } catch (NoSuchMethodException e) { + throw invalidGeneratedCodec(type, "creator constructor signature does not exist"); + } + if (!record) { + validateGeneratedExecutable(type, constructor); + } + creator = constructor; + } else { + Method factory; + try { + factory = type.getDeclaredMethod(creatorFactory, creatorTypes); + } catch (NoSuchMethodException e) { + throw invalidGeneratedCodec(type, "creator factory signature does not exist"); + } + validateGeneratedExecutable(type, factory); + if (!Modifier.isStatic(factory.getModifiers()) || factory.getReturnType() != type) { + throw invalidGeneratedCodec( + type, "creator factory must be static and return the model type"); + } + creator = factory; + } + if (record) { + validateGeneratedRecordAccessors(type, memberAccessors, creatorNames, creatorTypes); + } + return creator; + } + + private static void validateGeneratedExecutable(Class type, Executable creator) { + int modifiers = creator.getModifiers(); + if (!Modifier.isPublic(modifiers) + || creator.isSynthetic() + || creator.isVarArgs() + || creator.getParameterCount() == 0 + || creator.getTypeParameters().length != 0 + || creator instanceof Method && ((Method) creator).isBridge()) { + throw invalidGeneratedCodec(type, "creator executable has an invalid shape"); + } + } + + private static void validateGeneratedRecordAccessors( + Class type, + Map memberAccessors, + String[] componentNames, + Class[] componentTypes) { + for (int i = 0; i < componentNames.length; i++) { + Method getter; + try { + getter = type.getDeclaredMethod(componentNames[i]); + } catch (NoSuchMethodException e) { + throw invalidGeneratedCodec(type, "Record component getter does not exist at index " + i); + } + JsonFieldAccessor accessor = memberAccessors.get(getter); + if (getter.getReturnType() != componentTypes[i] + || getter.getParameterCount() != 0 + || accessor == null + || !getter.equals(accessor.getter())) { + throw invalidGeneratedCodec(type, "invalid generated Record accessor at index " + i); + } + } + } + + private static ForyJsonException invalidGeneratedCodec(Class type, String reason) { + return new ForyJsonException( + "Invalid generated JSON codec for " + type.getName() + ": " + reason); + } + + private static Throwable unwrap(Throwable throwable) { + return throwable instanceof InvocationTargetException + ? ((InvocationTargetException) throwable).getCause() + : throwable; + } + + /** Returns the deterministic companion binary name for one model class. */ + @Internal + public static String generatedCodecBinaryName(Class type) { + return generatedCodecBinaryName(type.getName()); + } + + /** Returns the deterministic companion binary name for one model binary name. */ + @Internal + public static String generatedCodecBinaryName(String binaryName) { + return GeneratedClassNames.withSuffix(binaryName, "_ForyJsonCodec"); + } + public JsonValueCodec createCodec( Class rawType, TypeRef typeRef, JsonTypeResolver localResolver) { JsonValueCodec customCodec = customCodecs.get(rawType); @@ -372,7 +731,11 @@ JsonValueDeclaration valueDeclaration(Class targetType) { if (typesWithoutValueDeclaration.contains(targetType)) { return null; } - JsonValueDeclaration resolved = JsonValueDeclaration.resolve(targetType); + GeneratedJsonCodec generatedCodec = + targetType.getDeclaredAnnotation(JsonType.class) == null + ? null + : generatedCodecIfPresent(targetType); + JsonValueDeclaration resolved = JsonValueDeclaration.resolve(targetType, generatedCodec); if (resolved == null) { typesWithoutValueDeclaration.add(targetType); } else { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java index 43b3dfa35d..1cb5cab47b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java @@ -26,6 +26,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.reader.Latin1JsonReader; @@ -44,10 +45,15 @@ final class JsonStringValueCodec implements JsonValueCodec { private final boolean raw; JsonStringValueCodec( - Class ownerType, JsonFieldAccessor accessor, Executable creator, boolean raw) { + Class ownerType, + JsonFieldAccessor accessor, + Executable creator, + GeneratedJsonCodec generatedCodec, + boolean raw) { this.ownerType = ownerType; this.accessor = accessor; - this.creator = creator == null ? null : ValueCreator.forExecutable(ownerType, creator); + this.creator = + creator == null ? null : ValueCreator.forExecutable(ownerType, creator, generatedCodec); this.raw = raw; } @@ -154,7 +160,11 @@ final ForyJsonException creatorFailure(Throwable cause) { return new ForyJsonException("JSON creator failed for " + ownerType.getName(), cause); } - static ValueCreator forExecutable(Class ownerType, Executable executable) { + static ValueCreator forExecutable( + Class ownerType, Executable executable, GeneratedJsonCodec generatedCodec) { + if (generatedCodec != null) { + return new GeneratedCreator(ownerType, generatedCodec); + } if (!AndroidSupport.IS_ANDROID) { return new MethodHandleCreator(ownerType, buildInvoker(ownerType, executable)); } @@ -178,6 +188,24 @@ private static MethodHandle buildInvoker(Class ownerType, Executable executab } } + private static final class GeneratedCreator extends ValueCreator { + private final GeneratedJsonCodec generatedCodec; + + private GeneratedCreator(Class ownerType, GeneratedJsonCodec generatedCodec) { + super(ownerType); + this.generatedCodec = generatedCodec; + } + + @Override + Object create(String value) { + try { + return requireResult(generatedCodec.newInstance(new Object[] {value})); + } catch (Throwable cause) { + throw creatorFailure(cause); + } + } + } + private static final class MethodHandleCreator extends ValueCreator { private final MethodHandle invoker; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 8cdf81d2bf..2c49daec79 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -21,6 +21,7 @@ import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; @@ -43,6 +44,7 @@ import org.apache.fory.json.codec.ClosedSubtypeCodec; import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; +import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.codec.JsonSubTypesInfo; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.codec.Latin1ReaderCodec; @@ -1489,11 +1491,20 @@ private ObjectCodec buildObjectCodec(TypeRef ownerType, Object key) { private ObjectCodec newObjectCodec(TypeRef ownerType) { Class rawType = ownerType.getRawType(); sharedRegistry.checkSecure(rawType); + if (rawType.isInterface() + || Modifier.isAbstract(rawType.getModifiers()) + || rawType.isPrimitive() + || rawType.isArray() + || rawType.isEnum()) { + throw new ForyJsonException("Unsupported JSON object type " + rawType); + } + GeneratedJsonCodec generatedCodec = sharedRegistry.generatedCodec(rawType); return ObjectCodec.build( ownerType, sharedRegistry.propertyDiscoveryEnabled(), sharedRegistry.propertyNamingStrategy(), - sharedRegistry.writeNullFields()); + sharedRegistry.writeNullFields(), + generatedCodec); } private JsonTypeInfo buildTypeInfo(Class rawType, Type declaredType, Object key) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java index 253c21fece..c83301d329 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java @@ -37,6 +37,7 @@ import org.apache.fory.json.annotation.JsonProperty; import org.apache.fory.json.annotation.JsonRawValue; import org.apache.fory.json.annotation.JsonValue; +import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.meta.JsonCreatorDeclaration; import org.apache.fory.json.meta.JsonFieldAccessor; @@ -59,11 +60,11 @@ JsonValueCodec codec() { return codec; } - static JsonValueDeclaration resolve(Class type) { + static JsonValueDeclaration resolve(Class type, GeneratedJsonCodec generatedCodec) { List members = new ArrayList<>(); collectFields(type, members); collectMethods(type, members); - coalesceRecordMember(type, members); + coalesceRecordMember(type, members, generatedCodec); if (members.isEmpty()) { return null; } @@ -77,16 +78,31 @@ static JsonValueDeclaration resolve(Class type) { if (member instanceof Field) { Field field = (Field) member; validateField(field); - accessor = JsonFieldAccessor.forField(field); + accessor = generatedAccessor(generatedCodec, field); raw = field.isAnnotationPresent(JsonRawValue.class); } else { Method method = (Method) member; validateMethod(method); - accessor = JsonFieldAccessor.forGetter(method); + accessor = generatedAccessor(generatedCodec, method); raw = method.isAnnotationPresent(JsonRawValue.class); } - Executable creator = valueCreator(type); - return new JsonValueDeclaration(new JsonStringValueCodec(type, accessor, creator, raw)); + Executable creator = valueCreator(type, generatedCodec); + GeneratedJsonCodec creatorBackend = + generatedCodec != null && generatedCodec.matchesCreator(creator) ? generatedCodec : null; + return new JsonValueDeclaration( + new JsonStringValueCodec(type, accessor, creator, creatorBackend, raw)); + } + + private static JsonFieldAccessor generatedAccessor( + GeneratedJsonCodec generatedCodec, Member member) { + JsonFieldAccessor accessor = + generatedCodec == null ? null : generatedCodec.validatedAccessor(member); + if (accessor != null) { + return accessor; + } + return member instanceof Field + ? JsonFieldAccessor.forField((Field) member) + : JsonFieldAccessor.forGetter((Method) member); } private static void collectFields(Class type, List members) { @@ -122,8 +138,14 @@ private static void collectMethods(Class type, List members) { } } - private static void coalesceRecordMember(Class type, List members) { - if (!RecordUtils.isRecord(type) || members.size() != 2) { + private static void coalesceRecordMember( + Class type, List members, GeneratedJsonCodec generatedCodec) { + // Android desugaring removes the platform Record identity but preserves the propagated field + // and accessor annotations. The validated companion is therefore the authoritative Record + // identity and lets this semantic owner select the direct accessor without structural guesses. + boolean record = + generatedCodec == null ? RecordUtils.isRecord(type) : generatedCodec.validatedRecord(); + if (!record || members.size() != 2) { return; } Field field = null; @@ -143,7 +165,10 @@ private static void coalesceRecordMember(Class type, List members) { && field.isAnnotationPresent(JsonRawValue.class) == method.isAnnotationPresent(JsonRawValue.class)) { members.clear(); - members.add(field); + members.add( + generatedCodec != null && generatedCodec.validatedAccessor(method) != null + ? method + : field); } } @@ -183,7 +208,7 @@ private static void validateMethod(Method method) { } } - private static Executable valueCreator(Class type) { + private static Executable valueCreator(Class type, GeneratedJsonCodec generatedCodec) { JsonCreatorDeclaration declaration = JsonCreatorDeclaration.find(type); if (declaration == null) { return null; @@ -192,19 +217,19 @@ private static Executable valueCreator(Class type) { if (declaration.annotation().value().length != 0 || executable.getParameterCount() != 1 || executable.getParameterTypes()[0] != String.class) { - rejectRecordCreator(type); + rejectRecordCreator(type, generatedCodec); return null; } Parameter parameter = executable.getParameters()[0]; if (parameter.isAnnotationPresent(JsonProperty.class)) { - rejectRecordCreator(type); + rejectRecordCreator(type, generatedCodec); return null; } return executable; } - private static void rejectRecordCreator(Class type) { - if (RecordUtils.isRecord(type)) { + private static void rejectRecordCreator(Class type, GeneratedJsonCodec generatedCodec) { + if (generatedCodec == null ? RecordUtils.isRecord(type) : generatedCodec.validatedRecord()) { throw new ForyJsonException("@JsonCreator is not supported on record " + type.getName()); } } diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java index 044a64b250..c6e8194aba 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java @@ -44,9 +44,13 @@ import org.apache.fory.json.annotation.JsonCreator; import org.apache.fory.json.annotation.JsonSubTypes; import org.apache.fory.json.annotation.JsonType; +import org.apache.fory.json.resolver.GeneratedJsonCodecFactories; +import org.apache.fory.json.resolver.JsonSharedRegistry; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; import org.graalvm.nativeimage.hosted.RuntimeReflection; /** Registers reachable Fory JSON models for GraalVM native image reflection. */ @@ -69,6 +73,7 @@ public String getDescription() { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { + RuntimeClassInitialization.initializeAtBuildTime(GeneratedJsonCodecFactories.class); access.registerSubtypeReachabilityHandler(this::processReachableType, Object.class); } @@ -104,24 +109,89 @@ private boolean registerModel(DuringAnalysisAccess access, Class type) { if (!processedModels.add(type)) { return false; } - GraalvmSupport.registerClass(type); + RuntimeReflection.register(type); registerContainer(type); registerDeclarations(type); if (!type.isEnum() && !Collection.class.isAssignableFrom(type) && !Map.class.isAssignableFrom(type)) { registerModelHierarchy(access, type); + if (!type.isRecord() && GraalvmSupport.needReflectionRegisterForCreation(type)) { + RuntimeReflection.registerForReflectiveInstantiation(type); + } } + registerGeneratedCodec(access, type); registerSubtypes(access, type); return true; } + private void registerGeneratedCodec(DuringAnalysisAccess access, Class type) { + String codecName = JsonSharedRegistry.generatedCodecBinaryName(type); + Class codecClass = access.findClassByName(codecName); + if (codecClass == null) { + return; + } + if (!GeneratedJsonCodec.class.isAssignableFrom(codecClass)) { + throw new IllegalStateException( + codecName + " does not extend " + GeneratedJsonCodec.class.getName()); + } + try { + if (!Modifier.isPublic(codecClass.getModifiers())) { + throw new IllegalStateException("Generated JSON codec must be public: " + codecName); + } + codecClass.getConstructor(); + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + "Generated JSON codec must have a public no-argument constructor: " + codecName, e); + } + String factoryName = codecName + "$Factory"; + Class factoryClass = access.findClassByName(factoryName); + if (factoryClass == null || !GeneratedJsonCodecFactory.class.isAssignableFrom(factoryClass)) { + throw new IllegalStateException( + "Missing generated JSON codec factory " + factoryName + " for " + type.getName()); + } + int modifiers = factoryClass.getModifiers(); + if (!Modifier.isPublic(modifiers) + || !Modifier.isStatic(modifiers) + || !Modifier.isFinal(modifiers)) { + throw new IllegalStateException( + "Generated JSON codec factory must be public static final: " + factoryName); + } + try { + GeneratedJsonCodecFactory factory = + (GeneratedJsonCodecFactory) ReflectionUtils.getCtrHandle(factoryClass).invoke(); + GeneratedJsonCodec codec = + JsonSharedRegistry.validateGeneratedCodec(type, factory.create()); + if (codec.validatedRecord() != type.isRecord()) { + throw new IllegalStateException( + "Generated JSON codec Record metadata does not match " + type.getName()); + } + GeneratedJsonCodecFactories.register(type, factory); + } catch (Throwable e) { + throw new IllegalStateException( + "Cannot initialize generated JSON codec factory " + factoryName, e); + } + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + GeneratedJsonCodecFactories.freeze(); + } + private void registerModelHierarchy(BeforeAnalysisAccess access, Class type) { TypeRef ownerType = TypeRef.of(type); boolean record = type.isRecord(); for (Class current = type; current != null && current != Object.class; current = current.getSuperclass()) { + // ObjectCodecBuilder still reads semantic annotations, generic types, and exact creator + // signatures at image runtime. Register that metadata here instead of routing JSON models + // through the core Fory feature, whose Record registration initializes RecordUtils and would + // reintroduce the native-Record path that generated JSON companions replace. + RuntimeReflection.register(current); + RuntimeReflection.register(current.getDeclaredFields()); + RuntimeReflection.register(current.getDeclaredMethods()); + RuntimeReflection.register(current.getDeclaredConstructors()); for (Field field : current.getDeclaredFields()) { if (isJsonField(field)) { if (!current.isRecord() && Runtime.version().feature() <= 24) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecAnnotationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecAnnotationTest.java index 65144434c5..f24d3b9f9a 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecAnnotationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecAnnotationTest.java @@ -150,9 +150,9 @@ public void recordComponent() throws Exception { + " @JsonCodec(AStringCodec.class) public String value() { return value; }\n" + "}\n"); Object accessorValue = accessorType.getConstructor(String.class).newInstance("x"); - assertFailure( - () -> jsonText(newJson(), accessorValue), - "@JsonCodec requires an effective ordinary JSON getter"); + assertEquals(jsonText(newJson(), accessorValue), "{\"value\":\"A:x\"}"); + Object accessorDecoded = newJson().fromJson("{\"value\":\"A:y\"}", accessorType); + assertEquals(accessorType.getMethod("value").invoke(accessorDecoded), "y"); } @Test diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java index 2bd700bc7a..ac0a6e1622 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java @@ -142,6 +142,26 @@ public void propagateCreatorError() { assertThrows(AssertionError.class, () -> newJson().fromJson("{\"id\":1}", ErrorFactory.class)); } + @Test + public void hiddenCreatorParameter() { + PublicHiddenCreator value = + newJson().fromJson("{\"input\":{\"value\":7}}", PublicHiddenCreator.class); + assertEquals(value.value, 7); + } + + static final class HiddenArgument { + public int value; + } + + public static final class PublicHiddenCreator { + public final int value; + + @JsonCreator + public PublicHiddenCreator(@JsonProperty("input") HiddenArgument input) { + value = input.value; + } + } + public static final class User { public final long id; public final String name; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java index 2f1f5cfa47..bc60e927ec 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java @@ -22,10 +22,12 @@ import static org.apache.fory.json.JsonTestSupport.nullCodec; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; +import java.util.Map; import org.apache.fory.platform.JdkVersion; import org.testng.SkipException; import org.testng.annotations.Factory; @@ -114,4 +116,60 @@ public void renamedRecordComponent() throws Exception { assertEquals(type.getMethod("id").invoke(decoded), Integer.valueOf(7)); assertEquals(type.getMethod("displayName").invoke(decoded), "alice"); } + + @Test + public void canonicalCreator() throws Exception { + if (JdkVersion.MAJOR_VERSION < 17) { + throw new SkipException("Java record test requires JDK 17+"); + } + Class emptyType = + compileRecordClass( + "JsonEmptyRecord", + "package org.apache.fory.json.records;\npublic record JsonEmptyRecord() {}\n"); + ForyJson json = newJson(); + assertEquals(json.toJson(emptyType.getConstructor().newInstance()), "{}"); + assertEquals(json.fromJson("{}", emptyType), emptyType.getConstructor().newInstance()); + + Class type = + compileRecordClass( + "JsonCanonicalRecord", + "package org.apache.fory.json.records;\n" + + "public record JsonCanonicalRecord(int id, String name) {\n" + + " public JsonCanonicalRecord { name = name.trim(); }\n" + + " public JsonCanonicalRecord(int id) { this(id, \"aux\"); }\n" + + " @Override public String name() { return name.toUpperCase(); }\n" + + "}\n"); + Object value = type.getConstructor(int.class, String.class).newInstance(9, " value "); + assertEquals(json.toJson(value), "{\"id\":9,\"name\":\"VALUE\"}"); + Object decoded = json.fromJson("{\"id\":10,\"name\":\" next \"}", type); + assertEquals(type.getMethod("id").invoke(decoded), Integer.valueOf(10)); + assertEquals(type.getMethod("name").invoke(decoded), "NEXT"); + } + + @Test + public void ignoredComponent() throws Exception { + if (JdkVersion.MAJOR_VERSION < 17) { + throw new SkipException("Java record test requires JDK 17+"); + } + Class type = + compileRecordClass( + "JsonIgnoredRecord", + "package org.apache.fory.json.records;\n" + + "import java.util.Map;\n" + + "import org.apache.fory.json.annotation.*;\n" + + "public record JsonIgnoredRecord(\n" + + " @JsonIgnore(ignoreWrite = false) int id,\n" + + " @JsonAnyProperty Map extra) {}\n"); + ForyJson json = newJson(); + byte[] bytes = "{\"id\":9,\"unknown\":1}".getBytes(StandardCharsets.UTF_8); + for (Object decoded : + Arrays.asList( + json.fromJson(new String(bytes, StandardCharsets.UTF_8), type), + json.fromJson(bytes, type))) { + assertEquals(type.getMethod("id").invoke(decoded), Integer.valueOf(0)); + Map extra = (Map) type.getMethod("extra").invoke(decoded); + assertEquals(extra.size(), 1); + assertTrue(extra.containsKey("unknown")); + } + } } diff --git a/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/ForyKotlinSymbolProcessor.kt b/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/ForyKotlinSymbolProcessor.kt index eb5614ba6b..d50bd050dd 100644 --- a/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/ForyKotlinSymbolProcessor.kt +++ b/kotlin/fory-kotlin-ksp/src/main/kotlin/org/apache/fory/kotlin/ksp/ForyKotlinSymbolProcessor.kt @@ -40,6 +40,7 @@ import com.google.devtools.ksp.symbol.Modifier import com.google.devtools.ksp.symbol.Nullability import java.nio.charset.StandardCharsets import java.util.Locale +import org.apache.fory.codegen.GeneratedClassNames private const val MAX_DEFAULT_FIELDS = 12 @@ -241,7 +242,7 @@ internal class ForyKotlinSymbolProcessor(private val environment: SymbolProcesso packageName = packageName, typeName = typeName, qualifiedTypeName = declaration.qualifiedName!!.asString(), - serializerName = "${escapeBinarySimpleName(typeName)}_ForySerializer", + serializerName = "${GeneratedClassNames.escapeBinarySimpleName(typeName)}_ForySerializer", serializerVisibility = if (Modifier.INTERNAL in declaration.modifiers) { KotlinSerializerVisibility.INTERNAL @@ -542,7 +543,7 @@ internal class ForyKotlinSymbolProcessor(private val environment: SymbolProcesso packageName = packageName, typeName = typeName, qualifiedTypeName = declaration.qualifiedName!!.asString(), - serializerName = "${escapeBinarySimpleName(typeName)}_ForySerializer", + serializerName = "${GeneratedClassNames.escapeBinarySimpleName(typeName)}_ForySerializer", serializerVisibility = if (Modifier.INTERNAL in declaration.modifiers) { KotlinSerializerVisibility.INTERNAL @@ -1716,17 +1717,7 @@ internal class ForyKotlinSymbolProcessor(private val environment: SymbolProcesso annotation.annotationType.resolve().declaration.qualifiedName?.asString() == qualifiedName private fun escapedResourceName(targetBinaryName: String): String { - val builder = StringBuilder(targetBinaryName.length + 32) - for (char in targetBinaryName) { - when { - char == '.' -> builder.append('.') - char == '$' -> builder.append('_') - char == '_' -> builder.append("_u_") - Character.isJavaIdentifierPart(char) -> builder.append(char) - else -> builder.append("_x").append(char.code.toString(16)).append('_') - } - } - return builder.toString() + return GeneratedClassNames.escapeBinaryName(targetBinaryName) } private sealed class Encoding(val sourceName: String) { @@ -1765,22 +1756,6 @@ internal class ForyKotlinSymbolProcessor(private val environment: SymbolProcesso } } -internal fun escapeBinarySimpleName(binarySimpleName: String): String { - val builder = StringBuilder(binarySimpleName.length + 32) - var index = 0 - while (index < binarySimpleName.length) { - val codePoint = Character.codePointAt(binarySimpleName, index) - when { - codePoint == '$'.code -> builder.append('_') - codePoint == '_'.code -> builder.append("_u_") - Character.isJavaIdentifierPart(codePoint) -> builder.appendCodePoint(codePoint) - else -> builder.append("_x").append(codePoint.toString(16)).append('_') - } - index += Character.charCount(codePoint) - } - return builder.toString() -} - internal fun isValidMapKeyType(type: KotlinSourceTypeNode): Boolean { if (type.nullable || type.typeArguments.isNotEmpty() || type.componentType != null) { return false diff --git a/kotlin/fory-kotlin-ksp/src/test/kotlin/org/apache/fory/kotlin/ksp/ProcessorValidationTest.kt b/kotlin/fory-kotlin-ksp/src/test/kotlin/org/apache/fory/kotlin/ksp/ProcessorValidationTest.kt index 2644c12c17..d7ed7ad6e4 100644 --- a/kotlin/fory-kotlin-ksp/src/test/kotlin/org/apache/fory/kotlin/ksp/ProcessorValidationTest.kt +++ b/kotlin/fory-kotlin-ksp/src/test/kotlin/org/apache/fory/kotlin/ksp/ProcessorValidationTest.kt @@ -21,8 +21,10 @@ package org.apache.fory.kotlin.ksp import com.google.devtools.ksp.symbol.ClassKind import com.google.devtools.ksp.symbol.Modifier +import org.apache.fory.codegen.GeneratedClassNames import org.testng.Assert.assertEquals import org.testng.Assert.assertFalse +import org.testng.Assert.assertNotEquals import org.testng.Assert.assertNull import org.testng.Assert.assertTrue import org.testng.annotations.Test @@ -827,23 +829,30 @@ class ProcessorValidationTest { @Test fun escapesSerializerNames() { - assertEquals(escapeBinarySimpleName("User") + "_ForySerializer", "User_ForySerializer") assertEquals( - escapeBinarySimpleName("Outer\$Inner") + "_ForySerializer", - "Outer_Inner_ForySerializer", + GeneratedClassNames.escapeBinarySimpleName("User") + "_ForySerializer", + "User_ForySerializer", ) assertEquals( - escapeBinarySimpleName("Outer_Inner") + "_ForySerializer", + GeneratedClassNames.escapeBinarySimpleName("Outer\$Inner") + "_ForySerializer", + "Outer_d_Inner_ForySerializer", + ) + assertEquals( + GeneratedClassNames.escapeBinarySimpleName("Outer_Inner") + "_ForySerializer", "Outer_u_Inner_ForySerializer", ) assertEquals( - escapeBinarySimpleName("Outer__Inner") + "_ForySerializer", + GeneratedClassNames.escapeBinarySimpleName("Outer__Inner") + "_ForySerializer", "Outer_u__u_Inner_ForySerializer", ) assertEquals( - escapeBinarySimpleName("Outer-Inner") + "_ForySerializer", + GeneratedClassNames.escapeBinarySimpleName("Outer-Inner") + "_ForySerializer", "Outer_x2d_Inner_ForySerializer", ) + assertNotEquals( + GeneratedClassNames.escapeBinarySimpleName("A\$u_X"), + GeneratedClassNames.escapeBinarySimpleName("A_u\$X"), + ) } @Test diff --git a/kotlin/fory-kotlin/src/main/java/org/apache/fory/serializer/kotlin/KotlinSerializers.java b/kotlin/fory-kotlin/src/main/java/org/apache/fory/serializer/kotlin/KotlinSerializers.java index 41b003f0eb..d8edd694dc 100644 --- a/kotlin/fory-kotlin/src/main/java/org/apache/fory/serializer/kotlin/KotlinSerializers.java +++ b/kotlin/fory-kotlin/src/main/java/org/apache/fory/serializer/kotlin/KotlinSerializers.java @@ -35,6 +35,7 @@ import kotlin.uuid.Uuid; import org.apache.fory.Fory; import org.apache.fory.ThreadSafeFory; +import org.apache.fory.codegen.GeneratedClassNames; import org.apache.fory.config.Config; import org.apache.fory.exception.ForyException; import org.apache.fory.resolver.TypeResolver; @@ -316,33 +317,7 @@ private static Class generatedSerializerClass(Class cls) { } private static String generatedSerializerBinaryName(Class cls) { - String binaryName = cls.getName(); - int packageEnd = binaryName.lastIndexOf('.'); - if (packageEnd < 0) { - return escapeBinarySimpleName(binaryName) + XLANG_GENERATED_SERIALIZER_SUFFIX; - } - return binaryName.substring(0, packageEnd) - + "." - + escapeBinarySimpleName(binaryName.substring(packageEnd + 1)) - + XLANG_GENERATED_SERIALIZER_SUFFIX; - } - - private static String escapeBinarySimpleName(String binarySimpleName) { - StringBuilder builder = new StringBuilder(binarySimpleName.length() + 32); - for (int index = 0; index < binarySimpleName.length(); ) { - int codePoint = binarySimpleName.codePointAt(index); - if (codePoint == '$') { - builder.append('_'); - } else if (codePoint == '_') { - builder.append("_u_"); - } else if (Character.isJavaIdentifierPart(codePoint)) { - builder.appendCodePoint(codePoint); - } else { - builder.append("_x").append(Integer.toHexString(codePoint)).append('_'); - } - index += Character.charCount(codePoint); - } - return builder.toString(); + return GeneratedClassNames.withSuffix(cls.getName(), XLANG_GENERATED_SERIALIZER_SUFFIX); } @SuppressWarnings("unchecked")