diff --git a/benchmarks/java/pom.xml b/benchmarks/java/pom.xml index fe87785476..c6e68b611c 100644 --- a/benchmarks/java/pom.xml +++ b/benchmarks/java/pom.xml @@ -43,7 +43,9 @@ 1.8.0 2.0.62 3.25.5 - 2.18.6 + 2.22.0 + 2.22 + 2.14.0 3.6.1 3.6.2 3.5.0 @@ -82,7 +84,7 @@ com.fasterxml.jackson.core jackson-annotations - ${jackson.version} + ${jackson.annotations.version} com.fasterxml.jackson.datatype @@ -98,6 +100,11 @@ fory-core ${project.version} + + org.apache.fory + fory-json + ${project.version} + org.apache.fory fory-format @@ -173,6 +180,26 @@ fastjson2 ${fastjson2.version} + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.annotations.version} + + + com.google.code.gson + gson + ${gson.version} + com.google.protobuf protobuf-java diff --git a/benchmarks/java/src/main/java/org/apache/fory/benchmark/JsonSerializationSuite.java b/benchmarks/java/src/main/java/org/apache/fory/benchmark/JsonSerializationSuite.java new file mode 100644 index 0000000000..52f31985e7 --- /dev/null +++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/JsonSerializationSuite.java @@ -0,0 +1,140 @@ +/* + * 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.benchmark; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONWriter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.apache.fory.benchmark.data.MediaContent; +import org.apache.fory.json.ForyJson; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3, time = 2) +@Measurement(iterations = 5, time = 2) +@Fork(1) +@Threads(1) +public class JsonSerializationSuite { + @State(Scope.Thread) + public static class JsonState { + ForyJson foryJson; + JSONWriter.Context fastjson2Context; + ObjectMapper mapper; + Gson gson; + MediaContent mediaContent; + + @Setup + public void setup() { + foryJson = ForyJson.builder().build(); + fastjson2Context = new JSONWriter.Context(); + mapper = new ObjectMapper(); + gson = new Gson(); + mediaContent = JSON.parseObject(readResource(), MediaContent.class); + byte[] foryBytes = foryJson.toJsonBytes(mediaContent); + byte[] fastjsonBytes = + JSON.toJSONBytes(mediaContent, StandardCharsets.UTF_8, fastjson2Context); + if (!JSON.parseObject(foryBytes).equals(JSON.parseObject(fastjsonBytes))) { + throw new IllegalStateException("Fory JSON and fastjson2 produce different JSON objects"); + } + String foryString = foryJson.toJson(mediaContent); + String fastjsonString = JSON.toJSONString(mediaContent, fastjson2Context); + if (!JSON.parseObject(foryString).equals(JSON.parseObject(fastjsonString))) { + throw new IllegalStateException("Fory JSON and fastjson2 produce different JSON strings"); + } + } + + private static String readResource() { + InputStream input = + JsonSerializationSuite.class.getClassLoader().getResourceAsStream("data/eishay.json"); + if (input == null) { + throw new IllegalStateException("Missing data/eishay.json"); + } + try (InputStream closeable = input; + InputStreamReader reader = new InputStreamReader(closeable, StandardCharsets.UTF_8)) { + char[] buffer = new char[1024]; + StringBuilder builder = new StringBuilder(); + int read; + while ((read = reader.read(buffer)) != -1) { + builder.append(buffer, 0, read); + } + return builder.toString(); + } catch (IOException e) { + throw new IllegalStateException("Unable to read data/eishay.json", e); + } + } + } + + @Benchmark + public byte[] foryToJsonBytes(JsonState state) { + return state.foryJson.toJsonBytes(state.mediaContent); + } + + @Benchmark + public byte[] fastjson2ToJsonBytes(JsonState state) { + return JSON.toJSONBytes(state.mediaContent, StandardCharsets.UTF_8, state.fastjson2Context); + } + + @Benchmark + public byte[] jacksonToJsonBytes(JsonState state) throws IOException { + return state.mapper.writeValueAsBytes(state.mediaContent); + } + + @Benchmark + public byte[] gsonToJsonBytes(JsonState state) { + return state.gson.toJson(state.mediaContent).getBytes(StandardCharsets.UTF_8); + } + + @Benchmark + public String foryToJsonString(JsonState state) { + return state.foryJson.toJson(state.mediaContent); + } + + @Benchmark + public String fastjson2ToJsonString(JsonState state) { + return JSON.toJSONString(state.mediaContent, state.fastjson2Context); + } + + @Benchmark + public String jacksonToJsonString(JsonState state) throws IOException { + return state.mapper.writeValueAsString(state.mediaContent); + } + + @Benchmark + public String gsonToJsonString(JsonState state) { + return state.gson.toJson(state.mediaContent); + } +} diff --git a/benchmarks/java/src/main/resources/data/eishay.json b/benchmarks/java/src/main/resources/data/eishay.json new file mode 100644 index 0000000000..a96a3a898a --- /dev/null +++ b/benchmarks/java/src/main/resources/data/eishay.json @@ -0,0 +1,30 @@ +{"images": [{ + "height":768, + "size":"LARGE", + "title":"Javaone Keynote", + "uri":"http://javaone.com/keynote_large.jpg", + "width":1024 + }, { + "height":240, + "size":"SMALL", + "title":"Javaone Keynote", + "uri":"http://javaone.com/keynote_small.jpg", + "width":320 + } + ], + "media": { + "bitrate":262144, + "duration":18000000, + "format":"video/mpg4", + "height":480, + "persons": [ + "Bill Gates", + "Steve Jobs" + ], + "player":"JAVA", + "size":58982400, + "title":"Javaone Keynote", + "uri":"http://javaone.com/keynote.mpg", + "width":640 + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java index 0017b83400..185fb70c07 100644 --- a/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/Expression.java @@ -396,6 +396,10 @@ public static Literal ofLong(long v) { return new Literal(v, PRIMITIVE_LONG_TYPE); } + public static Literal ofChar(char v) { + return new Literal(v, TypeRef.of(char.class)); + } + public static Literal ofClass(Class v) { return new Literal(v, CLASS_TYPE); } @@ -461,6 +465,9 @@ public ExprCode doGenCode(CodegenContext ctx) { return new ExprCode( FalseLiteral, new LiteralValue(javaType, String.format("%dL", ((Number) (value)).longValue()))); + } else if (javaType == Character.class) { + return new ExprCode( + FalseLiteral, new LiteralValue(javaType, charLiteral(((Character) value)))); } else if (isPrimitive(javaType)) { return new ExprCode(FalseLiteral, new LiteralValue(javaType, String.valueOf(value))); } else if (javaType == Class.class) { @@ -478,6 +485,32 @@ public ExprCode doGenCode(CodegenContext ctx) { } } + private static String charLiteral(char value) { + switch (value) { + case '\b': + return "'\\b'"; + case '\t': + return "'\\t'"; + case '\n': + return "'\\n'"; + case '\f': + return "'\\f'"; + case '\r': + return "'\\r'"; + case '\"': + return "'\\\"'"; + case '\'': + return "'\\\''"; + case '\\': + return "'\\\\'"; + default: + if (value < 0x20 || value > 0x7e) { + return String.format("'\\u%04x'", (int) value); + } + return "'" + value + "'"; + } + } + public Object getValue() { return value; } @@ -1764,6 +1797,64 @@ public ExprCode doGenCode(CodegenContext ctx) { } } + class ArrayValue extends Inlineable { + private final Expression targetArray; + private final Expression[] indexes; + private final TypeRef type; + + public ArrayValue(Expression targetArray, Expression... indexes) { + this(targetArray.type().getComponentType(), targetArray, indexes); + } + + public ArrayValue(TypeRef type, Expression targetArray, Expression... indexes) { + super(ofArrayList(targetArray, indexes)); + this.targetArray = targetArray; + this.indexes = indexes; + this.type = type; + this.inlineCall = true; + } + + @Override + public TypeRef type() { + return type; + } + + @Override + public ExprCode doGenCode(CodegenContext ctx) { + StringBuilder codeBuilder = new StringBuilder(); + ExprCode targetExprCode = targetArray.genCode(ctx); + if (StringUtils.isNotBlank(targetExprCode.code())) { + codeBuilder.append(targetExprCode.code()).append('\n'); + } + ExprCode[] indexExprCodes = new ExprCode[indexes.length]; + for (int i = 0; i < indexes.length; i++) { + ExprCode indexExprCode = indexes[i].genCode(ctx); + indexExprCodes[i] = indexExprCode; + if (StringUtils.isNotBlank(indexExprCode.code())) { + codeBuilder.append(indexExprCode.code()).append('\n'); + } + } + StringBuilder value = new StringBuilder(targetExprCode.value().code()); + for (ExprCode indexExprCode : indexExprCodes) { + value.append('[').append(indexExprCode.value()).append(']'); + } + if (inlineCall) { + String code = StringUtils.isBlank(codeBuilder) ? null : codeBuilder.toString(); + return new ExprCode(code, FalseLiteral, Code.variable(getRawType(type), value.toString())); + } + Class rawType = getRawType(type); + String name = ctx.newName(ctx.namePrefix(rawType)); + codeBuilder + .append(ctx.type(type)) + .append(' ') + .append(name) + .append(" = ") + .append(value) + .append(';'); + return new ExprCode(codeBuilder.toString(), FalseLiteral, Code.variable(rawType, name)); + } + } + class If extends AbstractExpression { private Expression predicate; private Expression trueExpr; @@ -2042,7 +2133,7 @@ public Not(Expression target) { super(target); this.target = target; Preconditions.checkArgument( - target.type() == PRIMITIVE_BOOLEAN_TYPE || target.type() == BOOLEAN_TYPE); + PRIMITIVE_BOOLEAN_TYPE.equals(target.type()) || BOOLEAN_TYPE.equals(target.type())); } @Override @@ -2264,6 +2355,76 @@ public LogicalOr(boolean inline, Expression left, Expression right) { } } + class Ternary extends ValueExpression { + private final Expression predicate; + private final Expression trueValue; + private final Expression falseValue; + private final TypeRef type; + + public Ternary(Expression predicate, Expression trueValue, Expression falseValue) { + this(predicate, trueValue, falseValue, true, trueValue.type()); + } + + public Ternary( + Expression predicate, + Expression trueValue, + Expression falseValue, + boolean inline, + TypeRef type) { + super(new Expression[] {predicate, trueValue, falseValue}); + this.predicate = predicate; + this.trueValue = trueValue; + this.falseValue = falseValue; + this.inlineCall = inline; + this.type = type; + } + + @Override + public TypeRef type() { + return type; + } + + @Override + public ExprCode doGenCode(CodegenContext ctx) { + StringBuilder codeBuilder = new StringBuilder(); + ExprCode predicateCode = predicate.genCode(ctx); + ExprCode trueCode = trueValue.genCode(ctx); + ExprCode falseCode = falseValue.genCode(ctx); + Stream.of(predicateCode, trueCode, falseCode) + .forEach( + exprCode -> { + if (StringUtils.isNotBlank(exprCode.code())) { + appendNewlineIfNeeded(codeBuilder); + codeBuilder.append(exprCode.code()); + } + }); + String value = + StringUtils.format( + "((${predicate}) ? (${trueValue}) : (${falseValue}))", + "predicate", + predicateCode.value(), + "trueValue", + trueCode.value(), + "falseValue", + falseCode.value()); + if (inlineCall) { + String code = StringUtils.isBlank(codeBuilder) ? null : codeBuilder.toString(); + return new ExprCode(code, FalseLiteral, Code.variable(getRawType(type), value)); + } + Class rawType = getRawType(type); + String name = ctx.newName(valuePrefix); + appendNewlineIfNeeded(codeBuilder); + codeBuilder + .append(ctx.type(type)) + .append(' ') + .append(name) + .append(" = ") + .append(value) + .append(';'); + return new ExprCode(codeBuilder.toString(), FalseLiteral, Code.variable(rawType, name)); + } + } + /** * While expression for java while. TODO(chaokunyang) refactor to: * @@ -2807,6 +2968,105 @@ public String toString() { } } + class Switch extends AbstractExpression { + private final Expression selector; + private final Case[] cases; + private final Expression defaultAction; + + public Switch(Expression selector, Case[] cases, Expression defaultAction) { + super(switchInputs(selector, cases, defaultAction)); + this.selector = selector; + this.cases = cases; + this.defaultAction = defaultAction; + } + + private static List switchInputs( + Expression selector, Case[] cases, Expression defaultAction) { + List inputs = new ArrayList<>(1 + cases.length + (defaultAction == null ? 0 : 1)); + inputs.add(selector); + if (defaultAction != null) { + inputs.add(defaultAction); + } + for (Case switchCase : cases) { + inputs.add(switchCase.action); + } + return inputs; + } + + @Override + public TypeRef type() { + return PRIMITIVE_VOID_TYPE; + } + + @Override + public ExprCode doGenCode(CodegenContext ctx) { + StringBuilder codeBuilder = new StringBuilder(); + ExprCode selectorCode = selector.genCode(ctx); + if (StringUtils.isNotBlank(selectorCode.code())) { + codeBuilder.append(selectorCode.code()).append('\n'); + } + codeBuilder.append("switch (").append(selectorCode.value()).append(") {\n"); + for (Case switchCase : cases) { + ExprCode actionCode = switchCase.action.genCode(ctx); + codeBuilder.append(" case ").append(switchCase.value).append(":\n"); + if (StringUtils.isNotBlank(actionCode.code())) { + codeBuilder.append(indent(actionCode.code(), 4)).append('\n'); + } + } + if (defaultAction != null) { + ExprCode defaultCode = defaultAction.genCode(ctx); + codeBuilder.append(" default:\n"); + if (StringUtils.isNotBlank(defaultCode.code())) { + codeBuilder.append(indent(defaultCode.code(), 4)).append('\n'); + } + } + codeBuilder.append('}'); + return new ExprCode(codeBuilder.toString(), null, null); + } + + public static final class Case { + private final int value; + private final Expression action; + + public Case(int value, Expression action) { + this.value = value; + this.action = action; + } + } + } + + class SuperCall extends AbstractExpression { + private final Expression[] arguments; + + public SuperCall(Expression... arguments) { + super(arguments); + this.arguments = arguments; + } + + @Override + public TypeRef type() { + return PRIMITIVE_VOID_TYPE; + } + + @Override + public ExprCode doGenCode(CodegenContext ctx) { + StringBuilder codeBuilder = new StringBuilder(); + StringBuilder argsBuilder = new StringBuilder(); + for (int i = 0; i < arguments.length; i++) { + ExprCode argCode = arguments[i].genCode(ctx); + if (StringUtils.isNotBlank(argCode.code())) { + codeBuilder.append(argCode.code()).append('\n'); + } + if (i != 0) { + argsBuilder.append(", "); + } + argsBuilder.append(argCode.value()); + } + codeBuilder.append("super(").append(argsBuilder).append(");"); + return new ExprCode(codeBuilder.toString(), null, null); + } + } + class Break extends AbstractExpression { public Break() { super(new Expression[0]); diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/LittleEndian.java b/java/fory-core/src/main/java/org/apache/fory/memory/LittleEndian.java index 4cb7b5ef6a..8f325c2748 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/LittleEndian.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/LittleEndian.java @@ -81,6 +81,25 @@ public static long getInt64(byte[] o, int index) { return NativeByteOrder.IS_LITTLE_ENDIAN ? v : Long.reverseBytes(v); } + public static int getInt32(byte[] o, int index) { + if (AndroidSupport.IS_ANDROID) { + return MemoryOps.getInt32(o, index); + } + int v = UNSAFE.getInt(o, (long) BYTE_ARRAY_OFFSET + index); + return NativeByteOrder.IS_LITTLE_ENDIAN ? v : Integer.reverseBytes(v); + } + + public static void putInt32(byte[] o, int index, int value) { + if (AndroidSupport.IS_ANDROID) { + MemoryOps.putInt32(o, index, value); + return; + } + if (!NativeByteOrder.IS_LITTLE_ENDIAN) { + value = Integer.reverseBytes(value); + } + UNSAFE.putInt(o, (long) BYTE_ARRAY_OFFSET + index, value); + } + public static void putInt64(byte[] o, int index, long value) { if (AndroidSupport.IS_ANDROID) { MemoryOps.putInt64(o, index, value); diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java b/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java index 3639734496..0c3b0f3242 100644 --- a/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java +++ b/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java @@ -336,6 +336,25 @@ public void putDouble(Object obj, double value) { checkObj(obj); UNSAFE.putDouble(obj, fieldOffset, value); } + + @Override + public Object getObject(Object obj) { + if (accessKind != OBJECT_ACCESS) { + return get(obj); + } + checkObj(obj); + return UNSAFE.getObject(obj, fieldOffset); + } + + @Override + public void putObject(Object obj, Object value) { + if (accessKind != OBJECT_ACCESS) { + set(obj, value); + return; + } + checkObj(obj); + UNSAFE.putObject(obj, fieldOffset, value); + } } static final class GeneratedAccessor extends FieldAccessor { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java index cd4d8a75db..bb3d93f22a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java @@ -33,6 +33,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import org.apache.fory.annotation.CodegenInvoke; +import org.apache.fory.annotation.Internal; import org.apache.fory.codegen.Expression; import org.apache.fory.codegen.Expression.Invoke; import org.apache.fory.codegen.Expression.StaticInvoke; @@ -428,10 +429,39 @@ private static Object getStringValue(String value) { return PlatformStringUtils.getStringValue(value); } - private static byte getStringCoder(String value) { + @Internal + public static boolean isBytesBackedString() { + return STRING_VALUE_FIELD_IS_BYTES; + } + + @Internal + public static byte[] getStringBytes(String value) { + if (!STRING_VALUE_FIELD_IS_BYTES) { + throw new IllegalStateException("String byte layout is not available"); + } + return (byte[]) getStringValue(value); + } + + @Internal + public static byte getStringCoder(String value) { return PlatformStringUtils.getStringCoder(value); } + @Internal + public static boolean isLatin1Coder(byte coder) { + return coder == LATIN1; + } + + @Internal + public static boolean isUtf16Coder(byte coder) { + return coder == UTF16; + } + + @Internal + public static char getBytesChar(byte[] bytes, int byteIndex) { + return PlatformStringUtils.getBytesChar(bytes, byteIndex); + } + private static int getStringOffset(String value) { return PlatformStringUtils.getStringOffset(value); } @@ -916,7 +946,7 @@ public static String newCharsStringZeroCopy(char[] data) { // coder param first to make inline call args // `(buffer.readByte(), buffer.readBytesWithSizeEmbedded())` work. public static String newBytesStringZeroCopy(byte coder, byte[] data) { - if (!JDK_INTERNAL_FIELD_ACCESS) { + if (!JDK_INTERNAL_FIELD_ACCESS || BYTES_STRING_ZERO_COPY_CTR == null) { return newBytesStringSlow(coder, data); } if (coder == LATIN1) { @@ -931,6 +961,11 @@ public static String newBytesStringZeroCopy(byte coder, byte[] data) { } } + @Internal + public static String newLatin1StringZeroCopy(byte[] data) { + return newBytesStringZeroCopy(LATIN1, data); + } + private static String newBytesStringSlow(byte coder, byte[] data) { if (coder == LATIN1) { return new String(data, StandardCharsets.ISO_8859_1); diff --git a/java/fory-core/src/main/java25/org/apache/fory/memory/LittleEndian.java b/java/fory-core/src/main/java25/org/apache/fory/memory/LittleEndian.java index 7d6e524826..cc22992b50 100644 --- a/java/fory-core/src/main/java25/org/apache/fory/memory/LittleEndian.java +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/LittleEndian.java @@ -24,6 +24,8 @@ import java.nio.ByteOrder; public class LittleEndian { + private static final VarHandle BYTE_ARRAY_INT = + MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.LITTLE_ENDIAN); private static final VarHandle BYTE_ARRAY_LONG = MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN); @@ -66,6 +68,14 @@ public static long getInt64(byte[] o, int index) { return (long) BYTE_ARRAY_LONG.get(o, index); } + public static int getInt32(byte[] o, int index) { + return (int) BYTE_ARRAY_INT.get(o, index); + } + + public static void putInt32(byte[] o, int index, int value) { + BYTE_ARRAY_INT.set(o, index, value); + } + public static void putInt64(byte[] o, int index, long value) { BYTE_ARRAY_LONG.set(o, index, value); } diff --git a/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java b/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java index fe468bc9e9..fc0048faa2 100644 --- a/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java +++ b/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java @@ -280,5 +280,15 @@ public double getDouble(Object obj) { public void putDouble(Object obj, double value) { handle.set(obj, value); } + + @Override + public Object getObject(Object obj) { + return handle.get(obj); + } + + @Override + public void putObject(Object obj, Object value) { + handle.set(obj, value); + } } } diff --git a/java/fory-core/src/test/java/org/apache/fory/reflect/FieldAccessorTest.java b/java/fory-core/src/test/java/org/apache/fory/reflect/FieldAccessorTest.java index 14778e4d94..003902fc1a 100644 --- a/java/fory-core/src/test/java/org/apache/fory/reflect/FieldAccessorTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/reflect/FieldAccessorTest.java @@ -71,8 +71,12 @@ public void testHiddenAccessor() throws Exception { FieldAccessor intAccessor = FieldAccessor.createAccessor(HiddenFields.class.getDeclaredField("i")); Assert.assertEquals(intAccessor.getInt(fields), 1); + Assert.assertEquals(intAccessor.getObject(fields), 1); intAccessor.putInt(fields, 2); Assert.assertEquals(intAccessor.getInt(fields), 2); + intAccessor.putObject(fields, 3); + Assert.assertEquals(intAccessor.getInt(fields), 3); + Assert.assertEquals(intAccessor.getObject(fields), 3); FieldAccessor objectAccessor = FieldAccessor.createAccessor(HiddenFields.class.getDeclaredField("text")); diff --git a/java/fory-json/pom.xml b/java/fory-json/pom.xml new file mode 100644 index 0000000000..1425c3e3f7 --- /dev/null +++ b/java/fory-json/pom.xml @@ -0,0 +1,157 @@ + + + + + org.apache.fory + fory-parent + 1.3.0-SNAPSHOT + + 4.0.0 + + fory-json + + + Apache Fory JSON serialization. + + + + 8 + 8 + ${basedir}/.. + + + + + org.apache.fory + fory-core + ${project.version} + + + org.apache.fory + fory-test-core + ${project.version} + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.apache.fory.json + true + + + + + + + + + + jpms-java9 + + [9,) + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + compile-java9-module-info + process-classes + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + inject-java9-module-info + package + + run + + + + + + + + + + + + + + + + diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java new file mode 100644 index 0000000000..8fc4662da3 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -0,0 +1,358 @@ +/* + * 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; + +import java.lang.reflect.Type; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceArray; +import org.apache.fory.json.codec.GeneratedObjectCodec; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.CodecRegistry; +import org.apache.fory.json.resolver.JsonSharedRegistry; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.reflect.TypeRef; +import org.apache.fory.serializer.StringSerializer; + +/** Thread-safe public facade for Fory JSON serialization and parsing. */ +public final class ForyJson { + private static final int PREFERRED_SLOT_RETRIES = 2; + private static final int INITIAL_BUFFER_SIZE = 8192; + private static final int PRIMARY_SLOT = -1; + private static final int TEMPORARY_SLOT = -2; + private static final int DEFAULT_POOL_SIZE = + Math.max(1, Runtime.getRuntime().availableProcessors() * 4); + + /** Default maximum nested JSON object/array depth accepted by parsers. */ + public static final int DEFAULT_MAX_DEPTH = 20; + + private final JsonSharedRegistry sharedRegistry; + private final boolean writeNullFields; + private final int maxDepth; + private final int poolSize; + private final AtomicReference primarySlot; + private final AtomicReferenceArray slots; + + ForyJson( + boolean writeNullFields, boolean codegenEnabled, int maxDepth, CodecRegistry codecRegistry) { + this.writeNullFields = writeNullFields; + this.maxDepth = maxDepth; + sharedRegistry = new JsonSharedRegistry(codegenEnabled, writeNullFields, codecRegistry); + poolSize = DEFAULT_POOL_SIZE; + primarySlot = + new AtomicReference<>( + new PooledState(new JsonState(writeNullFields, sharedRegistry), PRIMARY_SLOT)); + slots = new AtomicReferenceArray<>(poolSize); + for (int i = 0; i < poolSize; i++) { + slots.set(i, new PooledState(new JsonState(writeNullFields, sharedRegistry), i)); + } + } + + public static ForyJsonBuilder builder() { + return new ForyJsonBuilder(); + } + + public String toJson(Object value) { + PooledState entry = acquire(); + JsonState state = entry.state; + StringJsonWriter writer = state.stringWriter; + try { + if (value == null) { + writer.writeNull(); + } else { + JsonTypeResolver resolver = state.typeResolver; + JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); + typeInfo.codec().writeString(writer, value, resolver); + } + return writer.toJson(); + } finally { + writer.reset(); + release(entry); + } + } + + public byte[] toJsonBytes(Object value) { + PooledState entry = acquire(); + JsonState state = entry.state; + Utf8JsonWriter writer = state.utf8Writer; + try { + if (value == null) { + writer.writeNull(); + } else { + JsonTypeResolver resolver = state.typeResolver; + JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); + typeInfo.codec().writeUtf8(writer, value, resolver); + } + return writer.toJsonBytes(); + } finally { + writer.reset(); + release(entry); + } + } + + public T fromJson(String json, Class type) { + PooledState entry = acquire(); + JsonState state = entry.state; + try { + return castValue(readJavaStringValue(json, type, type, state), type); + } finally { + state.clearReaders(); + release(entry); + } + } + + /** Parses JSON using a generic type captured by {@link TypeRef}. */ + public T fromJson(String json, TypeRef typeRef) { + PooledState entry = acquire(); + JsonState state = entry.state; + try { + Object value = readJavaStringValue(json, typeRef.getType(), typeRef.getRawType(), state); + return castValue(value, typeRef); + } finally { + state.clearReaders(); + release(entry); + } + } + + public T fromJson(byte[] bytes, Class type) { + PooledState entry = acquire(); + JsonState state = entry.state; + try { + return castValue(readUtf8Value(state.utf8Reader(bytes, maxDepth), type, type, state), type); + } finally { + state.clearReaders(); + release(entry); + } + } + + /** Parses UTF-8 JSON bytes using a generic type captured by {@link TypeRef}. */ + public T fromJson(byte[] bytes, TypeRef typeRef) { + PooledState entry = acquire(); + JsonState state = entry.state; + try { + Object value = + readUtf8Value( + state.utf8Reader(bytes, maxDepth), typeRef.getType(), typeRef.getRawType(), state); + return castValue(value, typeRef); + } finally { + state.clearReaders(); + release(entry); + } + } + + boolean hasGeneratedWriter(Class type) { + PooledState entry = acquire(); + try { + return entry.state.typeResolver.getObjectCodec(type) instanceof GeneratedObjectCodec; + } finally { + release(entry); + } + } + + private PooledState acquire() { + PooledState entry = primarySlot.get(); + if (entry != null && primarySlot.compareAndSet(entry, null)) { + return entry; + } + int slotIndex = slotIndexForCurrentThread(); + entry = tryBorrowPreferredSlots(slotIndex); + if (entry != null) { + return entry; + } + return new PooledState(new JsonState(writeNullFields, sharedRegistry), TEMPORARY_SLOT); + } + + private void release(PooledState entry) { + if (entry.homeIndex == PRIMARY_SLOT) { + primarySlot.lazySet(entry); + } else if (entry.homeIndex >= 0) { + slots.lazySet(entry.homeIndex, entry); + } + } + + private PooledState tryBorrowPreferredSlots(int slotIndex) { + PooledState entry = tryBorrowSlot(slotIndex); + if (entry != null) { + return entry; + } + for (int i = 1; i < PREFERRED_SLOT_RETRIES; i++) { + entry = tryBorrowSlot(slotIndex); + if (entry != null) { + return entry; + } + } + int index = slotIndex + 1; + if (index == poolSize) { + index = 0; + } + for (int i = 1; i < poolSize; i++) { + entry = tryBorrowSlot(index); + if (entry != null) { + return entry; + } + index++; + if (index == poolSize) { + index = 0; + } + } + return null; + } + + private PooledState tryBorrowSlot(int index) { + return slots.getAndSet(index, null); + } + + private int slotIndexForCurrentThread() { + return Math.floorMod(spread(System.identityHashCode(Thread.currentThread())), poolSize); + } + + private static int spread(int hash) { + return hash ^ (hash >>> 16); + } + + private Object readJavaStringValue(String json, Type type, Class fallback, JsonState state) { + if (StringSerializer.isBytesBackedString()) { + byte coder = StringSerializer.getStringCoder(json); + if (StringSerializer.isLatin1Coder(coder)) { + return readLatin1Value(state.latin1Reader(json, maxDepth), type, fallback, state); + } + if (StringSerializer.isUtf16Coder(coder)) { + return readUtf16Value(state.utf16Reader(json, maxDepth), type, fallback, state); + } + } + return readUtf16Value(state.utf16Reader(json, maxDepth), type, fallback, state); + } + + private Object readLatin1Value( + Latin1JsonReader reader, Type type, Class fallback, JsonState state) { + JsonTypeResolver resolver = state.typeResolver; + JsonTypeInfo typeInfo = state.rootTypeInfo(type, fallback); + Object value = typeInfo.codec().readLatin1(reader, typeInfo, resolver); + reader.finish(); + return value; + } + + private Object readUtf16Value( + Utf16JsonReader reader, Type type, Class fallback, JsonState state) { + JsonTypeResolver resolver = state.typeResolver; + JsonTypeInfo typeInfo = state.rootTypeInfo(type, fallback); + Object value = typeInfo.codec().readUtf16(reader, typeInfo, resolver); + reader.finish(); + return value; + } + + private Object readUtf8Value( + Utf8JsonReader reader, Type type, Class fallback, JsonState state) { + JsonTypeResolver resolver = state.typeResolver; + JsonTypeInfo typeInfo = state.rootTypeInfo(type, fallback); + Object value = typeInfo.codec().readUtf8(reader, typeInfo, resolver); + reader.finish(); + return value; + } + + @SuppressWarnings("unchecked") + private static T castValue(Object value, Class type) { + return type.isPrimitive() ? (T) value : type.cast(value); + } + + @SuppressWarnings("unchecked") + private static T castValue(Object value, TypeRef typeRef) { + Class rawType = typeRef.getRawType(); + return rawType.isPrimitive() ? (T) value : (T) rawType.cast(value); + } + + private static final class PooledState { + private final JsonState state; + private final int homeIndex; + + private PooledState(JsonState state, int homeIndex) { + this.state = state; + this.homeIndex = homeIndex; + } + } + + private static final class JsonState { + private final Utf8JsonWriter utf8Writer; + private final StringJsonWriter stringWriter; + private final Utf8JsonReader utf8Reader; + private final Latin1JsonReader latin1Reader; + private final Utf16JsonReader utf16Reader; + private final JsonTypeResolver typeResolver; + private Type lastRootType; + private Class lastRootFallback; + private JsonTypeInfo lastRootInfo; + + private JsonState(boolean writeNullFields, JsonSharedRegistry sharedRegistry) { + utf8Writer = new Utf8JsonWriter(writeNullFields, new byte[INITIAL_BUFFER_SIZE]); + stringWriter = new StringJsonWriter(writeNullFields, new byte[INITIAL_BUFFER_SIZE]); + utf8Reader = new Utf8JsonReader(); + latin1Reader = new Latin1JsonReader(); + utf16Reader = new Utf16JsonReader(); + typeResolver = new JsonTypeResolver(sharedRegistry); + } + + private Latin1JsonReader latin1Reader(String input, int maxDepth) { + latin1Reader.reset(input); + latin1Reader.resetDepth(maxDepth); + return latin1Reader; + } + + private Utf16JsonReader utf16Reader(String input, int maxDepth) { + utf16Reader.reset(input); + utf16Reader.resetDepth(maxDepth); + return utf16Reader; + } + + private Utf8JsonReader utf8Reader(byte[] input, int maxDepth) { + utf8Reader.reset(input); + utf8Reader.resetDepth(maxDepth); + return utf8Reader; + } + + private void clearReaders() { + latin1Reader.clearDepth(); + utf16Reader.clearDepth(); + utf8Reader.clearDepth(); + latin1Reader.clear(); + utf16Reader.clear(); + utf8Reader.clear(); + } + + private JsonTypeInfo rootTypeInfo(Class type) { + return rootTypeInfo(type, type); + } + + private JsonTypeInfo rootTypeInfo(Type type, Class fallback) { + JsonTypeInfo typeInfo = lastRootInfo; + if (lastRootType == type && lastRootFallback == fallback && typeInfo != null) { + return typeInfo; + } + typeInfo = typeResolver.getTypeInfo(type, fallback); + lastRootType = type; + lastRootFallback = fallback; + lastRootInfo = typeInfo; + return typeInfo; + } + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java new file mode 100644 index 0000000000..17a5cb6c53 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java @@ -0,0 +1,64 @@ +/* + * 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; + +import org.apache.fory.json.codec.JsonCodec; +import org.apache.fory.json.resolver.CodecRegistry; + +/** Builder for {@link ForyJson}. */ +public final class ForyJsonBuilder { + private boolean writeNullFields; + private boolean codegenEnabled = true; + private int maxDepth = ForyJson.DEFAULT_MAX_DEPTH; + private final CodecRegistry codecRegistry = new CodecRegistry(); + + ForyJsonBuilder() {} + + /** Writes object fields with null values when enabled. */ + public ForyJsonBuilder writeNullFields(boolean writeNullFields) { + this.writeNullFields = writeNullFields; + return this; + } + + /** Enables runtime-generated writers for supported public-field classes. */ + public ForyJsonBuilder withCodegen(boolean codegenEnabled) { + this.codegenEnabled = codegenEnabled; + return this; + } + + /** Sets the maximum nested JSON object/array depth allowed while parsing. */ + public ForyJsonBuilder maxDepth(int maxDepth) { + if (maxDepth < 1) { + throw new IllegalArgumentException("maxDepth must be positive"); + } + this.maxDepth = maxDepth; + return this; + } + + /** Registers a custom JSON codec for {@code type}. */ + public ForyJsonBuilder registerCodec(Class type, JsonCodec codec) { + codecRegistry.register(type, codec); + return this; + } + + public ForyJson build() { + return new ForyJson(writeNullFields, codegenEnabled, maxDepth, codecRegistry); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonException.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonException.java new file mode 100644 index 0000000000..12c94c56cd --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonException.java @@ -0,0 +1,33 @@ +/* + * 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; + +import org.apache.fory.exception.ForyException; + +/** Runtime exception raised by Fory JSON readers, writers, and metadata builders. */ +public class ForyJsonException extends ForyException { + public ForyJsonException(String message) { + super(message); + } + + public ForyJsonException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JSONArray.java b/java/fory-json/src/main/java/org/apache/fory/json/JSONArray.java new file mode 100644 index 0000000000..76a3ec5a9c --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/JSONArray.java @@ -0,0 +1,36 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.Collection; + +/** Mutable JSON array container for dynamic JSON values. */ +public final class JSONArray extends ArrayList { + public JSONArray() { + // JSON input has no trusted array size; start from zero to avoid default capacity + // amplification for many tiny arrays. + super(0); + } + + public JSONArray(Collection values) { + super(values); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JSONObject.java b/java/fory-json/src/main/java/org/apache/fory/json/JSONObject.java new file mode 100644 index 0000000000..9ec87c2682 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/JSONObject.java @@ -0,0 +1,36 @@ +/* + * 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; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** Mutable JSON object container for dynamic JSON values. */ +public final class JSONObject extends LinkedHashMap { + public JSONObject() { + // JSON input has no trusted object size; start from zero to avoid default capacity + // amplification for many tiny objects. + super(0); + } + + public JSONObject(Map values) { + super(values); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonIgnore.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonIgnore.java new file mode 100644 index 0000000000..dd4a8800ca --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonIgnore.java @@ -0,0 +1,36 @@ +/* + * 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Ignores a public JSON field for reading, writing, or both directions. */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface JsonIgnore { + /** Whether this field is ignored when reading JSON into an object. */ + boolean ignoreRead() default true; + + /** Whether this field is ignored when writing an object as JSON. */ + boolean ignoreWrite() default true; +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/AbstractJsonCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/AbstractJsonCodec.java new file mode 100644 index 0000000000..af1639a5e1 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/AbstractJsonCodec.java @@ -0,0 +1,89 @@ +/* + * 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.json.ForyJsonException; +import org.apache.fory.json.meta.JsonFieldAccessor; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; + +public abstract class AbstractJsonCodec implements JsonCodec { + @Override + public final void write(JsonWriter writer, Object value, JsonTypeResolver resolver) { + if (value == null) { + writer.writeNull(); + } else { + writeNonNull(writer, value, resolver); + } + } + + @Override + public final void writeString(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + if (value == null) { + writer.writeNull(); + } else { + writeStringNonNull(writer, value, resolver); + } + } + + @Override + public final void writeUtf8(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + if (value == null) { + writer.writeNull(); + } else { + writeUtf8NonNull(writer, value, resolver); + } + } + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.peekNull()) { + reader.readNull(); + if (typeInfo.primitive()) { + throw new ForyJsonException("Cannot read null into primitive " + typeInfo.rawType()); + } + return null; + } + return readNonNull(reader, typeInfo, resolver); + } + + abstract void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver); + + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + abstract void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver); + + abstract Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver); + + final void readFieldDefault( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + JsonCodec.super.readField(reader, object, accessor, typeInfo, resolver); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java new file mode 100644 index 0000000000..9724abbd92 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -0,0 +1,749 @@ +/* + * 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.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; + +public abstract class ArrayCodec extends AbstractJsonCodec { + final Class componentType; + + ArrayCodec(Class componentType) { + this.componentType = componentType; + } + + public static ArrayCodec create(Class componentType, JsonTypeResolver resolver) { + if (componentType == int.class) { + return IntArrayCodec.INSTANCE; + } else if (componentType == long.class) { + return LongArrayCodec.INSTANCE; + } else if (componentType == boolean.class) { + return BooleanArrayCodec.INSTANCE; + } else if (componentType == short.class) { + return ShortArrayCodec.INSTANCE; + } else if (componentType == byte.class) { + return ByteArrayCodec.INSTANCE; + } else if (componentType == char.class) { + return CharArrayCodec.INSTANCE; + } else if (componentType == float.class) { + return FloatArrayCodec.INSTANCE; + } else if (componentType == double.class) { + return DoubleArrayCodec.INSTANCE; + } + return new ObjectArrayCodec(componentType, resolver.getTypeInfo(componentType, componentType)); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + public static final class IntArrayCodec extends ArrayCodec { + private static final IntArrayCodec INSTANCE = new IntArrayCodec(); + + private IntArrayCodec() { + super(int.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + int[] array = (int[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + writer.writeInt(array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new int[0]; + } + int[] values = new int[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readInt(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new int[0]; + } + int[] values = new int[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readIntValue(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new int[0]; + } + int[] values = new int[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readIntValue(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new int[0]; + } + int[] values = new int[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readIntValue(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class LongArrayCodec extends ArrayCodec { + private static final LongArrayCodec INSTANCE = new LongArrayCodec(); + + private LongArrayCodec() { + super(long.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + long[] array = (long[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + writer.writeLong(array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new long[0]; + } + long[] values = new long[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readLong(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new long[0]; + } + long[] values = new long[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readLongValue(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new long[0]; + } + long[] values = new long[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readLongValue(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new long[0]; + } + long[] values = new long[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readLongValue(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BooleanArrayCodec extends ArrayCodec { + private static final BooleanArrayCodec INSTANCE = new BooleanArrayCodec(); + + private BooleanArrayCodec() { + super(boolean.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + boolean[] array = (boolean[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + writer.writeBoolean(array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new boolean[0]; + } + boolean[] values = new boolean[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readBoolean(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new boolean[0]; + } + boolean[] values = new boolean[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readBooleanValue(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new boolean[0]; + } + boolean[] values = new boolean[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readBooleanValue(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new boolean[0]; + } + boolean[] values = new boolean[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readBooleanValue(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class ShortArrayCodec extends ArrayCodec { + private static final ShortArrayCodec INSTANCE = new ShortArrayCodec(); + + private ShortArrayCodec() { + super(short.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + short[] array = (short[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + writer.writeInt(array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new short[0]; + } + short[] values = new short[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = readShort(reader.readInt()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class ByteArrayCodec extends ArrayCodec { + private static final ByteArrayCodec INSTANCE = new ByteArrayCodec(); + + private ByteArrayCodec() { + super(byte.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + byte[] array = (byte[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + writer.writeInt(array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new byte[0]; + } + byte[] values = new byte[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = readByte(reader.readInt()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class CharArrayCodec extends ArrayCodec { + private static final CharArrayCodec INSTANCE = new CharArrayCodec(); + + private CharArrayCodec() { + super(char.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + char[] array = (char[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + writer.writeChar(array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new char[0]; + } + char[] values = new char[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = readChar(reader); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class FloatArrayCodec extends ArrayCodec { + private static final FloatArrayCodec INSTANCE = new FloatArrayCodec(); + + private FloatArrayCodec() { + super(float.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + float[] array = (float[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + writer.writeFloat(array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new float[0]; + } + float[] values = new float[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = Float.parseFloat(reader.readNumber()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class DoubleArrayCodec extends ArrayCodec { + private static final DoubleArrayCodec INSTANCE = new DoubleArrayCodec(); + + private DoubleArrayCodec() { + super(double.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + double[] array = (double[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + writer.writeDouble(array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new double[0]; + } + double[] values = new double[8]; + int size = 0; + do { + rejectNull(reader); + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = Double.parseDouble(reader.readNumber()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class ObjectArrayCodec extends ArrayCodec { + private final JsonTypeInfo elementTypeInfo; + private final JsonCodec elementCodec; + + private ObjectArrayCodec(Class componentType, JsonTypeInfo elementTypeInfo) { + super(componentType); + this.elementTypeInfo = elementTypeInfo; + elementCodec = elementTypeInfo.codec(); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Object[] array = (Object[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + elementCodec.write(writer, array[i], resolver); + } + writer.writeArrayEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + Object[] array = (Object[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + elementCodec.writeString(writer, array[i], resolver); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + Object[] array = (Object[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + elementCodec.writeUtf8(writer, array[i], resolver); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + List values = new ArrayList<>(0); + reader.expect('['); + if (!reader.consume(']')) { + do { + values.add(elementCodec.read(reader, elementTypeInfo, resolver)); + } while (reader.consume(',')); + reader.expect(']'); + } + reader.exitDepth(); + return toArray(values); + } + + private Object toArray(List values) { + Object array = Array.newInstance(componentType, values.size()); + for (int i = 0; i < values.size(); i++) { + Array.set(array, i, values.get(i)); + } + return array; + } + } + + private static void rejectNull(JsonReader reader) { + if (reader.tryReadNull()) { + throw new ForyJsonException("Cannot read null into primitive array element"); + } + } + + private static void rejectNull(Latin1JsonReader reader) { + if (reader.tryReadNullToken()) { + throw new ForyJsonException("Cannot read null into primitive array element"); + } + } + + private static void rejectNull(Utf16JsonReader reader) { + if (reader.tryReadNullToken()) { + throw new ForyJsonException("Cannot read null into primitive array element"); + } + } + + private static void rejectNull(Utf8JsonReader reader) { + if (reader.tryReadNullToken()) { + throw new ForyJsonException("Cannot read null into primitive array element"); + } + } + + private static short readShort(int value) { + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new ForyJsonException("Short overflow"); + } + return (short) value; + } + + private static byte readByte(int value) { + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + throw new ForyJsonException("Byte overflow"); + } + return (byte) value; + } + + private static char readChar(JsonReader reader) { + String value = reader.readString(); + if (value.length() != 1) { + throw new ForyJsonException("Expected one-character JSON string for char"); + } + return value.charAt(0); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java new file mode 100644 index 0000000000..3566caa8bc --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java @@ -0,0 +1,451 @@ +/* + * 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.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.apache.fory.annotation.Expose; +import org.apache.fory.annotation.Internal; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.annotation.JsonIgnore; +import org.apache.fory.json.meta.JsonFieldAccessor; +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.meta.JsonFieldTable; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.reflect.ObjectInstantiator; +import org.apache.fory.reflect.ObjectInstantiators; +import org.apache.fory.util.record.RecordInfo; +import org.apache.fory.util.record.RecordUtils; + +public abstract class BaseObjectCodec extends AbstractJsonCodec { + protected final Class type; + protected final JsonFieldInfo[] writeFields; + protected final JsonFieldInfo[] readFields; + protected final JsonFieldTable readTable; + protected final ObjectInstantiator instantiator; + protected final boolean record; + private final RecordInfo recordInfo; + private final Object[] recordFieldDefaults; + + protected BaseObjectCodec( + Class type, + JsonFieldInfo[] writeFields, + JsonFieldInfo[] readFields, + ObjectInstantiator instantiator) { + this.type = type; + this.writeFields = writeFields; + this.readFields = readFields; + readTable = new JsonFieldTable(readFields); + this.instantiator = instantiator; + record = RecordUtils.isRecord(type); + if (record) { + List fieldNames = new ArrayList<>(readFields.length); + for (JsonFieldInfo field : readFields) { + fieldNames.add(field.name()); + } + recordInfo = new RecordInfo(type, fieldNames); + recordFieldDefaults = recordFieldDefaults(type, readFields, recordInfo); + } else { + recordInfo = null; + recordFieldDefaults = null; + } + } + + public static ObjectCodec build(Class type) { + 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 writeExpose = hasWriteExpose(type); + boolean readExpose = hasReadExpose(type, record); + TreeMap builders = new TreeMap<>(); + for (Class current = type; + current != null && current != Object.class; + current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (!isEligibleField(field)) { + continue; + } + boolean write = includeWrite(field, writeExpose); + boolean read = (record || !Modifier.isFinal(modifiers)) && includeRead(field, readExpose); + if (!write && !read) { + continue; + } + FieldBuilder builder = new FieldBuilder(field.getName()); + if (write) { + builder.setWriteField(field); + } + if (read) { + builder.setReadField(field); + } + if (builders.put(field.getName(), builder) != null) { + throw new ForyJsonException("Duplicate JSON field " + field.getName()); + } + } + } + List writes = new ArrayList<>(); + List reads = new ArrayList<>(); + for (FieldBuilder builder : builders.values()) { + JsonFieldInfo field = builder.build(record); + if (builder.writeAccessor != null) { + writes.add(field); + } + if (builder.readField != null) { + reads.add(field); + } + } + JsonFieldInfo[] writeArray = writes.toArray(new JsonFieldInfo[0]); + JsonFieldInfo[] readArray = reads.toArray(new JsonFieldInfo[0]); + for (int i = 0; i < readArray.length; i++) { + readArray[i].setReadIndex(i); + } + return new ObjectCodec( + type, writeArray, readArray, ObjectInstantiators.createObjectInstantiator(type)); + } + + public final Class type() { + return type; + } + + public final JsonFieldInfo[] writeFields() { + return writeFields; + } + + public final JsonFieldInfo[] readFields() { + return readFields; + } + + public final JsonFieldTable readTable() { + return readTable; + } + + public final boolean isRecord() { + return record; + } + + public final void resolveTypes(JsonTypeResolver typeResolver) { + for (JsonFieldInfo field : writeFields) { + field.resolveTypes(typeResolver); + } + for (JsonFieldInfo field : readFields) { + field.resolveTypes(typeResolver); + } + } + + public final Object newInstance() { + return instantiator.newInstance(); + } + + @Internal + public final Object[] newRecordFieldValues() { + return Arrays.copyOf(recordFieldDefaults, recordFieldDefaults.length); + } + + @Internal + public final Object newRecord(Object[] values) { + Object[] arguments = RecordUtils.remapping(recordInfo, values); + Object object = instantiator.newInstanceWithArguments(arguments); + Arrays.fill(recordInfo.getRecordComponents(), null); + return object; + } + + @Override + final void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeObject(writer, value, resolver); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writeStringObject(writer, value, resolver); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeUtf8Object(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + if (record) { + Object object = readRecord(reader, resolver); + reader.exitDepth(); + return object; + } + Object object = newInstance(); + reader.expect('{'); + if (reader.consume('}')) { + reader.exitDepth(); + return object; + } + do { + JsonFieldInfo field = reader.readField(readTable); + reader.expect(':'); + if (field == null) { + reader.skipValue(); + } else { + field.read(reader, object, resolver); + } + } while (reader.consume(',')); + reader.expect('}'); + reader.exitDepth(); + return object; + } + + @Internal + public Object readLatin1NonNull( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return readNonNull(reader, typeInfo, resolver); + } + + @Internal + public Object readUtf16NonNull( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return readNonNull(reader, typeInfo, resolver); + } + + @Internal + public Object readUtf8NonNull( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return readNonNull(reader, typeInfo, resolver); + } + + private Object readRecord(JsonReader reader, JsonTypeResolver resolver) { + 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.readValue(reader, resolver); + } + } while (reader.consume(',')); + reader.expect('}'); + } + return newRecord(values); + } + + protected final void writeObject(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeObjectStart(); + int written = 0; + JsonFieldInfo[] fields = writeFields; + int length = fields.length; + int i = 0; + while (i + 4 <= length) { + if (fields[i++].write(writer, value, resolver, written)) { + written++; + } + if (fields[i++].write(writer, value, resolver, written)) { + written++; + } + if (fields[i++].write(writer, value, resolver, written)) { + written++; + } + if (fields[i++].write(writer, value, resolver, written)) { + written++; + } + } + while (i < length) { + if (fields[i++].write(writer, value, resolver, written)) { + written++; + } + } + writer.writeObjectEnd(); + } + + protected final void writeStringObject( + StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeObjectStart(); + int written = 0; + JsonFieldInfo[] fields = writeFields; + int length = fields.length; + int i = 0; + while (i + 4 <= length) { + if (fields[i++].writeString(writer, value, resolver, written)) { + written++; + } + if (fields[i++].writeString(writer, value, resolver, written)) { + written++; + } + if (fields[i++].writeString(writer, value, resolver, written)) { + written++; + } + if (fields[i++].writeString(writer, value, resolver, written)) { + written++; + } + } + while (i < length) { + if (fields[i++].writeString(writer, value, resolver, written)) { + written++; + } + } + writer.writeObjectEnd(); + } + + protected final void writeUtf8Object( + Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeObjectStart(); + int written = 0; + JsonFieldInfo[] fields = writeFields; + int length = fields.length; + int i = 0; + while (i + 4 <= length) { + if (fields[i++].writeUtf8(writer, value, resolver, written)) { + written++; + } + if (fields[i++].writeUtf8(writer, value, resolver, written)) { + written++; + } + if (fields[i++].writeUtf8(writer, value, resolver, written)) { + written++; + } + if (fields[i++].writeUtf8(writer, value, resolver, written)) { + written++; + } + } + while (i < length) { + if (fields[i++].writeUtf8(writer, value, resolver, written)) { + written++; + } + } + writer.writeObjectEnd(); + } + + private static boolean hasWriteExpose(Class type) { + for (Class current = type; + current != null && current != Object.class; + current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (isEligibleField(field) && field.isAnnotationPresent(Expose.class)) { + return true; + } + } + } + return false; + } + + private static boolean hasReadExpose(Class type, boolean record) { + for (Class current = type; + current != null && current != Object.class; + current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (isEligibleField(field) + && (record || !Modifier.isFinal(field.getModifiers())) + && field.isAnnotationPresent(Expose.class)) { + return true; + } + } + } + return false; + } + + private static boolean isEligibleField(Field field) { + int modifiers = field.getModifiers(); + return !Modifier.isStatic(modifiers) + && !Modifier.isTransient(modifiers) + && !field.isSynthetic(); + } + + private static boolean includeWrite(Field field, boolean exposeMode) { + return include(field, exposeMode, true); + } + + private static boolean includeRead(Field field, boolean exposeMode) { + return include(field, exposeMode, false); + } + + private static boolean include(Field field, boolean exposeMode, boolean write) { + JsonIgnore ignore = field.getAnnotation(JsonIgnore.class); + boolean ignored = ignore != null && (write ? ignore.ignoreWrite() : ignore.ignoreRead()); + boolean exposed = field.isAnnotationPresent(Expose.class); + if (ignored && exposed) { + throw new ForyJsonException("JSON field cannot be both exposed and ignored: " + field); + } + if (ignored) { + return false; + } + return !exposeMode || exposed; + } + + private static Object[] recordFieldDefaults( + Class type, JsonFieldInfo[] readFields, RecordInfo recordInfo) { + Object[] defaults = new Object[readFields.length]; + Object[] componentDefaults = recordInfo.getRecordComponentsDefaultValues(); + Map componentIndexes = RecordUtils.buildFieldToComponentMapping(type); + for (int i = 0; i < readFields.length; i++) { + Integer componentIndex = componentIndexes.get(readFields[i].name()); + defaults[i] = componentIndex == null ? null : componentDefaults[componentIndex.intValue()]; + } + return defaults; + } + + private static final class FieldBuilder { + private final String name; + private Field writeField; + private Field readField; + private JsonFieldAccessor writeAccessor; + private JsonFieldAccessor readAccessor; + + private FieldBuilder(String name) { + this.name = name; + } + + private void setWriteField(Field field) { + if (writeField != null) { + throw new ForyJsonException("Duplicate public JSON field " + name); + } + writeField = field; + } + + private void setReadField(Field field) { + if (readField != null) { + throw new ForyJsonException("Duplicate public JSON field " + name); + } + readField = field; + } + + private JsonFieldInfo build(boolean record) { + writeAccessor = writeField == null ? null : JsonFieldAccessor.forField(writeField); + readAccessor = readField == null || record ? null : JsonFieldAccessor.forField(readField); + return new JsonFieldInfo(name, writeField, readField, writeAccessor, readAccessor); + } + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CodecUtils.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CodecUtils.java new file mode 100644 index 0000000000..df7383df89 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CodecUtils.java @@ -0,0 +1,93 @@ +/* + * 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.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import org.apache.fory.collection.Tuple2; +import org.apache.fory.reflect.TypeRef; + +public final class CodecUtils { + private CodecUtils() {} + + public static Class rawType(Type type, Class fallback) { + if (type instanceof Class) { + Class rawType = (Class) type; + if (rawType == Object.class && fallback != null) { + return fallback; + } + return rawType; + } + if (type instanceof ParameterizedType) { + Type rawType = ((ParameterizedType) type).getRawType(); + if (rawType instanceof Class) { + return (Class) rawType; + } + } + return fallback == null ? Object.class : fallback; + } + + public static Type elementType(Type type) { + if (type instanceof ParameterizedType) { + Type[] arguments = ((ParameterizedType) type).getActualTypeArguments(); + if (arguments.length == 1) { + return arguments[0]; + } + } + return Object.class; + } + + public static Type mapValueType(Type type) { + if (type instanceof ParameterizedType) { + Type[] arguments = ((ParameterizedType) type).getActualTypeArguments(); + if (arguments.length == 2) { + return arguments[1]; + } + } + return Object.class; + } + + public static Type mapKeyType(Type type) { + if (type instanceof ParameterizedType) { + Type[] arguments = ((ParameterizedType) type).getActualTypeArguments(); + if (arguments.length == 2) { + return arguments[0]; + } + } + return String.class; + } + + public static TypeRef elementTypeRef(TypeRef typeRef) { + List> arguments = typeRef.getTypeArguments(); + if (arguments.size() == 1) { + return arguments.get(0); + } + return TypeRef.of(Object.class); + } + + public static Tuple2, TypeRef> mapKeyValueTypeRefs(TypeRef typeRef) { + List> arguments = typeRef.getTypeArguments(); + if (arguments.size() == 2) { + return Tuple2.of(arguments.get(0), arguments.get(1)); + } + return Tuple2.of(TypeRef.of(String.class), TypeRef.of(Object.class)); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java new file mode 100644 index 0000000000..dfd70c8346 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -0,0 +1,952 @@ +/* + * 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.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.NavigableSet; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.JSONArray; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.reflect.TypeRef; + +public abstract class CollectionCodec extends AbstractJsonCodec { + private static final Class UNTYPED_COLLECTION = ArrayList.class; + + private final TypeRef typeRef; + private final CollectionFactory factory; + + CollectionCodec(TypeRef typeRef, CollectionFactory factory) { + this.typeRef = typeRef; + this.factory = factory; + } + + public static CollectionCodec create( + Class rawType, TypeRef typeRef, JsonTypeResolver resolver) { + TypeRef elementTypeRef = CodecUtils.elementTypeRef(typeRef); + Type elementType = elementTypeRef.getType(); + Class elementRawType = CodecUtils.rawType(elementType, Object.class); + CollectionFactory factory = collectionFactory(rawType, elementRawType); + if (elementRawType == String.class) { + return new StringCollectionCodec(typeRef, factory); + } + if (elementRawType == Boolean.class || elementRawType == boolean.class) { + return new BooleanCollectionCodec(typeRef, factory); + } + if (elementRawType == Integer.class || elementRawType == int.class) { + return new IntCollectionCodec(typeRef, factory); + } + if (elementRawType == Long.class || elementRawType == long.class) { + return new LongCollectionCodec(typeRef, factory); + } + if (elementRawType == Short.class || elementRawType == short.class) { + return new ShortCollectionCodec(typeRef, factory); + } + if (elementRawType == Byte.class || elementRawType == byte.class) { + return new ByteCollectionCodec(typeRef, factory); + } + if (elementRawType == Float.class || elementRawType == float.class) { + return new FloatCollectionCodec(typeRef, factory); + } + if (elementRawType == Double.class || elementRawType == double.class) { + return new DoubleCollectionCodec(typeRef, factory); + } + if (elementRawType == BigInteger.class) { + return new BigIntegerCollectionCodec(typeRef, factory); + } + if (elementRawType == BigDecimal.class) { + return new BigDecimalCollectionCodec(typeRef, factory); + } + JsonTypeInfo elementTypeInfo = resolver.getTypeInfo(elementType, elementRawType); + JsonCodec elementCodec = elementTypeInfo.codec(); + if (elementCodec instanceof BaseObjectCodec) { + return new ObjectCollectionCodec( + typeRef, factory, elementTypeInfo, (BaseObjectCodec) elementCodec); + } + return new GenericCollectionCodec(typeRef, factory, elementTypeInfo, elementCodec); + } + + final TypeRef typeRef() { + return typeRef; + } + + static Collection readUntyped(JsonReader reader, JsonTypeResolver resolver) { + JsonTypeInfo elementInfo = resolver.getTypeInfo(Object.class, Object.class); + Collection collection = new JSONArray(); + readGeneric(reader, collection, elementInfo, elementInfo.codec(), resolver); + return collection; + } + + final Collection newCollection() { + // JSON arrays do not carry a trusted size. Avoid speculative backing-array preallocation in + // parser hot paths; it can waste memory for small arrays and amplify untrusted input. + return factory.newCollection(); + } + + public abstract Object readLatin1NonNull( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver); + + public abstract Object readUtf16NonNull( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver); + + public abstract Object readUtf8NonNull( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver); + + private static void readGeneric( + JsonReader reader, + Collection collection, + JsonTypeInfo elementInfo, + JsonCodec elementCodec, + JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (!reader.consume(']')) { + do { + collection.add(elementCodec.read(reader, elementInfo, resolver)); + } while (reader.consumeCommaOrEndArray()); + } + reader.exitDepth(); + } + + @SuppressWarnings("unchecked") + private static CollectionFactory collectionFactory(Class rawType, Class elementRawType) { + if (rawType == JSONArray.class) { + return JSONArray::new; + } + if (rawType == EnumSet.class) { + if (!elementRawType.isEnum()) { + throw new ForyJsonException("EnumSet requires an enum element type"); + } + Class enumType = (Class) elementRawType; + return () -> (Collection) EnumSet.noneOf(enumType); + } + if (rawType == UNTYPED_COLLECTION || rawType.isInterface()) { + if (NavigableSet.class.isAssignableFrom(rawType) + || SortedSet.class.isAssignableFrom(rawType)) { + return TreeSet::new; + } + if (Set.class.isAssignableFrom(rawType)) { + return LinkedHashSet::new; + } + if (Queue.class.isAssignableFrom(rawType)) { + return ArrayDeque::new; + } + return () -> new ArrayList<>(0); + } + return () -> { + try { + return (Collection) rawType.newInstance(); + } catch (ReflectiveOperationException e) { + throw new ForyJsonException("Cannot create collection " + rawType, e); + } + }; + } + + private interface CollectionFactory { + Collection newCollection(); + } + + public abstract static class DirectCollectionCodec extends CollectionCodec { + DirectCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + final Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expect('['); + if (!reader.consume(']')) { + do { + collection.add(readNullableElement(reader)); + } while (reader.consumeCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + + @Override + public final Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readLatin1NonNull(reader, typeInfo, resolver); + } + + @Override + public final Object readLatin1NonNull( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expectNextToken('['); + if (!reader.consumeNextToken(']')) { + do { + collection.add(readNullableLatin1Element(reader)); + } while (reader.consumeNextCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + + @Override + public final Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readUtf16NonNull(reader, typeInfo, resolver); + } + + @Override + public final Object readUtf16NonNull( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expectNextToken('['); + if (!reader.consumeNextToken(']')) { + do { + collection.add(readNullableUtf16Element(reader)); + } while (reader.consumeNextCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + + @Override + public final Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readUtf8NonNull(reader, typeInfo, resolver); + } + + @Override + public final Object readUtf8NonNull( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expectNextToken('['); + if (!reader.consumeNextToken(']')) { + do { + collection.add(readNullableUtf8Element(reader)); + } while (reader.consumeNextCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + + abstract Object readElement(JsonReader reader); + + Object readNullableElement(JsonReader reader) { + return reader.tryReadNull() ? null : readElement(reader); + } + + Object readLatin1Element(Latin1JsonReader reader) { + return readElement(reader); + } + + Object readNullableLatin1Element(Latin1JsonReader reader) { + return reader.tryReadNextNullToken() ? null : readLatin1Element(reader); + } + + Object readUtf16Element(Utf16JsonReader reader) { + return readElement(reader); + } + + Object readNullableUtf16Element(Utf16JsonReader reader) { + return reader.tryReadNextNullToken() ? null : readUtf16Element(reader); + } + + Object readUtf8Element(Utf8JsonReader reader) { + return readElement(reader); + } + + Object readNullableUtf8Element(Utf8JsonReader reader) { + return reader.tryReadNextNullToken() ? null : readUtf8Element(reader); + } + } + + public static final class GenericCollectionCodec extends CollectionCodec { + private final JsonTypeInfo elementTypeInfo; + private final JsonCodec elementCodec; + + private GenericCollectionCodec( + TypeRef typeRef, + CollectionFactory factory, + JsonTypeInfo elementTypeInfo, + JsonCodec elementCodec) { + super(typeRef, factory); + this.elementTypeInfo = elementTypeInfo; + this.elementCodec = elementCodec; + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + elementCodec.write(writer, element, resolver); + } + writer.writeArrayEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + elementCodec.writeString(writer, element, resolver); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + elementCodec.writeUtf8(writer, element, resolver); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + Collection collection = newCollection(); + readGeneric(reader, collection, elementTypeInfo, elementCodec, resolver); + return collection; + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readLatin1NonNull(reader, typeInfo, resolver); + } + + @Override + public Object readLatin1NonNull( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expectNextToken('['); + if (!reader.consumeNextToken(']')) { + do { + collection.add(elementCodec.readLatin1(reader, elementTypeInfo, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readUtf16NonNull(reader, typeInfo, resolver); + } + + @Override + public Object readUtf16NonNull( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expectNextToken('['); + if (!reader.consumeNextToken(']')) { + do { + collection.add(elementCodec.readUtf16(reader, elementTypeInfo, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readUtf8NonNull(reader, typeInfo, resolver); + } + + @Override + public Object readUtf8NonNull( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expectNextToken('['); + if (!reader.consumeNextToken(']')) { + do { + collection.add(elementCodec.readUtf8(reader, elementTypeInfo, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + } + + public static final class ObjectCollectionCodec extends CollectionCodec { + private final JsonTypeInfo elementTypeInfo; + private final BaseObjectCodec elementCodec; + + private ObjectCollectionCodec( + TypeRef typeRef, + CollectionFactory factory, + JsonTypeInfo elementTypeInfo, + BaseObjectCodec elementCodec) { + super(typeRef, factory); + this.elementTypeInfo = elementTypeInfo; + this.elementCodec = elementCodec; + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeNonNull(writer, element, resolver); + } + } + writer.writeArrayEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeStringNonNull(writer, element, resolver); + } + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeUtf8NonNull(writer, element, resolver); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expect('['); + if (!reader.consume(']')) { + do { + collection.add( + reader.tryReadNull() + ? null + : elementCodec.readNonNull(reader, elementTypeInfo, resolver)); + } while (reader.consumeCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readLatin1NonNull(reader, typeInfo, resolver); + } + + @Override + public Object readLatin1NonNull( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expectNextToken('['); + if (!reader.consumeNextToken(']')) { + do { + collection.add( + reader.tryReadNextNullToken() + ? null + : elementCodec.readLatin1NonNull(reader, elementTypeInfo, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readUtf16NonNull(reader, typeInfo, resolver); + } + + @Override + public Object readUtf16NonNull( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expectNextToken('['); + if (!reader.consumeNextToken(']')) { + do { + collection.add( + reader.tryReadNextNullToken() + ? null + : elementCodec.readUtf16NonNull(reader, elementTypeInfo, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readUtf8NonNull(reader, typeInfo, resolver); + } + + @Override + public Object readUtf8NonNull( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Collection collection = newCollection(); + reader.expectNextToken('['); + if (!reader.consumeNextToken(']')) { + do { + collection.add( + reader.tryReadNextNullToken() + ? null + : elementCodec.readUtf8NonNull(reader, elementTypeInfo, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + } + reader.exitDepth(); + return collection; + } + } + + public static final class StringCollectionCodec extends DirectCollectionCodec { + private StringCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + writer.writeString((String) element); + } + } + writer.writeArrayEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeStringElement(index++, (String) element); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeStringElement(index++, (String) element); + } + writer.writeArrayEnd(); + } + + @Override + Object readElement(JsonReader reader) { + return reader.readString(); + } + + @Override + Object readNullableElement(JsonReader reader) { + return reader.readNullableString(); + } + + @Override + Object readNullableLatin1Element(Latin1JsonReader reader) { + return reader.readNextNullableString(); + } + + @Override + Object readNullableUtf16Element(Utf16JsonReader reader) { + return reader.readNextNullableString(); + } + + @Override + Object readNullableUtf8Element(Utf8JsonReader reader) { + return reader.readNextNullableString(); + } + } + + public static final class BooleanCollectionCodec extends DirectCollectionCodec { + private BooleanCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + writer.writeBoolean((boolean) element); + } + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + writer.writeBoolean((boolean) element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readElement(JsonReader reader) { + return reader.readBoolean(); + } + + @Override + Object readLatin1Element(Latin1JsonReader reader) { + return reader.readNextBooleanValue(); + } + + @Override + Object readUtf16Element(Utf16JsonReader reader) { + return reader.readNextBooleanValue(); + } + + @Override + Object readUtf8Element(Utf8JsonReader reader) { + return reader.readNextBooleanValue(); + } + } + + public abstract static class NumberCollectionCodec extends DirectCollectionCodec { + NumberCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + final void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + writeNumber(writer, element); + } + } + writer.writeArrayEnd(); + } + + @Override + final void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeArrayStart(); + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + writeNumber(writer, element); + } + } + writer.writeArrayEnd(); + } + + abstract void writeNumber(JsonWriter writer, Object value); + } + + public static final class IntCollectionCodec extends NumberCollectionCodec { + private IntCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeInt((int) value); + } + + @Override + Object readElement(JsonReader reader) { + return reader.readInt(); + } + + @Override + Object readLatin1Element(Latin1JsonReader reader) { + return reader.readNextIntValue(); + } + + @Override + Object readUtf16Element(Utf16JsonReader reader) { + return reader.readNextIntValue(); + } + + @Override + Object readUtf8Element(Utf8JsonReader reader) { + return reader.readNextIntValue(); + } + } + + public static final class LongCollectionCodec extends NumberCollectionCodec { + private LongCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeLong((long) value); + } + + @Override + Object readElement(JsonReader reader) { + return reader.readLong(); + } + + @Override + Object readLatin1Element(Latin1JsonReader reader) { + return reader.readNextLongValue(); + } + + @Override + Object readUtf16Element(Utf16JsonReader reader) { + return reader.readNextLongValue(); + } + + @Override + Object readUtf8Element(Utf8JsonReader reader) { + return reader.readNextLongValue(); + } + } + + public static final class ShortCollectionCodec extends NumberCollectionCodec { + private ShortCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeInt((short) value); + } + + @Override + Object readElement(JsonReader reader) { + return readShort(reader.readInt()); + } + + @Override + Object readLatin1Element(Latin1JsonReader reader) { + return readShort(reader.readNextIntValue()); + } + + @Override + Object readUtf16Element(Utf16JsonReader reader) { + return readShort(reader.readNextIntValue()); + } + + @Override + Object readUtf8Element(Utf8JsonReader reader) { + return readShort(reader.readNextIntValue()); + } + } + + public static final class ByteCollectionCodec extends NumberCollectionCodec { + private ByteCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeInt((byte) value); + } + + @Override + Object readElement(JsonReader reader) { + return readByte(reader.readInt()); + } + + @Override + Object readLatin1Element(Latin1JsonReader reader) { + return readByte(reader.readNextIntValue()); + } + + @Override + Object readUtf16Element(Utf16JsonReader reader) { + return readByte(reader.readNextIntValue()); + } + + @Override + Object readUtf8Element(Utf8JsonReader reader) { + return readByte(reader.readNextIntValue()); + } + } + + public static final class FloatCollectionCodec extends NumberCollectionCodec { + private FloatCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeFloat((float) value); + } + + @Override + Object readElement(JsonReader reader) { + return Float.parseFloat(reader.readNumber()); + } + } + + public static final class DoubleCollectionCodec extends NumberCollectionCodec { + private DoubleCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeDouble((double) value); + } + + @Override + Object readElement(JsonReader reader) { + return Double.parseDouble(reader.readNumber()); + } + } + + public static final class BigIntegerCollectionCodec extends NumberCollectionCodec { + private BigIntegerCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeNumber(value.toString()); + } + + @Override + Object readElement(JsonReader reader) { + return new BigInteger(reader.readNumber()); + } + } + + public static final class BigDecimalCollectionCodec extends NumberCollectionCodec { + private BigDecimalCollectionCodec(TypeRef typeRef, CollectionFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeNumber(value.toString()); + } + + @Override + Object readElement(JsonReader reader) { + return new BigDecimal(reader.readNumber()); + } + } + + private static short readShort(int value) { + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new ForyJsonException("Short overflow"); + } + return (short) value; + } + + private static byte readByte(int value) { + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + throw new ForyJsonException("Byte overflow"); + } + return (byte) value; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedObjectCodec.java new file mode 100644 index 0000000000..fa4dc59b35 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedObjectCodec.java @@ -0,0 +1,124 @@ +/* + * 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.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Latin1ObjectReader; +import org.apache.fory.json.reader.ObjectReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf16ObjectReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.reader.Utf8ObjectReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.StringObjectWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.json.writer.Utf8ObjectWriter; + +public final class GeneratedObjectCodec extends BaseObjectCodec { + private final StringObjectWriter stringWriter; + private final Utf8ObjectWriter utf8Writer; + private final ObjectReader reader; + private final Latin1ObjectReader latin1Reader; + private final Utf16ObjectReader utf16Reader; + private final Utf8ObjectReader utf8Reader; + + GeneratedObjectCodec(ObjectCodec base, ObjectCodecs codecs) { + super(base.type, base.writeFields, base.readFields, base.instantiator); + stringWriter = codecs.stringWriter(); + utf8Writer = codecs.utf8Writer(); + reader = codecs.reader(); + latin1Reader = codecs.latin1Reader(); + utf16Reader = codecs.utf16Reader(); + utf8Reader = codecs.utf8Reader(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + stringWriter.writeString(writer, value, resolver); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + utf8Writer.writeUtf8(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader input, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + input.enterDepth(); + Object object = reader.read(input, this, resolver); + input.exitDepth(); + return object; + } + + @Override + public Object readLatin1( + Latin1JsonReader input, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (input.tryReadNullToken()) { + return null; + } + return readLatin1NonNull(input, typeInfo, resolver); + } + + @Override + public Object readLatin1NonNull( + Latin1JsonReader input, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + input.enterDepth(); + Object object = latin1Reader.readLatin1(input, this, resolver); + input.exitDepth(); + return object; + } + + @Override + public Object readUtf16(Utf16JsonReader input, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (input.tryReadNullToken()) { + return null; + } + return readUtf16NonNull(input, typeInfo, resolver); + } + + @Override + public Object readUtf16NonNull( + Utf16JsonReader input, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + input.enterDepth(); + Object object = utf16Reader.readUtf16(input, this, resolver); + input.exitDepth(); + return object; + } + + @Override + public Object readUtf8(Utf8JsonReader input, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (input.tryReadNullToken()) { + return null; + } + return readUtf8NonNull(input, typeInfo, resolver); + } + + @Override + public Object readUtf8NonNull( + Utf8JsonReader input, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + input.enterDepth(); + Object object = utf8Reader.readUtf8(input, this, resolver); + input.exitDepth(); + return object; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonCodec.java new file mode 100644 index 0000000000..07bfaefb0f --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonCodec.java @@ -0,0 +1,65 @@ +/* + * 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.json.meta.JsonFieldAccessor; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; + +/** JSON read/write behavior for one resolved Java type binding. */ +public interface JsonCodec { + void write(JsonWriter writer, Object value, JsonTypeResolver resolver); + + void writeString(StringJsonWriter writer, Object value, JsonTypeResolver resolver); + + void writeUtf8(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver); + + Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver); + + default Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return read(reader, typeInfo, resolver); + } + + default Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return read(reader, typeInfo, resolver); + } + + default Object readUtf8(Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return read(reader, typeInfo, resolver); + } + + default void readField( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + accessor.putObject(object, read(reader, typeInfo, resolver)); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java new file mode 100644 index 0000000000..54ab7628af --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java @@ -0,0 +1,1035 @@ +/* + * 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.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NavigableMap; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; +import org.apache.fory.collection.Tuple2; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.JSONObject; +import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.reflect.TypeRef; + +public abstract class MapCodec extends AbstractJsonCodec { + private static final Class UNTYPED_MAP = LinkedHashMap.class; + + private final TypeRef typeRef; + private final MapFactory factory; + + MapCodec(TypeRef typeRef, MapFactory factory) { + this.typeRef = typeRef; + this.factory = factory; + } + + public static MapCodec create(Class rawType, TypeRef typeRef, JsonTypeResolver resolver) { + Tuple2, TypeRef> keyValueTypeRefs = CodecUtils.mapKeyValueTypeRefs(typeRef); + Type keyType = keyValueTypeRefs.f0.getType(); + Class keyRawType = CodecUtils.rawType(keyType, String.class); + Type valueType = keyValueTypeRefs.f1.getType(); + Class valueRawType = CodecUtils.rawType(valueType, Object.class); + MapFactory factory = mapFactory(rawType, keyRawType); + if (keyRawType == String.class || keyRawType == Object.class) { + if (valueRawType == String.class) { + return new StringStringMapCodec(typeRef, factory); + } + if (valueRawType == Boolean.class || valueRawType == boolean.class) { + return new StringBooleanMapCodec(typeRef, factory); + } + if (valueRawType == Integer.class || valueRawType == int.class) { + return new StringIntMapCodec(typeRef, factory); + } + if (valueRawType == Long.class || valueRawType == long.class) { + return new StringLongMapCodec(typeRef, factory); + } + if (valueRawType == Short.class || valueRawType == short.class) { + return new StringShortMapCodec(typeRef, factory); + } + if (valueRawType == Byte.class || valueRawType == byte.class) { + return new StringByteMapCodec(typeRef, factory); + } + if (valueRawType == Float.class || valueRawType == float.class) { + return new StringFloatMapCodec(typeRef, factory); + } + if (valueRawType == Double.class || valueRawType == double.class) { + return new StringDoubleMapCodec(typeRef, factory); + } + if (valueRawType == BigInteger.class) { + return new StringBigIntegerMapCodec(typeRef, factory); + } + if (valueRawType == BigDecimal.class) { + return new StringBigDecimalMapCodec(typeRef, factory); + } + } + if (valueRawType == String.class && isNumericKey(keyRawType)) { + return new NumberStringMapCodec(typeRef, factory, MapKeyCodec.of(keyRawType)); + } + return new GenericMapCodec( + typeRef, factory, MapKeyCodec.of(keyRawType), valueType, valueRawType, resolver); + } + + final TypeRef typeRef() { + return typeRef; + } + + static Map readUntyped(JsonReader reader, JsonTypeResolver resolver) { + JsonTypeInfo valueInfo = resolver.getTypeInfo(Object.class, Object.class); + Map map = (Map) (Map) new JSONObject(); + readGeneric(reader, map, MapKeyCodec.STRING, valueInfo, valueInfo.codec(), resolver); + return map; + } + + final Map newMap() { + return factory.newMap(); + } + + private static void writeKey(JsonWriter writer, Object key, MapKeyCodec keyCodec) { + keyCodec.writeName(writer, key); + } + + private static void readGeneric( + JsonReader reader, + Map map, + MapKeyCodec keyCodec, + JsonTypeInfo valueInfo, + JsonCodec valueCodec, + JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('{'); + if (!reader.consume('}')) { + do { + Object key = keyCodec.readName(reader); + reader.expect(':'); + map.put(key, valueCodec.read(reader, valueInfo, resolver)); + } while (reader.consume(',')); + reader.expect('}'); + } + reader.exitDepth(); + } + + @SuppressWarnings("unchecked") + private static MapFactory mapFactory(Class rawType, Class keyRawType) { + if (rawType == JSONObject.class) { + return () -> (Map) (Map) new JSONObject(); + } + if (rawType == EnumMap.class) { + if (!keyRawType.isEnum()) { + throw new ForyJsonException("EnumMap requires an enum key type"); + } + return () -> new EnumMap(keyRawType); + } + if (rawType == UNTYPED_MAP || rawType.isInterface()) { + if (ConcurrentMap.class.isAssignableFrom(rawType)) { + if (NavigableMap.class.isAssignableFrom(rawType) + || SortedMap.class.isAssignableFrom(rawType)) { + return ConcurrentSkipListMap::new; + } + return ConcurrentHashMap::new; + } + if (NavigableMap.class.isAssignableFrom(rawType) + || SortedMap.class.isAssignableFrom(rawType)) { + return TreeMap::new; + } + return () -> new LinkedHashMap<>(0); + } + return () -> { + try { + return (Map) rawType.newInstance(); + } catch (ReflectiveOperationException e) { + throw new ForyJsonException("Cannot create map " + rawType, e); + } + }; + } + + private static boolean isNumericKey(Class type) { + return type == int.class + || type == Integer.class + || type == long.class + || type == Long.class + || type == short.class + || type == Short.class + || type == byte.class + || type == Byte.class; + } + + private interface MapFactory { + Map newMap(); + } + + public static final class GenericMapCodec extends MapCodec { + private final MapKeyCodec keyCodec; + private final JsonTypeInfo valueTypeInfo; + private final JsonCodec valueCodec; + + private GenericMapCodec( + TypeRef typeRef, + MapFactory factory, + MapKeyCodec keyCodec, + Type valueType, + Class valueRawType, + JsonTypeResolver resolver) { + super(typeRef, factory); + this.keyCodec = keyCodec; + valueTypeInfo = resolver.getTypeInfo(valueType, valueRawType); + valueCodec = valueTypeInfo.codec(); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeObjectStart(); + int index = 0; + for (Map.Entry entry : ((Map) value).entrySet()) { + writer.writeComma(index++); + writeKey(writer, entry.getKey(), keyCodec); + valueCodec.write(writer, entry.getValue(), resolver); + } + writer.writeObjectEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + Map map = newMap(); + readGeneric(reader, map, keyCodec, valueTypeInfo, valueCodec, resolver); + return map; + } + } + + public abstract static class StringKeyMapCodec extends MapCodec { + StringKeyMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + public final Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + Map map = newMap(); + reader.expectNextToken('{'); + if (!reader.consumeNextToken('}')) { + do { + String key = reader.readString(); + reader.expectNextToken(':'); + map.put(key, reader.tryReadNullToken() ? null : readLatin1Value(reader)); + } while (reader.consumeNextToken(',')); + reader.expectNextToken('}'); + } + reader.exitDepth(); + return map; + } + + @Override + public final Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + Map map = newMap(); + reader.expectNextToken('{'); + if (!reader.consumeNextToken('}')) { + do { + String key = reader.readString(); + reader.expectNextToken(':'); + map.put(key, reader.tryReadNullToken() ? null : readUtf16Value(reader)); + } while (reader.consumeNextToken(',')); + reader.expectNextToken('}'); + } + reader.exitDepth(); + return map; + } + + @Override + public final Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + Map map = newMap(); + reader.expectNextToken('{'); + if (!reader.consumeNextToken('}')) { + do { + String key = reader.readString(); + reader.expectNextToken(':'); + map.put(key, reader.tryReadNullToken() ? null : readUtf8Value(reader)); + } while (reader.consumeNextToken(',')); + reader.expectNextToken('}'); + } + reader.exitDepth(); + return map; + } + + abstract Object readLatin1Value(Latin1JsonReader reader); + + abstract Object readUtf16Value(Utf16JsonReader reader); + + abstract Object readUtf8Value(Utf8JsonReader reader); + } + + public static final class StringStringMapCodec extends StringKeyMapCodec { + private StringStringMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeObjectStart(); + int index = 0; + for (Map.Entry entry : ((Map) value).entrySet()) { + writer.writeComma(index++); + writer.writeFieldName((String) entry.getKey()); + Object element = entry.getValue(); + if (element == null) { + writer.writeNull(); + } else { + writer.writeString((String) element); + } + } + writer.writeObjectEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Map map = newMap(); + reader.expect('{'); + if (!reader.consume('}')) { + do { + String key = reader.readString(); + reader.expect(':'); + map.put(key, reader.tryReadNull() ? null : reader.readString()); + } while (reader.consume(',')); + reader.expect('}'); + } + reader.exitDepth(); + return map; + } + + @Override + Object readLatin1Value(Latin1JsonReader reader) { + return reader.readString(); + } + + @Override + Object readUtf16Value(Utf16JsonReader reader) { + return reader.readString(); + } + + @Override + Object readUtf8Value(Utf8JsonReader reader) { + return reader.readString(); + } + } + + public static final class StringBooleanMapCodec extends StringKeyMapCodec { + private StringBooleanMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeObjectStart(); + int index = 0; + for (Map.Entry entry : ((Map) value).entrySet()) { + writer.writeComma(index++); + writer.writeFieldName((String) entry.getKey()); + Object element = entry.getValue(); + if (element == null) { + writer.writeNull(); + } else { + writer.writeBoolean((boolean) element); + } + } + writer.writeObjectEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Map map = newMap(); + reader.expect('{'); + if (!reader.consume('}')) { + do { + String key = reader.readString(); + reader.expect(':'); + map.put(key, reader.tryReadNull() ? null : reader.readBoolean()); + } while (reader.consume(',')); + reader.expect('}'); + } + reader.exitDepth(); + return map; + } + + @Override + Object readLatin1Value(Latin1JsonReader reader) { + return reader.readBooleanValue(); + } + + @Override + Object readUtf16Value(Utf16JsonReader reader) { + return reader.readBooleanValue(); + } + + @Override + Object readUtf8Value(Utf8JsonReader reader) { + return reader.readBooleanValue(); + } + } + + public abstract static class StringNumberMapCodec extends StringKeyMapCodec { + StringNumberMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + final void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeObjectStart(); + int index = 0; + for (Map.Entry entry : ((Map) value).entrySet()) { + writer.writeComma(index++); + writer.writeFieldName((String) entry.getKey()); + Object element = entry.getValue(); + if (element == null) { + writer.writeNull(); + } else { + writeNumber(writer, element); + } + } + writer.writeObjectEnd(); + } + + @Override + final void writeStringNonNull( + StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + final void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + final Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Map map = newMap(); + reader.expect('{'); + if (!reader.consume('}')) { + do { + String key = reader.readString(); + reader.expect(':'); + map.put(key, reader.tryReadNull() ? null : readNumber(reader)); + } while (reader.consume(',')); + reader.expect('}'); + } + reader.exitDepth(); + return map; + } + + abstract void writeNumber(JsonWriter writer, Object value); + + abstract Object readNumber(JsonReader reader); + + Object readLatin1Number(Latin1JsonReader reader) { + return readNumber(reader); + } + + Object readUtf16Number(Utf16JsonReader reader) { + return readNumber(reader); + } + + Object readUtf8Number(Utf8JsonReader reader) { + return readNumber(reader); + } + + @Override + final Object readLatin1Value(Latin1JsonReader reader) { + return readLatin1Number(reader); + } + + @Override + final Object readUtf16Value(Utf16JsonReader reader) { + return readUtf16Number(reader); + } + + @Override + final Object readUtf8Value(Utf8JsonReader reader) { + return readUtf8Number(reader); + } + } + + public static final class StringIntMapCodec extends StringNumberMapCodec { + private StringIntMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeInt((int) value); + } + + @Override + Object readNumber(JsonReader reader) { + return reader.readInt(); + } + + @Override + Object readLatin1Number(Latin1JsonReader reader) { + return reader.readIntValue(); + } + + @Override + Object readUtf16Number(Utf16JsonReader reader) { + return reader.readIntValue(); + } + + @Override + Object readUtf8Number(Utf8JsonReader reader) { + return reader.readIntValue(); + } + } + + public static final class StringLongMapCodec extends StringNumberMapCodec { + private StringLongMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeLong((long) value); + } + + @Override + Object readNumber(JsonReader reader) { + return reader.readLong(); + } + + @Override + Object readLatin1Number(Latin1JsonReader reader) { + return reader.readLongValue(); + } + + @Override + Object readUtf16Number(Utf16JsonReader reader) { + return reader.readLongValue(); + } + + @Override + Object readUtf8Number(Utf8JsonReader reader) { + return reader.readLongValue(); + } + } + + public static final class StringShortMapCodec extends StringNumberMapCodec { + private StringShortMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeInt((short) value); + } + + @Override + Object readNumber(JsonReader reader) { + return readShort(reader.readInt()); + } + + @Override + Object readLatin1Number(Latin1JsonReader reader) { + return readShort(reader.readIntValue()); + } + + @Override + Object readUtf16Number(Utf16JsonReader reader) { + return readShort(reader.readIntValue()); + } + + @Override + Object readUtf8Number(Utf8JsonReader reader) { + return readShort(reader.readIntValue()); + } + } + + public static final class StringByteMapCodec extends StringNumberMapCodec { + private StringByteMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeInt((byte) value); + } + + @Override + Object readNumber(JsonReader reader) { + return readByte(reader.readInt()); + } + + @Override + Object readLatin1Number(Latin1JsonReader reader) { + return readByte(reader.readIntValue()); + } + + @Override + Object readUtf16Number(Utf16JsonReader reader) { + return readByte(reader.readIntValue()); + } + + @Override + Object readUtf8Number(Utf8JsonReader reader) { + return readByte(reader.readIntValue()); + } + } + + public static final class StringFloatMapCodec extends StringNumberMapCodec { + private StringFloatMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeFloat((float) value); + } + + @Override + Object readNumber(JsonReader reader) { + return Float.parseFloat(reader.readNumber()); + } + } + + public static final class StringDoubleMapCodec extends StringNumberMapCodec { + private StringDoubleMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeDouble((double) value); + } + + @Override + Object readNumber(JsonReader reader) { + return Double.parseDouble(reader.readNumber()); + } + } + + public static final class StringBigIntegerMapCodec extends StringNumberMapCodec { + private StringBigIntegerMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeNumber(value.toString()); + } + + @Override + Object readNumber(JsonReader reader) { + return new BigInteger(reader.readNumber()); + } + } + + public static final class StringBigDecimalMapCodec extends StringNumberMapCodec { + private StringBigDecimalMapCodec(TypeRef typeRef, MapFactory factory) { + super(typeRef, factory); + } + + @Override + void writeNumber(JsonWriter writer, Object value) { + writer.writeNumber(value.toString()); + } + + @Override + Object readNumber(JsonReader reader) { + return new BigDecimal(reader.readNumber()); + } + } + + public static final class NumberStringMapCodec extends MapCodec { + private final MapKeyCodec keyCodec; + + private NumberStringMapCodec(TypeRef typeRef, MapFactory factory, MapKeyCodec keyCodec) { + super(typeRef, factory); + this.keyCodec = keyCodec; + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeObjectStart(); + int index = 0; + for (Map.Entry entry : ((Map) value).entrySet()) { + writer.writeComma(index++); + writeKey(writer, entry.getKey(), keyCodec); + Object element = entry.getValue(); + if (element == null) { + writer.writeNull(); + } else { + writer.writeString((String) element); + } + } + writer.writeObjectEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + Map map = newMap(); + reader.expect('{'); + if (!reader.consume('}')) { + do { + Object key = keyCodec.readName(reader); + reader.expect(':'); + map.put(key, reader.tryReadNull() ? null : reader.readString()); + } while (reader.consume(',')); + reader.expect('}'); + } + reader.exitDepth(); + return map; + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + Map map = newMap(); + reader.expectNextToken('{'); + if (!reader.consumeNextToken('}')) { + do { + Object key = keyCodec.readName(reader); + reader.expectNextToken(':'); + map.put(key, reader.tryReadNullToken() ? null : reader.readString()); + } while (reader.consumeNextToken(',')); + reader.expectNextToken('}'); + } + reader.exitDepth(); + return map; + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + Map map = newMap(); + reader.expectNextToken('{'); + if (!reader.consumeNextToken('}')) { + do { + Object key = keyCodec.readName(reader); + reader.expectNextToken(':'); + map.put(key, reader.tryReadNullToken() ? null : reader.readString()); + } while (reader.consumeNextToken(',')); + reader.expectNextToken('}'); + } + reader.exitDepth(); + return map; + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + Map map = newMap(); + reader.expectNextToken('{'); + if (!reader.consumeNextToken('}')) { + do { + Object key = keyCodec.readName(reader); + reader.expectNextToken(':'); + map.put(key, reader.tryReadNullToken() ? null : reader.readString()); + } while (reader.consumeNextToken(',')); + reader.expectNextToken('}'); + } + reader.exitDepth(); + return map; + } + } + + public interface MapKeyCodec { + MapKeyCodec STRING = + new MapKeyCodec() { + @Override + public String toName(Object key) { + return (String) key; + } + + @Override + public Object fromName(String name) { + return name; + } + }; + + String toName(Object key); + + Object fromName(String name); + + default void writeName(JsonWriter writer, Object key) { + writer.writeFieldName(toName(key)); + } + + default Object readName(JsonReader reader) { + return fromName(reader.readString()); + } + + static MapKeyCodec of(Class rawType) { + if (rawType == String.class || rawType == Object.class) { + return STRING; + } + if (rawType.isEnum()) { + return new EnumKeyCodec(rawType); + } + if (rawType == int.class || rawType == Integer.class) { + return IntKeyCodec.INSTANCE; + } + if (rawType == long.class || rawType == Long.class) { + return LongKeyCodec.INSTANCE; + } + if (rawType == short.class || rawType == Short.class) { + return ShortKeyCodec.INSTANCE; + } + if (rawType == byte.class || rawType == Byte.class) { + return ByteKeyCodec.INSTANCE; + } + throw new ForyJsonException("Unsupported JSON map key type " + rawType); + } + } + + public static final class EnumKeyCodec implements MapKeyCodec { + private final Class type; + private final long[] nameHashes; + private final Enum[] values; + + @SuppressWarnings("unchecked") + private EnumKeyCodec(Class type) { + this.type = type; + Enum[] constants = (Enum[]) type.getEnumConstants(); + nameHashes = new long[constants.length]; + values = new Enum[constants.length]; + for (int i = 0; i < constants.length; i++) { + Enum constant = constants[i]; + nameHashes[i] = JsonFieldNameHash.hash(constant.name()); + values[i] = constant; + } + } + + @Override + public String toName(Object key) { + return ((Enum) key).name(); + } + + @Override + public Object fromName(String name) { + return enumValue(JsonFieldNameHash.hash(name)); + } + + @Override + public Object readName(JsonReader reader) { + return enumValue(reader.readFieldNameHash()); + } + + private Enum enumValue(long nameHash) { + long[] localHashes = nameHashes; + for (int i = 0; i < localHashes.length; i++) { + if (localHashes[i] == nameHash) { + return values[i]; + } + } + throw new ForyJsonException("Unknown enum map key for " + type); + } + } + + public static final class IntKeyCodec implements MapKeyCodec { + private static final IntKeyCodec INSTANCE = new IntKeyCodec(); + + @Override + public String toName(Object key) { + return String.valueOf((int) key); + } + + @Override + public Object fromName(String name) { + return Integer.parseInt(name); + } + + @Override + public void writeName(JsonWriter writer, Object key) { + writer.writeIntFieldName((int) key); + } + + @Override + public Object readName(JsonReader reader) { + return reader.readFieldNameInt(); + } + } + + public static final class LongKeyCodec implements MapKeyCodec { + private static final LongKeyCodec INSTANCE = new LongKeyCodec(); + + @Override + public String toName(Object key) { + return String.valueOf((long) key); + } + + @Override + public Object fromName(String name) { + return Long.parseLong(name); + } + + @Override + public void writeName(JsonWriter writer, Object key) { + writer.writeLongFieldName((long) key); + } + + @Override + public Object readName(JsonReader reader) { + return reader.readFieldNameLong(); + } + } + + public static final class ShortKeyCodec implements MapKeyCodec { + private static final ShortKeyCodec INSTANCE = new ShortKeyCodec(); + + @Override + public String toName(Object key) { + return String.valueOf((short) key); + } + + @Override + public Object fromName(String name) { + return readShort(Integer.parseInt(name)); + } + + @Override + public void writeName(JsonWriter writer, Object key) { + writer.writeIntFieldName((short) key); + } + + @Override + public Object readName(JsonReader reader) { + return readShort(reader.readFieldNameInt()); + } + } + + public static final class ByteKeyCodec implements MapKeyCodec { + private static final ByteKeyCodec INSTANCE = new ByteKeyCodec(); + + @Override + public String toName(Object key) { + return String.valueOf((byte) key); + } + + @Override + public Object fromName(String name) { + return readByte(Integer.parseInt(name)); + } + + @Override + public void writeName(JsonWriter writer, Object key) { + writer.writeIntFieldName((byte) key); + } + + @Override + public Object readName(JsonReader reader) { + return readByte(reader.readFieldNameInt()); + } + } + + private static short readShort(int value) { + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new ForyJsonException("Short overflow"); + } + return (short) value; + } + + private static byte readByte(int value) { + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + throw new ForyJsonException("Byte overflow"); + } + return (byte) value; + } +} 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 new file mode 100644 index 0000000000..225bb2929f --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java @@ -0,0 +1,37 @@ +/* + * 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.json.meta.JsonFieldInfo; +import org.apache.fory.reflect.ObjectInstantiator; + +public final class ObjectCodec extends BaseObjectCodec { + ObjectCodec( + Class type, + JsonFieldInfo[] writeFields, + JsonFieldInfo[] readFields, + ObjectInstantiator instantiator) { + super(type, writeFields, readFields, instantiator); + } + + public GeneratedObjectCodec withCodecs(ObjectCodecs codecs) { + return new GeneratedObjectCodec(this, codecs); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecs.java new file mode 100644 index 0000000000..c18719f4f4 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecs.java @@ -0,0 +1,75 @@ +/* + * 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.json.reader.Latin1ObjectReader; +import org.apache.fory.json.reader.ObjectReader; +import org.apache.fory.json.reader.Utf16ObjectReader; +import org.apache.fory.json.reader.Utf8ObjectReader; +import org.apache.fory.json.writer.StringObjectWriter; +import org.apache.fory.json.writer.Utf8ObjectWriter; + +public final class ObjectCodecs { + private final StringObjectWriter stringWriter; + private final Utf8ObjectWriter utf8Writer; + private final ObjectReader reader; + private final Latin1ObjectReader latin1Reader; + private final Utf16ObjectReader utf16Reader; + private final Utf8ObjectReader utf8Reader; + + public ObjectCodecs( + StringObjectWriter stringWriter, + Utf8ObjectWriter utf8Writer, + ObjectReader reader, + Latin1ObjectReader latin1Reader, + Utf16ObjectReader utf16Reader, + Utf8ObjectReader utf8Reader) { + this.stringWriter = stringWriter; + this.utf8Writer = utf8Writer; + this.reader = reader; + this.latin1Reader = latin1Reader; + this.utf16Reader = utf16Reader; + this.utf8Reader = utf8Reader; + } + + public StringObjectWriter stringWriter() { + return stringWriter; + } + + public Utf8ObjectWriter utf8Writer() { + return utf8Writer; + } + + public ObjectReader reader() { + return reader; + } + + public Latin1ObjectReader latin1Reader() { + return latin1Reader; + } + + public Utf16ObjectReader utf16Reader() { + return utf16Reader; + } + + public Utf8ObjectReader utf8Reader() { + return utf8Reader; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java new file mode 100644 index 0000000000..224af03c63 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -0,0 +1,1570 @@ +/* + * 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.math.BigDecimal; +import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Currency; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.meta.JsonAsciiToken; +import org.apache.fory.json.meta.JsonFieldAccessor; +import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.reflect.ReflectionUtils; +import org.apache.fory.type.BFloat16; +import org.apache.fory.type.Float16; + +public final class ScalarCodecs { + private ScalarCodecs() {} + + public static final class NaturalCodec extends AbstractJsonCodec { + public static final NaturalCodec INSTANCE = new NaturalCodec(); + + private NaturalCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + JsonTypeInfo typeInfo = resolver.getRuntimeTypeInfo(value.getClass()); + typeInfo.codec().write(writer, value, resolver); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + JsonTypeInfo typeInfo = resolver.getRuntimeTypeInfo(value.getClass()); + typeInfo.codec().writeString(writer, value, resolver); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + JsonTypeInfo typeInfo = resolver.getRuntimeTypeInfo(value.getClass()); + typeInfo.codec().writeUtf8(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + char token = reader.peekToken(); + if (token == '"') { + return reader.readString(); + } else if (token == '{') { + return MapCodec.readUntyped(reader, resolver); + } else if (token == '[') { + return CollectionCodec.readUntyped(reader, resolver); + } else if (token == 't' || token == 'f') { + return reader.readBoolean(); + } else if (token == 'n') { + reader.readNull(); + return null; + } + String number = reader.readNumber(); + if (number.indexOf('.') >= 0 || number.indexOf('e') >= 0 || number.indexOf('E') >= 0) { + return Double.parseDouble(number); + } + return Long.parseLong(number); + } + } + + public static final class StringCodec extends AbstractJsonCodec { + public static final StringCodec INSTANCE = new StringCodec(); + + private StringCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeString((String) value); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeString((String) value); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return reader.readString(); + } + } + + public static final class VoidCodec extends AbstractJsonCodec { + public static final VoidCodec INSTANCE = new VoidCodec(); + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.readNull(); + return null; + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.readNull(); + return null; + } + } + + public static final class BooleanCodec extends AbstractJsonCodec { + public static final BooleanCodec INSTANCE = new BooleanCodec(); + + private BooleanCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeBoolean((Boolean) value); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeBoolean((Boolean) value); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return reader.readBoolean(); + } + + @Override + public void readField( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + if (reader.peekNull()) { + readFieldDefault(reader, object, accessor, typeInfo, resolver); + } else if (typeInfo.primitive()) { + accessor.putBoolean(object, reader.readBoolean()); + } else { + accessor.putObject(object, reader.readBoolean()); + } + } + } + + public static final class IntCodec extends AbstractJsonCodec { + public static final IntCodec INSTANCE = new IntCodec(); + + private IntCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeInt((Integer) value); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeInt((Integer) value); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return reader.readInt(); + } + + @Override + public void readField( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + if (reader.peekNull()) { + readFieldDefault(reader, object, accessor, typeInfo, resolver); + } else if (typeInfo.primitive()) { + accessor.putInt(object, reader.readInt()); + } else { + accessor.putObject(object, reader.readInt()); + } + } + } + + public static final class LongCodec extends AbstractJsonCodec { + public static final LongCodec INSTANCE = new LongCodec(); + + private LongCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong((Long) value); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong((Long) value); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return reader.readLong(); + } + + @Override + public void readField( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + if (reader.peekNull()) { + readFieldDefault(reader, object, accessor, typeInfo, resolver); + } else if (typeInfo.primitive()) { + accessor.putLong(object, reader.readLong()); + } else { + accessor.putObject(object, reader.readLong()); + } + } + } + + public static final class ShortCodec extends AbstractJsonCodec { + public static final ShortCodec INSTANCE = new ShortCodec(); + + private ShortCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeInt(((Short) value).intValue()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeInt(((Short) value).intValue()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + int value = reader.readInt(); + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new ForyJsonException("Short overflow"); + } + return (short) value; + } + + @Override + public void readField( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + if (reader.peekNull()) { + readFieldDefault(reader, object, accessor, typeInfo, resolver); + } else if (typeInfo.primitive()) { + int value = reader.readInt(); + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new ForyJsonException("Short overflow"); + } + accessor.putShort(object, (short) value); + } else { + int value = reader.readInt(); + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new ForyJsonException("Short overflow"); + } + accessor.putObject(object, (short) value); + } + } + } + + public static final class ByteCodec extends AbstractJsonCodec { + public static final ByteCodec INSTANCE = new ByteCodec(); + + private ByteCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeInt(((Byte) value).intValue()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeInt(((Byte) value).intValue()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + int value = reader.readInt(); + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + throw new ForyJsonException("Byte overflow"); + } + return (byte) value; + } + + @Override + public void readField( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + if (reader.peekNull()) { + readFieldDefault(reader, object, accessor, typeInfo, resolver); + } else if (typeInfo.primitive()) { + int value = reader.readInt(); + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + throw new ForyJsonException("Byte overflow"); + } + accessor.putByte(object, (byte) value); + } else { + int value = reader.readInt(); + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + throw new ForyJsonException("Byte overflow"); + } + accessor.putObject(object, (byte) value); + } + } + } + + public static final class FloatCodec extends AbstractJsonCodec { + public static final FloatCodec INSTANCE = new FloatCodec(); + + private FloatCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeFloat((Float) value); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeFloat((Float) value); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return Float.parseFloat(reader.readNumber()); + } + + @Override + public void readField( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + if (reader.peekNull()) { + readFieldDefault(reader, object, accessor, typeInfo, resolver); + } else if (typeInfo.primitive()) { + accessor.putFloat(object, Float.parseFloat(reader.readNumber())); + } else { + accessor.putObject(object, Float.parseFloat(reader.readNumber())); + } + } + } + + public static final class DoubleCodec extends AbstractJsonCodec { + public static final DoubleCodec INSTANCE = new DoubleCodec(); + + private DoubleCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeDouble((Double) value); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeDouble((Double) value); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return Double.parseDouble(reader.readNumber()); + } + + @Override + public void readField( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + if (reader.peekNull()) { + readFieldDefault(reader, object, accessor, typeInfo, resolver); + } else if (typeInfo.primitive()) { + accessor.putDouble(object, Double.parseDouble(reader.readNumber())); + } else { + accessor.putObject(object, Double.parseDouble(reader.readNumber())); + } + } + } + + public static final class CharCodec extends AbstractJsonCodec { + public static final CharCodec INSTANCE = new CharCodec(); + + private CharCodec() {} + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeChar((Character) value); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeChar((Character) value); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + String value = reader.readString(); + if (value.length() != 1) { + throw new ForyJsonException("Expected one-character JSON string for char"); + } + return value.charAt(0); + } + + @Override + public void readField( + JsonReader reader, + Object object, + JsonFieldAccessor accessor, + JsonTypeInfo typeInfo, + JsonTypeResolver resolver) { + if (reader.peekNull()) { + readFieldDefault(reader, object, accessor, typeInfo, resolver); + } else { + char value = (Character) readNonNull(reader, typeInfo, resolver); + if (typeInfo.primitive()) { + accessor.putChar(object, value); + } else { + accessor.putObject(object, value); + } + } + } + } + + public abstract static class StringValueCodec extends AbstractJsonCodec { + @Override + final void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeString(toJsonString(value)); + } + + @Override + final void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeString(toJsonString(value)); + } + + @Override + final Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return fromJsonString(reader.readString()); + } + + abstract String toJsonString(Object value); + + abstract Object fromJsonString(String value); + } + + public abstract static class NumberValueCodec extends AbstractJsonCodec { + @Override + final void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNumber(toJsonNumber(value)); + } + + @Override + final void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNumber(toJsonNumber(value)); + } + + @Override + final Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return fromJsonNumber(reader.readNumber()); + } + + abstract String toJsonNumber(Object value); + + abstract Object fromJsonNumber(String value); + } + + public static final class BigIntegerCodec extends NumberValueCodec { + public static final BigIntegerCodec INSTANCE = new BigIntegerCodec(); + + @Override + String toJsonNumber(Object value) { + return value.toString(); + } + + @Override + Object fromJsonNumber(String value) { + return new BigInteger(value); + } + } + + public static final class BigDecimalCodec extends NumberValueCodec { + public static final BigDecimalCodec INSTANCE = new BigDecimalCodec(); + + @Override + String toJsonNumber(Object value) { + return value.toString(); + } + + @Override + Object fromJsonNumber(String value) { + return new BigDecimal(value); + } + } + + public static final class Float16Codec extends AbstractJsonCodec { + public static final Float16Codec INSTANCE = new Float16Codec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeFloat(((Float16) value).floatValue()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeFloat(((Float16) value).floatValue()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return Float16.valueOf(Float.parseFloat(reader.readNumber())); + } + } + + public static final class BFloat16Codec extends AbstractJsonCodec { + public static final BFloat16Codec INSTANCE = new BFloat16Codec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeFloat(((BFloat16) value).floatValue()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeFloat(((BFloat16) value).floatValue()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return BFloat16.valueOf(Float.parseFloat(reader.readNumber())); + } + } + + public static final class StringBuilderCodec extends StringValueCodec { + public static final StringBuilderCodec INSTANCE = new StringBuilderCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return new StringBuilder(value); + } + } + + public static final class StringBufferCodec extends StringValueCodec { + public static final StringBufferCodec INSTANCE = new StringBufferCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return new StringBuffer(value); + } + } + + public static final class ClassCodec extends StringValueCodec { + public static final ClassCodec INSTANCE = new ClassCodec(); + + @Override + String toJsonString(Object value) { + return ((Class) value).getName(); + } + + @Override + Object fromJsonString(String value) { + try { + return ReflectionUtils.loadClass(value); + } catch (RuntimeException e) { + throw new ForyJsonException("Cannot load class " + value, e); + } + } + } + + public static final class CurrencyCodec extends StringValueCodec { + public static final CurrencyCodec INSTANCE = new CurrencyCodec(); + + @Override + String toJsonString(Object value) { + return ((Currency) value).getCurrencyCode(); + } + + @Override + Object fromJsonString(String value) { + return Currency.getInstance(value); + } + } + + public static final class UriCodec extends StringValueCodec { + public static final UriCodec INSTANCE = new UriCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return URI.create(value); + } + } + + public static final class UrlCodec extends StringValueCodec { + public static final UrlCodec INSTANCE = new UrlCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + try { + return new URL(value); + } catch (MalformedURLException e) { + throw new ForyJsonException("Invalid URL " + value, e); + } + } + } + + public static final class PatternCodec extends StringValueCodec { + public static final PatternCodec INSTANCE = new PatternCodec(); + + @Override + String toJsonString(Object value) { + return ((Pattern) value).pattern(); + } + + @Override + Object fromJsonString(String value) { + return Pattern.compile(value); + } + } + + public static final class UuidCodec extends StringValueCodec { + public static final UuidCodec INSTANCE = new UuidCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return UUID.fromString(value); + } + } + + public static final class LocaleCodec extends StringValueCodec { + public static final LocaleCodec INSTANCE = new LocaleCodec(); + + @Override + String toJsonString(Object value) { + return ((Locale) value).toLanguageTag(); + } + + @Override + Object fromJsonString(String value) { + return Locale.forLanguageTag(value); + } + } + + public static final class CharsetCodec extends StringValueCodec { + public static final CharsetCodec INSTANCE = new CharsetCodec(); + + @Override + String toJsonString(Object value) { + return ((Charset) value).name(); + } + + @Override + Object fromJsonString(String value) { + return Charset.forName(value); + } + } + + public static final class DateCodec extends AbstractJsonCodec { + public static final DateCodec INSTANCE = new DateCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((Date) value).getTime()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((Date) value).getTime()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return new Date(reader.readLong()); + } + } + + public static final class SqlDateCodec extends AbstractJsonCodec { + public static final SqlDateCodec INSTANCE = new SqlDateCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((java.sql.Date) value).getTime()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((java.sql.Date) value).getTime()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return new java.sql.Date(reader.readLong()); + } + } + + public static final class SqlTimeCodec extends AbstractJsonCodec { + public static final SqlTimeCodec INSTANCE = new SqlTimeCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((java.sql.Time) value).getTime()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((java.sql.Time) value).getTime()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return new java.sql.Time(reader.readLong()); + } + } + + public static final class TimestampCodec extends AbstractJsonCodec { + public static final TimestampCodec INSTANCE = new TimestampCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((java.sql.Timestamp) value).getTime()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((java.sql.Timestamp) value).getTime()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return new java.sql.Timestamp(reader.readLong()); + } + } + + public static final class CalendarCodec extends AbstractJsonCodec { + public static final CalendarCodec INSTANCE = new CalendarCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((Calendar) value).getTimeInMillis()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((Calendar) value).getTimeInMillis()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + Calendar calendar = new GregorianCalendar(); + calendar.setTimeInMillis(reader.readLong()); + return calendar; + } + } + + public static final class TimeZoneCodec extends StringValueCodec { + public static final TimeZoneCodec INSTANCE = new TimeZoneCodec(); + + @Override + String toJsonString(Object value) { + return ((TimeZone) value).getID(); + } + + @Override + Object fromJsonString(String value) { + return TimeZone.getTimeZone(value); + } + } + + public static final class LocalDateCodec extends StringValueCodec { + public static final LocalDateCodec INSTANCE = new LocalDateCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return LocalDate.parse(value); + } + } + + public static final class LocalTimeCodec extends StringValueCodec { + public static final LocalTimeCodec INSTANCE = new LocalTimeCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return LocalTime.parse(value); + } + } + + public static final class LocalDateTimeCodec extends StringValueCodec { + public static final LocalDateTimeCodec INSTANCE = new LocalDateTimeCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return LocalDateTime.parse(value); + } + } + + public static final class InstantCodec extends StringValueCodec { + public static final InstantCodec INSTANCE = new InstantCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return Instant.parse(value); + } + } + + public static final class DurationCodec extends StringValueCodec { + public static final DurationCodec INSTANCE = new DurationCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return Duration.parse(value); + } + } + + public static final class ZoneOffsetCodec extends StringValueCodec { + public static final ZoneOffsetCodec INSTANCE = new ZoneOffsetCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return ZoneOffset.of(value); + } + } + + public static final class ZoneIdCodec extends StringValueCodec { + public static final ZoneIdCodec INSTANCE = new ZoneIdCodec(); + + @Override + String toJsonString(Object value) { + return ((ZoneId) value).getId(); + } + + @Override + Object fromJsonString(String value) { + return ZoneId.of(value); + } + } + + public static final class ZonedDateTimeCodec extends StringValueCodec { + public static final ZonedDateTimeCodec INSTANCE = new ZonedDateTimeCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return ZonedDateTime.parse(value); + } + } + + public static final class YearCodec extends StringValueCodec { + public static final YearCodec INSTANCE = new YearCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return Year.parse(value); + } + } + + public static final class YearMonthCodec extends StringValueCodec { + public static final YearMonthCodec INSTANCE = new YearMonthCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return YearMonth.parse(value); + } + } + + public static final class MonthDayCodec extends StringValueCodec { + public static final MonthDayCodec INSTANCE = new MonthDayCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return MonthDay.parse(value); + } + } + + public static final class PeriodCodec extends StringValueCodec { + public static final PeriodCodec INSTANCE = new PeriodCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return Period.parse(value); + } + } + + public static final class OffsetTimeCodec extends StringValueCodec { + public static final OffsetTimeCodec INSTANCE = new OffsetTimeCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return OffsetTime.parse(value); + } + } + + public static final class OffsetDateTimeCodec extends StringValueCodec { + public static final OffsetDateTimeCodec INSTANCE = new OffsetDateTimeCodec(); + + @Override + String toJsonString(Object value) { + return value.toString(); + } + + @Override + Object fromJsonString(String value) { + return OffsetDateTime.parse(value); + } + } + + public static final class AtomicBooleanCodec extends AbstractJsonCodec { + public static final AtomicBooleanCodec INSTANCE = new AtomicBooleanCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeBoolean(((AtomicBoolean) value).get()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeBoolean(((AtomicBoolean) value).get()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return new AtomicBoolean(reader.readBoolean()); + } + } + + public static final class AtomicIntegerCodec extends AbstractJsonCodec { + public static final AtomicIntegerCodec INSTANCE = new AtomicIntegerCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeInt(((AtomicInteger) value).get()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeInt(((AtomicInteger) value).get()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return new AtomicInteger(reader.readInt()); + } + } + + public static final class AtomicLongCodec extends AbstractJsonCodec { + public static final AtomicLongCodec INSTANCE = new AtomicLongCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((AtomicLong) value).get()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLong(((AtomicLong) value).get()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return new AtomicLong(reader.readLong()); + } + } + + public static final class AtomicReferenceCodec extends AbstractJsonCodec { + private final JsonTypeInfo valueTypeInfo; + private final JsonCodec valueCodec; + + public AtomicReferenceCodec(java.lang.reflect.Type valueType, JsonTypeResolver resolver) { + Class valueRawType = CodecUtils.rawType(valueType, Object.class); + valueTypeInfo = resolver.getTypeInfo(valueType, valueRawType); + valueCodec = valueTypeInfo.codec(); + } + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + Object value = + reader.peekNull() + ? readNullValue(reader) + : valueCodec.read(reader, valueTypeInfo, resolver); + return new AtomicReference<>(value); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return new AtomicReference<>(valueCodec.read(reader, valueTypeInfo, resolver)); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + valueCodec.write(writer, ((AtomicReference) value).get(), resolver); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + valueCodec.writeUtf8(writer, ((AtomicReference) value).get(), resolver); + } + } + + public static final class OptionalCodec extends AbstractJsonCodec { + private final JsonTypeInfo valueTypeInfo; + private final JsonCodec valueCodec; + + public OptionalCodec(java.lang.reflect.Type valueType, JsonTypeResolver resolver) { + Class valueRawType = CodecUtils.rawType(valueType, Object.class); + valueTypeInfo = resolver.getTypeInfo(valueType, valueRawType); + valueCodec = valueTypeInfo.codec(); + } + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.peekNull()) { + reader.readNull(); + return Optional.empty(); + } + return Optional.ofNullable(valueCodec.read(reader, valueTypeInfo, resolver)); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return Optional.ofNullable(valueCodec.read(reader, valueTypeInfo, resolver)); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Optional optional = (Optional) value; + if (optional.isPresent()) { + valueCodec.write(writer, optional.get(), resolver); + } else { + writer.writeNull(); + } + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + Optional optional = (Optional) value; + if (optional.isPresent()) { + valueCodec.writeUtf8(writer, optional.get(), resolver); + } else { + writer.writeNull(); + } + } + } + + public static final class OptionalIntCodec extends AbstractJsonCodec { + public static final OptionalIntCodec INSTANCE = new OptionalIntCodec(); + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.peekNull()) { + reader.readNull(); + return OptionalInt.empty(); + } + return OptionalInt.of(reader.readInt()); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + OptionalInt optional = (OptionalInt) value; + if (optional.isPresent()) { + writer.writeInt(optional.getAsInt()); + } else { + writer.writeNull(); + } + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return OptionalInt.of(reader.readInt()); + } + } + + public static final class OptionalLongCodec extends AbstractJsonCodec { + public static final OptionalLongCodec INSTANCE = new OptionalLongCodec(); + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.peekNull()) { + reader.readNull(); + return OptionalLong.empty(); + } + return OptionalLong.of(reader.readLong()); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + OptionalLong optional = (OptionalLong) value; + if (optional.isPresent()) { + writer.writeLong(optional.getAsLong()); + } else { + writer.writeNull(); + } + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return OptionalLong.of(reader.readLong()); + } + } + + public static final class OptionalDoubleCodec extends AbstractJsonCodec { + public static final OptionalDoubleCodec INSTANCE = new OptionalDoubleCodec(); + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.peekNull()) { + reader.readNull(); + return OptionalDouble.empty(); + } + return OptionalDouble.of(Double.parseDouble(reader.readNumber())); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + OptionalDouble optional = (OptionalDouble) value; + if (optional.isPresent()) { + writer.writeDouble(optional.getAsDouble()); + } else { + writer.writeNull(); + } + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeNonNull(writer, value, resolver); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return OptionalDouble.of(Double.parseDouble(reader.readNumber())); + } + } + + public static final class ByteBufferCodec extends AbstractJsonCodec { + public static final ByteBufferCodec INSTANCE = new ByteBufferCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeBuffer(writer, (ByteBuffer) value); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writeBuffer(writer, (ByteBuffer) value); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + byte[] bytes = new byte[16]; + int size = 0; + reader.expect('['); + if (!reader.consume(']')) { + do { + if (size == bytes.length) { + bytes = Arrays.copyOf(bytes, size << 1); + } + int value = reader.readInt(); + if (value < Byte.MIN_VALUE || value > 255) { + throw new ForyJsonException("ByteBuffer element out of byte range: " + value); + } + bytes[size++] = (byte) value; + } while (reader.consume(',')); + reader.expect(']'); + } + reader.exitDepth(); + return ByteBuffer.wrap(Arrays.copyOf(bytes, size)); + } + + private void writeBuffer(JsonWriter writer, ByteBuffer value) { + ByteBuffer buffer = value.duplicate(); + writer.writeArrayStart(); + int index = 0; + while (buffer.hasRemaining()) { + writer.writeComma(index++); + writer.writeInt(buffer.get()); + } + writer.writeArrayEnd(); + } + } + + public static final class EnumCodec extends AbstractJsonCodec { + private final Class type; + private final long[] nameHashes; + private final long[] tokenPrefixes; + private final long[] tokenMasks; + private final int[] tokenSuffixes; + private final byte[] tokenSuffixLengths; + private final int[] tokenLengths; + private final Enum[] values; + private final Enum[] tokenValues; + private final int tokenCount; + + public EnumCodec(Class type) { + this.type = type; + Enum[] constants = (Enum[]) type.getEnumConstants(); + nameHashes = new long[constants.length]; + tokenPrefixes = new long[constants.length]; + tokenMasks = new long[constants.length]; + tokenSuffixes = new int[constants.length]; + tokenSuffixLengths = new byte[constants.length]; + tokenLengths = new int[constants.length]; + values = new Enum[constants.length]; + tokenValues = new Enum[constants.length]; + int localTokenCount = 0; + for (int i = 0; i < constants.length; i++) { + Enum constant = constants[i]; + String name = constant.name(); + nameHashes[i] = JsonFieldNameHash.hash(name); + values[i] = constant; + String token = "\"" + name + "\""; + if (JsonAsciiToken.isPackable(token)) { + int tokenLength = token.length(); + tokenPrefixes[localTokenCount] = JsonAsciiToken.prefix(token); + tokenMasks[localTokenCount] = JsonAsciiToken.prefixMask(tokenLength); + tokenSuffixes[localTokenCount] = JsonAsciiToken.suffix(token); + tokenSuffixLengths[localTokenCount] = (byte) JsonAsciiToken.suffixLength(tokenLength); + tokenLengths[localTokenCount] = tokenLength; + tokenValues[localTokenCount] = constant; + localTokenCount++; + } + } + tokenCount = localTokenCount; + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeString(((Enum) value).name()); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeString(((Enum) value).name()); + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readLatin1Enum(reader); + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readUtf16Enum(reader); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return readUtf8Enum(reader); + } + + public Object readEnum(JsonReader reader) { + return enumValue(reader.readStringHash()); + } + + public Object readLatin1Enum(Latin1JsonReader reader) { + return enumValue(reader.readPackedStringHash()); + } + + public Object readNextLatin1Enum(Latin1JsonReader reader) { + Object value = readDirectLatin1EnumToken(reader); + if (value != null) { + return value; + } + return enumValue(reader.readNextPackedStringHash()); + } + + public Object readLatin1EnumToken(Latin1JsonReader reader) { + Object value = readDirectLatin1EnumToken(reader); + if (value != null) { + return value; + } + return readLatin1EnumHashToken(reader); + } + + public Object readLatin1EnumHashToken(Latin1JsonReader reader) { + return enumValue(reader.readPackedStringHashTokenValue()); + } + + public Object readUtf16Enum(Utf16JsonReader reader) { + return enumValue(reader.readPackedStringHash()); + } + + public Object readNextUtf16Enum(Utf16JsonReader reader) { + return enumValue(reader.readNextPackedStringHash()); + } + + public Object readUtf8Enum(Utf8JsonReader reader) { + return enumValue(reader.readPackedStringHash()); + } + + public Object readNextUtf8Enum(Utf8JsonReader reader) { + Object value = readDirectUtf8EnumToken(reader); + if (value != null) { + return value; + } + return enumValue(reader.readNextPackedStringHash()); + } + + public Object readUtf8EnumToken(Utf8JsonReader reader) { + Object value = readDirectUtf8EnumToken(reader); + if (value != null) { + return value; + } + return readUtf8EnumHashToken(reader); + } + + public Object readUtf8EnumHashToken(Utf8JsonReader reader) { + return enumValue(reader.readPackedStringHashTokenValue()); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + return readEnum(reader); + } + + private Enum enumValue(long nameHash) { + long[] localHashes = nameHashes; + for (int i = 0; i < localHashes.length; i++) { + if (localHashes[i] == nameHash) { + return values[i]; + } + } + throw new ForyJsonException("Unknown enum value for " + type); + } + + private Object readDirectLatin1EnumToken(Latin1JsonReader reader) { + for (int i = 0; i < tokenCount; i++) { + boolean matched; + switch (tokenSuffixLengths[i]) { + case 0: + matched = + reader.tryReadNextStringToken0(tokenPrefixes[i], tokenMasks[i], tokenLengths[i]); + break; + case 1: + matched = + reader.tryReadNextStringToken1( + tokenPrefixes[i], tokenMasks[i], tokenSuffixes[i], tokenLengths[i]); + break; + case 2: + matched = + reader.tryReadNextStringToken2( + tokenPrefixes[i], tokenMasks[i], tokenSuffixes[i], tokenLengths[i]); + break; + default: + matched = + reader.tryReadNextStringToken3( + tokenPrefixes[i], tokenMasks[i], tokenSuffixes[i], tokenLengths[i]); + break; + } + if (matched) { + return tokenValues[i]; + } + } + return null; + } + + private Object readDirectUtf8EnumToken(Utf8JsonReader reader) { + for (int i = 0; i < tokenCount; i++) { + boolean matched; + switch (tokenSuffixLengths[i]) { + case 0: + matched = + reader.tryReadNextStringToken0(tokenPrefixes[i], tokenMasks[i], tokenLengths[i]); + break; + case 1: + matched = + reader.tryReadNextStringToken1( + tokenPrefixes[i], tokenMasks[i], tokenSuffixes[i], tokenLengths[i]); + break; + case 2: + matched = + reader.tryReadNextStringToken2( + tokenPrefixes[i], tokenMasks[i], tokenSuffixes[i], tokenLengths[i]); + break; + default: + matched = + reader.tryReadNextStringToken3( + tokenPrefixes[i], tokenMasks[i], tokenSuffixes[i], tokenLengths[i]); + break; + } + if (matched) { + return tokenValues[i]; + } + } + return null; + } + } + + private static Object readNullValue(JsonReader reader) { + reader.readNull(); + return null; + } +} 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 new file mode 100644 index 0000000000..a98c7e6d58 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -0,0 +1,363 @@ +/* + * 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.codegen; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.fory.codegen.CodeGenerator; +import org.apache.fory.codegen.CompileUnit; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.BaseObjectCodec; +import org.apache.fory.json.codec.JsonCodec; +import org.apache.fory.json.codec.ObjectCodecs; +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.json.reader.Latin1ObjectReader; +import org.apache.fory.json.reader.ObjectReader; +import org.apache.fory.json.reader.Utf16ObjectReader; +import org.apache.fory.json.reader.Utf8ObjectReader; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.StringObjectWriter; +import org.apache.fory.json.writer.Utf8ObjectWriter; +import org.apache.fory.platform.JdkVersion; +import org.apache.fory.util.record.RecordUtils; + +public final class JsonCodegen { + static final String PACKAGE = "org.apache.fory.json.codegen"; + static final int GENERIC_READER = 0; + static final int LATIN1_READER = 1; + static final int UTF16_READER = 2; + static final int UTF8_READER = 3; + private static final AtomicInteger ID = new AtomicInteger(); + + final boolean writeNullFields; + private final CodeGenerator codeGenerator; + private final ClassLoader jsonLoader; + + public JsonCodegen(boolean writeNullFields) { + this.writeNullFields = writeNullFields; + jsonLoader = JsonCodegen.class.getClassLoader(); + codeGenerator = new CodeGenerator(jsonLoader); + } + + public ObjectCodecs compile(BaseObjectCodec objectCodec, JsonTypeResolver typeResolver) { + Class type = objectCodec.type(); + if (!canCompile(type)) { + return null; + } + boolean record = objectCodec.isRecord(); + JsonFieldInfo[] writeProperties = objectCodec.writeFields(); + for (int i = 0; i < writeProperties.length; i++) { + if (!canCompileWrite(writeProperties[i])) { + return null; + } + } + JsonFieldInfo[] readProperties = objectCodec.readFields(); + for (int i = 0; i < readProperties.length; i++) { + if (!canCompileRead(readProperties[i], record)) { + return null; + } + } + String className = className(type); + JsonCodec[] writeCodecs = writeCodecs(writeProperties); + Utf8ObjectWriter utf8Writer = + (Utf8ObjectWriter) + compileWriter(className + "_Utf8", type, writeProperties, writeCodecs, true); + if (utf8Writer == null) { + return null; + } + StringObjectWriter stringWriter = + (StringObjectWriter) + compileWriter(className + "_String", type, writeProperties, writeCodecs, false); + if (stringWriter == null) { + return null; + } + JsonCodec[] readCodecs = readCodecs(readProperties); + BaseObjectCodec[] readObjectCodecs = readObjectCodecs(objectCodec, typeResolver); + ObjectReader reader = + (ObjectReader) + compileReader( + className + "_Reader", type, readProperties, readCodecs, readObjectCodecs, record); + if (reader == null) { + return null; + } + return new ObjectCodecs( + stringWriter, + utf8Writer, + reader, + (Latin1ObjectReader) reader, + (Utf16ObjectReader) reader, + (Utf8ObjectReader) reader); + } + + private Object compileWriter( + String className, + Class type, + JsonFieldInfo[] properties, + JsonCodec[] nestedCodecs, + boolean utf8) { + String code = + new JsonGeneratedCodecBuilder(this, className, type, properties, utf8, true, false) + .genCode(); + try { + CompileUnit unit = new CompileUnit(PACKAGE, className, code, JsonCodegen.class); + Class writerClass = codeGenerator.compileAndLoad(unit, state -> state.lock.lock()); + Constructor constructor = + writerClass.getDeclaredConstructor(JsonFieldInfo[].class, JsonCodec[].class); + constructor.setAccessible(true); + return constructor.newInstance(properties, nestedCodecs); + } catch (Throwable e) { + throw new ForyJsonException("Cannot compile generated JSON writer " + className, e); + } + } + + private Object compileReader( + String className, + Class type, + JsonFieldInfo[] properties, + JsonCodec[] readCodecs, + BaseObjectCodec[] nestedCodecs, + boolean record) { + String code = + new JsonGeneratedCodecBuilder(this, className, type, properties, false, false, record) + .genCode(); + try { + CompileUnit unit = new CompileUnit(PACKAGE, className, code, JsonCodegen.class); + Class readerClass = codeGenerator.compileAndLoad(unit, state -> state.lock.lock()); + Constructor constructor = + readerClass.getDeclaredConstructor( + JsonFieldInfo[].class, JsonCodec[].class, BaseObjectCodec[].class); + constructor.setAccessible(true); + return constructor.newInstance(properties, readCodecs, nestedCodecs); + } catch (Throwable e) { + throw new ForyJsonException("Cannot compile generated JSON reader " + className, e); + } + } + + private static JsonCodec[] writeCodecs(JsonFieldInfo[] properties) { + JsonCodec[] codecs = new JsonCodec[properties.length]; + for (int i = 0; i < properties.length; i++) { + if (usesWriteCodec(properties[i])) { + codecs[i] = properties[i].writeTypeInfo().codec(); + } + } + return codecs; + } + + private static JsonCodec[] readCodecs(JsonFieldInfo[] properties) { + JsonCodec[] codecs = new JsonCodec[properties.length]; + for (int i = 0; i < properties.length; i++) { + if (usesReadCodec(properties[i])) { + codecs[i] = properties[i].readTypeInfo().codec(); + } + } + return codecs; + } + + private BaseObjectCodec[] readObjectCodecs( + BaseObjectCodec objectCodec, JsonTypeResolver typeResolver) { + JsonFieldInfo[] properties = objectCodec.readFields(); + BaseObjectCodec[] nestedCodecs = new BaseObjectCodec[properties.length]; + Class type = objectCodec.type(); + for (int i = 0; i < properties.length; i++) { + Class nestedType = readNestedType(properties[i]); + if (nestedType != null && nestedType != type) { + nestedCodecs[i] = typeResolver.getObjectCodec(nestedType); + } + } + return nestedCodecs; + } + + static Class readNestedType(JsonFieldInfo property) { + if (property.readKind() == JsonFieldKind.OBJECT + && property.readRawType() != Object.class + && property.readTypeInfo().codec() instanceof BaseObjectCodec) { + return property.readRawType(); + } + return null; + } + + private boolean canCompileWrite(JsonFieldInfo property) { + Field field = property.writeField(); + if (field == null) { + return false; + } + if (!isRecordField(property) && property.writeField() == null) { + return false; + } + Class rawType = property.writeRawType(); + if (rawType != null && !rawType.isPrimitive() && !isVisible(rawType)) { + return false; + } + Class elementType = property.writeElementRawType(); + return !isPojo(elementType) || isVisible(elementType); + } + + private boolean canCompileRead(JsonFieldInfo property, boolean record) { + if (!record && property.readAccessor() == null) { + return false; + } + if (!record && property.readAccessor().coreAccessor() == null) { + return false; + } + Class rawType = property.readRawType(); + if (rawType != null && !rawType.isPrimitive() && !isVisible(rawType)) { + return false; + } + Class elementType = property.readElementRawType(); + return !isPojo(elementType) || isVisible(elementType); + } + + private boolean canCompile(Class type) { + return supportsHiddenNestmateLoading() + && CodeGenerator.sourcePublicAccessible(type) + && isVisible(type); + } + + private static boolean supportsHiddenNestmateLoading() { + // Generated JSON codecs are defined as hidden nestmates of JsonCodegen; JDK 8 must keep using + // the interpreter path because hidden classes are unavailable there. + return JdkVersion.MAJOR_VERSION >= 15; + } + + private boolean isVisible(Class type) { + if (type.isPrimitive()) { + return true; + } + while (type.isArray()) { + type = type.getComponentType(); + } + if (type.isPrimitive()) { + return true; + } + String name = type.getName(); + try { + return Class.forName(name, false, jsonLoader) == type; + } catch (ClassNotFoundException e) { + return false; + } + } + + Class codecFieldType(JsonCodec codec) { + Class type = codec.getClass(); + if (isPublicSourceType(type) && isVisible(type)) { + return type; + } + return JsonCodec.class; + } + + private static boolean isPublicSourceType(Class type) { + if (!CodeGenerator.sourcePublicAccessible(type)) { + return false; + } + for (Class current = type; current != null; current = current.getEnclosingClass()) { + if (!Modifier.isPublic(current.getModifiers())) { + return false; + } + } + return true; + } + + static boolean usesWriteCodec(JsonFieldInfo property) { + switch (property.writeKind()) { + case ARRAY: + case MAP: + case OBJECT: + return true; + case COLLECTION: + return property.writeElementRawType() != String.class; + default: + return false; + } + } + + static boolean usesReadCodec(JsonFieldInfo property) { + switch (property.readKind()) { + case ENUM: + case ARRAY: + case COLLECTION: + case MAP: + return true; + default: + return false; + } + } + + static boolean usesReadTypeField(JsonFieldInfo property) { + switch (property.readKind()) { + case ARRAY: + case COLLECTION: + case MAP: + return true; + case OBJECT: + return usesReadObjectCodec(property); + default: + return false; + } + } + + static boolean usesReadObjectCodec(JsonFieldInfo property) { + return property.readKind() == JsonFieldKind.OBJECT + && property.readRawType() != Object.class + && property.readTypeInfo().codec() instanceof BaseObjectCodec; + } + + static boolean storesReadObjectCodec(Class type, JsonFieldInfo property) { + Class nestedType = readNestedType(property); + return nestedType != null && nestedType != type; + } + + private static String className(Class type) { + String name = type.getName().replace('.', '_').replace('$', '_'); + String uniqueId = CodeGenerator.getClassUniqueId(type); + if (uniqueId.isEmpty()) { + uniqueId = String.valueOf(ID.incrementAndGet()); + } + return "JsonWriter_" + name + "_" + uniqueId; + } + + private static boolean isPojo(Class type) { + return type != null + && type != Object.class + && type != String.class + && type != Boolean.class + && type != Byte.class + && type != Short.class + && type != Integer.class + && type != Long.class + && type != Float.class + && type != Double.class + && type != Character.class + && !type.isPrimitive() + && !type.isEnum() + && !type.isArray() + && !Collection.class.isAssignableFrom(type) + && !Map.class.isAssignableFrom(type); + } + + private static boolean isRecordField(JsonFieldInfo property) { + Field field = property.writeField(); + return field != null && RecordUtils.isRecord(field.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 new file mode 100644 index 0000000000..e5e0877993 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java @@ -0,0 +1,140 @@ +/* + * 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.codegen; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +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.codec.JsonCodec; +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.reflect.TypeRef; +import org.apache.fory.type.Descriptor; +import org.apache.fory.util.record.RecordUtils; + +final class JsonGeneratedCodecBuilder extends CodecBuilder { + private final String generatedClassName; + private final JsonFieldInfo[] properties; + private final boolean utf8; + private final boolean writer; + private final boolean record; + private final JsonCodegen codegen; + + JsonGeneratedCodecBuilder( + JsonCodegen codegen, + String generatedClassName, + Class type, + JsonFieldInfo[] properties, + boolean utf8, + boolean writer, + boolean record) { + super(new CodegenContext(), TypeRef.of(type)); + this.codegen = codegen; + this.generatedClassName = generatedClassName; + this.properties = properties; + this.utf8 = utf8; + this.writer = writer; + this.record = record; + ctx.setPackage(JsonCodegen.PACKAGE); + ctx.setClassName(generatedClassName); + ctx.setClassModifiers("final"); + ctx.addImports(JsonFieldInfo.class, JsonCodec.class, JsonTypeResolver.class); + String[] generatedMethodNames = { + "object", "value", "writer", "reader", "owner", "typeResolver" + }; + for (String name : generatedMethodNames) { + if (!ctx.containName(name)) { + ctx.reserveName(name); + } + } + } + + CodegenContext context() { + return ctx; + } + + @Override + public String codecClassName(Class cls) { + return generatedClassName; + } + + @Override + public String genCode() { + return writer + ? new JsonWriterCodegen(codegen) + .genWriterCode(this, generatedClassName, beanClass, properties, utf8) + : new JsonReaderCodegen(codegen) + .genReaderCode(this, generatedClassName, beanClass, properties, record); + } + + @Override + public Expression buildEncodeExpression() { + return new Expression.Empty(); + } + + @Override + public Expression buildDecodeExpression() { + return new Expression.Empty(); + } + + private Descriptor writeDescriptor(JsonFieldInfo property) { + Field field = property.writeField(); + return new Descriptor(field, TypeRef.of(field.getGenericType()), recordReadMethod(field), null); + } + + private Descriptor readDescriptor(JsonFieldInfo property) { + Field field = property.readField(); + 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) { + return getFieldValue(object, writeDescriptor(property)); + } + + Expression newObject() { + return newBean(); + } + + Expression setField(JsonFieldInfo property, Expression object, Expression value) { + return setFieldValue( + object, + readDescriptor(property), + tryInlineCast(value, TypeRef.of(property.readField().getGenericType()))); + } + + Expression setNull(JsonFieldInfo property, Expression object) { + return setField( + property, object, new Expression.Null(TypeRef.of(property.readRawType()), false)); + } +} 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 new file mode 100644 index 0000000000..1736168059 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -0,0 +1,1464 @@ +/* + * 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.codegen; + +import org.apache.fory.codegen.Code; +import org.apache.fory.codegen.CodegenContext; +import org.apache.fory.codegen.Expression; +import org.apache.fory.codegen.Expression.Reference; +import org.apache.fory.json.codec.BaseObjectCodec; +import org.apache.fory.json.codec.CollectionCodec; +import org.apache.fory.json.codec.JsonCodec; +import org.apache.fory.json.meta.JsonAsciiToken; +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Latin1ObjectReader; +import org.apache.fory.json.reader.ObjectReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf16ObjectReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.reader.Utf8ObjectReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.reflect.TypeRef; + +final class JsonReaderCodegen { + private static final int GENERIC_READER = JsonCodegen.GENERIC_READER; + private static final int LATIN1_READER = JsonCodegen.LATIN1_READER; + private static final int UTF16_READER = JsonCodegen.UTF16_READER; + private static final int UTF8_READER = JsonCodegen.UTF8_READER; + + private final JsonCodegen codegen; + + JsonReaderCodegen(JsonCodegen codegen) { + this.codegen = codegen; + } + + private Class codecFieldType(JsonCodec codec) { + return codegen.codecFieldType(codec); + } + + private static Class readNestedType(JsonFieldInfo property) { + return JsonCodegen.readNestedType(property); + } + + String genReaderCode( + JsonGeneratedCodecBuilder builder, + String className, + Class type, + JsonFieldInfo[] properties, + boolean record) { + CodegenContext ctx = builder.context(); + ctx.addImports( + BaseObjectCodec.class, + JsonReader.class, + Latin1JsonReader.class, + Utf16JsonReader.class, + Utf8JsonReader.class); + ctx.implementsInterfaces( + ctx.type(ObjectReader.class), + ctx.type(Latin1ObjectReader.class), + ctx.type(Utf16ObjectReader.class), + ctx.type(Utf8ObjectReader.class)); + ctx.addField(long[].class, "fieldHashes"); + for (int i = 0; i < properties.length; i++) { + ctx.addField(JsonFieldInfo.class, "p" + i); + if (usesReadCodec(properties[i])) { + ctx.addField(codecFieldType(properties[i].readTypeInfo().codec()), "r" + i); + } + if (usesReadTypeField(properties[i])) { + ctx.addField(JsonTypeInfo.class, "t" + i); + } + if (storesReadObjectCodec(type, properties[i])) { + ctx.addField(BaseObjectCodec.class, "c" + i); + } + } + addGeneratedConstructor( + ctx, + readerConstructorExpression(type, properties), + JsonFieldInfo[].class, + "properties", + JsonCodec[].class, + "codecs", + BaseObjectCodec[].class, + "objectCodecs"); + addGeneratedMethod( + ctx, + "final", + "fieldIndex", + fieldIndexExpression(properties), + int.class, + long.class, + "fieldHash"); + addGeneratedMethod( + ctx, + "public", + "read", + readExpression(builder, type, properties, GENERIC_READER, record), + Object.class, + JsonReader.class, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver"); + addGeneratedMethod( + ctx, + "public", + "readLatin1", + fastReadExpression(builder, "readLatin1Slow", type, properties, LATIN1_READER, record), + Object.class, + Latin1JsonReader.class, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver"); + addSlowReadMethods( + ctx, + builder, + "readLatin1Slow", + Latin1JsonReader.class, + type, + properties, + LATIN1_READER, + record); + addGeneratedMethod( + ctx, + "public", + "readUtf16", + fastReadExpression(builder, "readUtf16Slow", type, properties, UTF16_READER, record), + Object.class, + Utf16JsonReader.class, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver"); + addSlowReadMethods( + ctx, + builder, + "readUtf16Slow", + Utf16JsonReader.class, + type, + properties, + UTF16_READER, + record); + addGeneratedMethod( + ctx, + "public", + "readUtf8", + fastReadExpression(builder, "readUtf8Slow", type, properties, UTF8_READER, record), + Object.class, + Utf8JsonReader.class, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver"); + addSlowReadMethods( + ctx, builder, "readUtf8Slow", Utf8JsonReader.class, type, properties, UTF8_READER, record); + return ctx.genCode(); + } + + private void addSlowReadMethods( + CodegenContext ctx, + JsonGeneratedCodecBuilder builder, + String methodName, + Class readerType, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + Class objectType = record ? Object[].class : type; + addGeneratedMethod( + ctx, + "final", + methodName, + slowReadExpression(builder, type, properties, readerMode, record), + void.class, + readerType, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver", + objectType, + "object", + int.class, + "expectedIndex"); + addGeneratedMethod( + ctx, + "final", + methodName, + slowReadFromFirstExpression(builder, type, properties, readerMode, record), + void.class, + readerType, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver", + objectType, + "object", + int.class, + "expectedIndex", + int.class, + "firstFieldIndex"); + } + + private void addGeneratedConstructor( + CodegenContext ctx, Expression expression, Object... params) { + ctx.clearExprState(); + Code.ExprCode body = expression.genCode(ctx); + String code = body.code(); + code = code == null ? "" : ctx.optimizeMethodCode(code); + ctx.addConstructor(code, params); + } + + private void addGeneratedMethod( + CodegenContext ctx, + String modifier, + String name, + Expression expression, + Class returnType, + Object... params) { + ctx.clearExprState(); + Code.ExprCode body = expression.genCode(ctx); + String code = body.code(); + code = code == null ? "" : ctx.optimizeMethodCode(code); + ctx.addMethod(modifier, name, code, returnType, params); + } + + 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(JsonCodec[].class)); + Reference objectCodecsRef = new Reference("objectCodecs", TypeRef.of(BaseObjectCodec[].class)); + Reference hashes = new Reference("this.fieldHashes", TypeRef.of(long[].class)); + expressions.add( + new Expression.Assign( + hashes, + new Expression.NewArray( + long.class, + new Expression.FieldValue( + propertiesRef, "length", TypeRef.of(int.class), false, true)))); + for (int i = 0; i < properties.length; i++) { + Expression id = Expression.Literal.ofInt(i); + Expression property = new Expression.ArrayValue(propertiesRef, id); + expressions.add( + new Expression.Assign( + new Reference("this.p" + i, TypeRef.of(JsonFieldInfo.class)), property)); + expressions.add( + new Expression.AssignArrayElem( + hashes, new Expression.Invoke(property, "nameHash", TypeRef.of(long.class)), id)); + if (usesReadCodec(properties[i])) { + Class codecType = codecFieldType(properties[i].readTypeInfo().codec()); + expressions.add( + new Expression.Assign( + new Reference("this.r" + i, TypeRef.of(codecType)), + new Expression.Cast( + new Expression.ArrayValue(codecsRef, id), TypeRef.of(codecType)))); + } + if (usesReadTypeField(properties[i])) { + expressions.add( + new Expression.Assign( + new Reference("this.t" + i, TypeRef.of(JsonTypeInfo.class)), + new Expression.Invoke(property, "readTypeInfo", TypeRef.of(JsonTypeInfo.class)))); + } + if (storesReadObjectCodec(type, properties[i])) { + expressions.add( + new Expression.Assign( + new Reference("this.c" + i, TypeRef.of(BaseObjectCodec.class)), + new Expression.ArrayValue(objectCodecsRef, id))); + } + } + return expressions; + } + + private Expression fieldIndexExpression(JsonFieldInfo[] properties) { + Expression.ListExpression expressions = new Expression.ListExpression(); + Reference fieldHash = new Reference("fieldHash", TypeRef.of(long.class)); + for (int i = 0; i < properties.length; i++) { + expressions.add( + new Expression.If( + eq(fieldHash, Expression.Literal.ofLong(properties[i].nameHash())), + new Expression.Return(Expression.Literal.ofInt(i)))); + } + expressions.add(new Expression.Return(Expression.Literal.ofInt(-1))); + return expressions; + } + + private Expression readExpression( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + Expression object = objectExpression(builder, record); + Expression hashes = + new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); + Expression expectedIndex = + new Expression.Variable("expectedIndex", Expression.Literal.ofInt(0)); + Expression.ListExpression expressions = new Expression.ListExpression(); + expressions.add(object); + expressions.add(expectExpr(readerMode, '{')); + expressions.add(new Expression.If(consumeExpr(readerMode, '}'), returnObject(object, record))); + expressions.add(hashes); + expressions.add(expectedIndex); + Expression.ListExpression loop = new Expression.ListExpression(); + loop.add( + readNextHashedField( + builder, type, properties, readerMode, object, hashes, expectedIndex, record)); + loop.add( + new Expression.If(not(consumeCommaOrEndObjectExpr(readerMode)), new Expression.Break())); + expressions.add(new Expression.While(Expression.Literal.True, loop)); + expressions.add(returnObject(object, record)); + return expressions; + } + + private Expression fastReadExpression( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + Expression object = objectExpression(builder, record); + Expression.ListExpression expressions = new Expression.ListExpression(); + expressions.add(object); + expressions.add(expectExpr(readerMode, '{')); + expressions.add(new Expression.If(consumeExpr(readerMode, '}'), returnObject(object, record))); + if (properties.length == 0) { + expressions.add(slowCall(slowMethod, object, Expression.Literal.ofInt(0))); + expressions.add(returnObject(object, record)); + return expressions; + } + Expression hashes = + new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); + expressions.add(hashes); + Expression[] skips = new Expression[properties.length]; + for (int i = 1; i < properties.length; i++) { + skips[i] = new Expression.Variable("skip" + i, Expression.Literal.False); + expressions.add(skips[i]); + } + for (int i = 0; i < properties.length; i++) { + Expression read = + fastReadField( + builder, slowMethod, type, properties, i, readerMode, object, hashes, skips, record); + expressions.add(i == 0 ? read : new Expression.If(not(skips[i]), read)); + } + expressions.add(returnObject(object, record)); + return expressions; + } + + private Expression fastReadField( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int index, + int readerMode, + Expression object, + Expression hashes, + Expression[] skips, + boolean record) { + if (isPackedName(properties[index].name())) { + return new Expression.If( + tryReadNextFieldNameColon(readerMode, properties[index]), + new Expression.ListExpression( + readField( + builder, + type, + properties[index], + index, + readerMode, + object, + record, + usesTokenValueRead(readerMode)), + fieldEnd(slowMethod, properties.length, index, readerMode, object, record)), + nextDirectFallback( + builder, + slowMethod, + type, + properties, + index, + readerMode, + object, + hashes, + skips, + record)); + } + return nextDirectFallback( + builder, slowMethod, type, properties, index, readerMode, object, hashes, skips, record); + } + + private Expression nextDirectFallback( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int index, + int readerMode, + Expression object, + Expression hashes, + Expression[] skips, + boolean record) { + int nextIndex = index + 1; + if (nextIndex < properties.length && isPackedName(properties[nextIndex].name())) { + return new Expression.If( + tryReadNextFieldNameColon(readerMode, properties[nextIndex]), + new Expression.ListExpression( + readField( + builder, + type, + properties[nextIndex], + nextIndex, + readerMode, + object, + record, + usesTokenValueRead(readerMode)), + fieldEnd(slowMethod, properties.length, nextIndex, readerMode, object, record), + new Expression.Assign(skips[nextIndex], Expression.Literal.True)), + hashFallback( + builder, + slowMethod, + type, + properties, + index, + readerMode, + object, + hashes, + skips, + record)); + } + return hashFallback( + builder, slowMethod, type, properties, index, readerMode, object, hashes, skips, record); + } + + private Expression hashFallback( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int index, + int readerMode, + Expression object, + Expression hashes, + Expression[] skips, + boolean record) { + Expression fieldHash = readFieldNameHash(readerMode, "fieldHash" + index); + return new Expression.ListExpression( + fieldHash, + fastReadFieldFromHash( + builder, + slowMethod, + type, + properties, + index, + readerMode, + object, + hashes, + skips, + fieldHash, + record)); + } + + private Expression fastReadFieldFromHash( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int index, + int readerMode, + Expression object, + Expression hashes, + Expression[] skips, + Expression fieldHash, + boolean record) { + Expression fallback; + if (index + 1 < properties.length) { + fallback = + new Expression.If( + eq(fieldHash, arrayValue(hashes, index + 1)), + new Expression.ListExpression( + expectExpr(readerMode, ':'), + readField( + builder, + type, + properties[index + 1], + index + 1, + readerMode, + object, + record, + false), + fieldEnd(slowMethod, properties.length, index + 1, readerMode, object, record), + new Expression.Assign(skips[index + 1], Expression.Literal.True)), + slowConsumedReturn(slowMethod, index, fieldIndexInvoke(fieldHash), object, record)); + } else { + fallback = slowConsumedReturn(slowMethod, index, fieldIndexInvoke(fieldHash), object, record); + } + return new Expression.If( + ne(fieldHash, arrayValue(hashes, index)), + fallback, + new Expression.ListExpression( + expectExpr(readerMode, ':'), + readField(builder, type, properties[index], index, readerMode, object, record, false), + fieldEnd(slowMethod, properties.length, index, readerMode, object, record))); + } + + private static boolean isPackedName(String name) { + int length = name.length(); + if (length == 0 || length > Long.BYTES) { + return false; + } + for (int i = 0; i < length; i++) { + char ch = name.charAt(i); + if (ch == 0 || ch > 0xFF) { + return false; + } + } + return true; + } + + private static long packedNameMask(int length) { + return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L; + } + + private Expression objectExpression(JsonGeneratedCodecBuilder builder, boolean record) { + if (record) { + return new Expression.Variable( + "object", + new Expression.Invoke( + ownerRef(), "newRecordFieldValues", TypeRef.of(Object[].class), false)); + } + return new Expression.Variable("object", builder.newObject()); + } + + private Expression returnObject(Expression object, boolean record) { + if (record) { + return new Expression.Return( + new Expression.Invoke(ownerRef(), "newRecord", TypeRef.of(Object.class), object)); + } + return new Expression.Return(object); + } + + private Expression slowConsumedReturn( + String slowMethod, int index, Expression firstFieldIndex, Expression object, boolean record) { + return new Expression.ListExpression( + slowCall(slowMethod, object, Expression.Literal.ofInt(index), firstFieldIndex), + returnObject(object, record)); + } + + private Expression fieldEnd( + String slowMethod, + int propertyCount, + int index, + int readerMode, + Expression object, + boolean record) { + if (index + 1 < propertyCount) { + return new Expression.If( + not(consumeCommaOrEndObjectExpr(readerMode)), returnObject(object, record)); + } + return new Expression.If( + consumeCommaOrEndObjectExpr(readerMode), + slowCall(slowMethod, object, Expression.Literal.ofInt(propertyCount))); + } + + private Expression slowReadExpression( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + Expression object = objectParam(type, record); + Expression hashes = + new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); + Reference expectedIndex = new Reference("expectedIndex", TypeRef.of(int.class)); + Expression.ListExpression expressions = new Expression.ListExpression(); + expressions.add(hashes); + Expression.ListExpression loop = new Expression.ListExpression(); + loop.add( + readNextHashedField( + builder, type, properties, readerMode, object, hashes, expectedIndex, record)); + loop.add( + new Expression.If(not(consumeCommaOrEndObjectExpr(readerMode)), new Expression.Break())); + expressions.add(new Expression.While(Expression.Literal.True, loop)); + return expressions; + } + + private Expression slowReadFromFirstExpression( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + Expression object = objectParam(type, record); + Expression hashes = + new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); + Reference expectedIndex = new Reference("expectedIndex", TypeRef.of(int.class)); + Expression fieldIndex = + new Expression.Variable( + "fieldIndex", new Reference("firstFieldIndex", TypeRef.of(int.class))); + Expression.ListExpression expressions = new Expression.ListExpression(); + expressions.add(hashes); + expressions.add(fieldIndex); + Expression.ListExpression loop = new Expression.ListExpression(); + loop.add(expectExpr(readerMode, ':')); + loop.add(fieldSwitch(builder, type, properties, readerMode, object, fieldIndex, record)); + loop.add(updateExpectedIndex(expectedIndex, fieldIndex)); + loop.add( + new Expression.If(not(consumeCommaOrEndObjectExpr(readerMode)), new Expression.Return())); + Expression fieldHash = readFieldNameHash(readerMode, "fieldHash"); + loop.add(fieldHash); + loop.add(new Expression.Assign(fieldIndex, fieldIndexValue(expectedIndex, hashes, fieldHash))); + expressions.add(new Expression.While(Expression.Literal.True, loop)); + return expressions; + } + + private Expression readNextHashedField( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + int readerMode, + Expression object, + Expression hashes, + Expression expectedIndex, + boolean record) { + Expression fieldHash = readFieldNameHash(readerMode, "fieldHash"); + Expression fieldIndex = + new Expression.Variable("fieldIndex", fieldIndexValue(expectedIndex, hashes, fieldHash)); + return new Expression.ListExpression( + fieldHash, + fieldIndex, + expectExpr(readerMode, ':'), + fieldSwitch(builder, type, properties, readerMode, object, fieldIndex, record), + updateExpectedIndex(expectedIndex, fieldIndex)); + } + + private Expression fieldSwitch( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + int readerMode, + Expression object, + Expression fieldIndex, + boolean record) { + Expression.Switch.Case[] cases = new Expression.Switch.Case[properties.length]; + for (int i = 0; i < properties.length; i++) { + cases[i] = + new Expression.Switch.Case( + i, + new Expression.ListExpression( + readField(builder, type, properties[i], i, readerMode, object, record, false), + new Expression.Break())); + } + return new Expression.Switch( + fieldIndex, cases, new Expression.Invoke(readerRef(readerMode), "skipValue")); + } + + private Expression updateExpectedIndex(Expression expectedIndex, Expression fieldIndex) { + return new Expression.If( + ge(fieldIndex, Expression.Literal.ofInt(0)), + new Expression.Assign( + expectedIndex, new Expression.Add(fieldIndex, Expression.Literal.ofInt(1)))); + } + + private Expression fieldIndexValue( + Expression expectedIndex, Expression hashes, Expression fieldHash) { + return new Expression.Ternary( + and( + lt( + expectedIndex, + new Expression.FieldValue(hashes, "length", TypeRef.of(int.class), false, true)), + eq(fieldHash, new Expression.ArrayValue(hashes, expectedIndex))), + expectedIndex, + fieldIndexInvoke(fieldHash), + true, + TypeRef.of(int.class)); + } + + private static Expression objectParam(Class type, boolean record) { + return record + ? new Reference("object", TypeRef.of(Object[].class)) + : new Reference("object", TypeRef.of(type)); + } + + private static Reference ownerRef() { + return new Reference("owner", TypeRef.of(BaseObjectCodec.class)); + } + + private static Reference typeResolverRef() { + return new Reference("typeResolver", TypeRef.of(JsonTypeResolver.class)); + } + + private static Reference fieldRef(String name, Class type) { + return Reference.fieldRef(name, TypeRef.of(type)); + } + + private static Reference readerRef(int readerMode) { + return new Reference("reader", TypeRef.of(readerClass(readerMode))); + } + + private static Class readerClass(int readerMode) { + switch (readerMode) { + case LATIN1_READER: + return Latin1JsonReader.class; + case UTF16_READER: + return Utf16JsonReader.class; + case UTF8_READER: + return Utf8JsonReader.class; + default: + return JsonReader.class; + } + } + + private static Expression expectExpr(int readerMode, char token) { + return new Expression.Invoke( + readerRef(readerMode), + readerMode == GENERIC_READER ? "expect" : "expectNextToken", + Expression.Literal.ofChar(token)); + } + + private static Expression consumeExpr(int readerMode, char token) { + return new Expression.Invoke( + readerRef(readerMode), + readerMode == GENERIC_READER ? "consume" : "consumeNextToken", + TypeRef.of(boolean.class), + Expression.Literal.ofChar(token)) + .inline(); + } + + private static Expression consumeCommaOrEndObjectExpr(int readerMode) { + return new Expression.Invoke( + readerRef(readerMode), + readerMode == GENERIC_READER + ? "consumeCommaOrEndObject" + : "consumeNextCommaOrEndObject", + TypeRef.of(boolean.class)) + .inline(); + } + + private static Expression tryReadNullExpr(int readerMode) { + return new Expression.Invoke( + readerRef(readerMode), + readerMode == GENERIC_READER ? "tryReadNull" : "tryReadNextNullToken", + TypeRef.of(boolean.class)) + .inline(); + } + + private static Expression readBooleanExpr(int readerMode) { + return readBooleanExpr(readerMode, false); + } + + private static Expression readBooleanExpr(int readerMode, boolean tokenValueRead) { + return new Expression.Invoke( + readerRef(readerMode), + readBooleanMethod(readerMode, tokenValueRead), + TypeRef.of(boolean.class)) + .inline(); + } + + private static Expression readIntExpr(int readerMode) { + return readIntExpr(readerMode, false); + } + + private static Expression readIntExpr(int readerMode, boolean tokenValueRead) { + return new Expression.Invoke( + readerRef(readerMode), readIntMethod(readerMode, tokenValueRead), TypeRef.of(int.class)) + .inline(); + } + + private static Expression readLongExpr(int readerMode) { + return readLongExpr(readerMode, false); + } + + private static Expression readLongExpr(int readerMode, boolean tokenValueRead) { + return new Expression.Invoke( + readerRef(readerMode), + readLongMethod(readerMode, tokenValueRead), + TypeRef.of(long.class)) + .inline(); + } + + private static Expression readStringExpr(int readerMode) { + return readStringExpr(readerMode, false); + } + + private static Expression readStringExpr(int readerMode, boolean tokenValueRead) { + return new Expression.Invoke( + readerRef(readerMode), + readStringMethod(readerMode, tokenValueRead), + TypeRef.of(String.class), + true) + .inline(); + } + + private static String readBooleanMethod(int readerMode, boolean tokenValueRead) { + if (readerMode == GENERIC_READER) { + return "readBoolean"; + } + return tokenValueRead ? "readBooleanTokenValue" : "readNextBooleanValue"; + } + + private static String readIntMethod(int readerMode, boolean tokenValueRead) { + if (readerMode == GENERIC_READER) { + return "readInt"; + } + return tokenValueRead ? "readIntTokenValue" : "readNextIntValue"; + } + + private static String readLongMethod(int readerMode, boolean tokenValueRead) { + if (readerMode == GENERIC_READER) { + return "readLong"; + } + return tokenValueRead ? "readLongTokenValue" : "readNextLongValue"; + } + + private static String readStringMethod(int readerMode, boolean tokenValueRead) { + if (readerMode == GENERIC_READER) { + return "readNullableString"; + } + return tokenValueRead ? "readNullableStringToken" : "readNextNullableString"; + } + + private static boolean usesTokenValueRead(int readerMode) { + return readerMode == LATIN1_READER || readerMode == UTF8_READER; + } + + private static Expression readFieldNameHash(int readerMode, String namePrefix) { + return new Expression.Invoke( + readerRef(readerMode), "readFieldNameHash", namePrefix, TypeRef.of(long.class), false); + } + + private static Expression fieldIndexInvoke(Expression fieldHash) { + return new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + "fieldIndex", + "", + TypeRef.of(int.class), + false, + false, + fieldHash) + .inline(); + } + + private static Expression tryReadNextFieldNameColon(int readerMode, JsonFieldInfo property) { + if (readerMode == LATIN1_READER || readerMode == UTF8_READER) { + String name = property.name(); + String token = fieldNameToken(name); + int tokenLength = token.length(); + int suffixLength = JsonAsciiToken.suffixLength(tokenLength); + // This is a compact-JSON fast path. Whitespace, escapes, and UTF8 spellings that do not + // match the raw token fall through to the generated field-hash reader without consuming. + if (suffixLength == 0) { + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextFieldNameToken0", + TypeRef.of(boolean.class), + Expression.Literal.ofLong(JsonAsciiToken.prefix(token)), + Expression.Literal.ofLong(JsonAsciiToken.prefixMask(tokenLength)), + Expression.Literal.ofInt(tokenLength)) + .inline(); + } + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextFieldNameToken" + suffixLength, + TypeRef.of(boolean.class), + Expression.Literal.ofLong(JsonAsciiToken.prefix(token)), + Expression.Literal.ofLong(JsonAsciiToken.prefixMask(tokenLength)), + Expression.Literal.ofInt(JsonAsciiToken.suffix(token)), + Expression.Literal.ofInt(tokenLength)) + .inline(); + } + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextFieldNameColon", + TypeRef.of(boolean.class), + Expression.Literal.ofLong(property.nameHash()), + Expression.Literal.ofLong(packedNameMask(property.name().length())), + Expression.Literal.ofInt(property.name().length())) + .inline(); + } + + private static Expression tryReadNextStringToken(int readerMode, String token) { + int tokenLength = token.length(); + int suffixLength = JsonAsciiToken.suffixLength(tokenLength); + if (suffixLength == 0) { + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextStringToken0", + TypeRef.of(boolean.class), + Expression.Literal.ofLong(JsonAsciiToken.prefix(token)), + Expression.Literal.ofLong(JsonAsciiToken.prefixMask(tokenLength)), + Expression.Literal.ofInt(tokenLength)) + .inline(); + } + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextStringToken" + suffixLength, + TypeRef.of(boolean.class), + Expression.Literal.ofLong(JsonAsciiToken.prefix(token)), + Expression.Literal.ofLong(JsonAsciiToken.prefixMask(tokenLength)), + Expression.Literal.ofInt(JsonAsciiToken.suffix(token)), + Expression.Literal.ofInt(tokenLength)) + .inline(); + } + + private static String fieldNameToken(String name) { + return "\"" + name + "\":"; + } + + private static Expression slowCall( + String slowMethod, Expression object, Expression expectedIndex) { + return slowCall(slowMethod, object, expectedIndex, null); + } + + private static Expression slowCall( + String slowMethod, Expression object, Expression expectedIndex, Expression firstFieldIndex) { + if (firstFieldIndex == null) { + return new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + slowMethod, + "", + TypeRef.of(void.class), + false, + false, + readerRefForCall(), + ownerRef(), + typeResolverRef(), + object, + expectedIndex); + } + return new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + slowMethod, + "", + TypeRef.of(void.class), + false, + false, + readerRefForCall(), + ownerRef(), + typeResolverRef(), + object, + expectedIndex, + firstFieldIndex); + } + + private static Reference readerRefForCall() { + return new Reference("reader"); + } + + private static Expression arrayValue(Expression array, int index) { + return new Expression.ArrayValue(array, Expression.Literal.ofInt(index)); + } + + private static Expression eq(Expression left, Expression right) { + return new Expression.Comparator("==", left, right, true); + } + + private static Expression ne(Expression left, Expression right) { + return new Expression.Comparator("!=", left, right, true); + } + + private static Expression lt(Expression left, Expression right) { + return new Expression.Comparator("<", left, right, true); + } + + private static Expression ge(Expression left, Expression right) { + return new Expression.Comparator(">=", left, right, true); + } + + private static Expression and(Expression left, Expression right) { + return new Expression.LogicalAnd(left, right); + } + + private static Expression not(Expression expression) { + return new Expression.Not(expression); + } + + private static boolean usesWriteCodec(JsonFieldInfo property) { + switch (property.writeKind()) { + case ARRAY: + case MAP: + case OBJECT: + return true; + case COLLECTION: + return property.writeElementRawType() != String.class; + default: + return false; + } + } + + private static boolean usesReadCodec(JsonFieldInfo property) { + switch (property.readKind()) { + case ENUM: + case ARRAY: + case COLLECTION: + case MAP: + return true; + default: + return false; + } + } + + private static boolean usesReadTypeField(JsonFieldInfo property) { + switch (property.readKind()) { + case ARRAY: + case COLLECTION: + case MAP: + return true; + case OBJECT: + return usesReadObjectCodec(property); + default: + return false; + } + } + + private static boolean usesReadObjectCodec(JsonFieldInfo property) { + return property.readKind() == JsonFieldKind.OBJECT + && property.readRawType() != Object.class + && property.readTypeInfo().codec() instanceof BaseObjectCodec; + } + + private static boolean storesReadObjectCodec(Class type, JsonFieldInfo property) { + Class nestedType = readNestedType(property); + return nestedType != null && nestedType != type; + } + + private Expression readField( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo property, + int id, + int readerMode, + Expression object, + boolean record, + boolean tokenValueRead) { + if (record) { + return readRecordField(type, property, id, readerMode, object, tokenValueRead); + } + Class rawType = property.readRawType(); + switch (property.readKind()) { + case BOOLEAN: + return readBoolean(builder, property, rawType, readerMode, object, tokenValueRead); + case INT: + return readInt(builder, property, rawType, readerMode, object, tokenValueRead); + case LONG: + return readLong(builder, property, rawType, readerMode, object, tokenValueRead); + case STRING: + return builder.setField(property, object, readStringExpr(readerMode, tokenValueRead)); + case ENUM: + return readEnum(builder, property, id, readerMode, object, tokenValueRead); + case COLLECTION: + return readCollection(builder, property, id, readerMode, object); + case ARRAY: + case MAP: + return readResolvedField(builder, property, id, readerMode, object); + case OBJECT: + return readObject(builder, type, property, id, readerMode, object); + default: + return new Expression.Invoke( + fieldRef("p" + id, JsonFieldInfo.class), + "read", + readerRef(readerMode), + object, + typeResolverRef()); + } + } + + private Expression readRecordField( + Class type, + JsonFieldInfo property, + int id, + int readerMode, + Expression object, + boolean tokenValueRead) { + Class rawType = property.readRawType(); + switch (property.readKind()) { + case BOOLEAN: + return readRecordBoolean(rawType, id, readerMode, object, tokenValueRead); + case INT: + return readRecordInt(rawType, id, readerMode, object, tokenValueRead); + case LONG: + return readRecordLong(rawType, id, readerMode, object, tokenValueRead); + case STRING: + return assignRecord(object, id, readStringExpr(readerMode, tokenValueRead)); + case ENUM: + return readRecordEnum(id, readerMode, object, tokenValueRead); + case COLLECTION: + return readRecordCollection(property, id, readerMode, object); + case ARRAY: + case MAP: + return assignRecord(object, id, readResolvedValue(property, id, readerMode)); + case OBJECT: + return readRecordObject(type, property, id, readerMode, object); + default: + return assignRecord( + object, + id, + new Expression.Invoke( + fieldRef("p" + id, JsonFieldInfo.class), + "readValue", + TypeRef.of(Object.class), + true, + readerRef(readerMode), + typeResolverRef())); + } + } + + private static Expression readRecordBoolean( + Class rawType, int id, int readerMode, Expression object, boolean tokenValueRead) { + Expression value = box(Boolean.class, readBooleanExpr(readerMode, tokenValueRead)); + if (rawType.isPrimitive()) { + return assignRecord(object, id, value); + } + return new Expression.If( + tryReadNullExpr(readerMode), + assignRecord(object, id, new Expression.Null(TypeRef.of(Boolean.class), false)), + assignRecord(object, id, value)); + } + + private static Expression readRecordInt( + Class rawType, int id, int readerMode, Expression object, boolean tokenValueRead) { + Expression value = box(Integer.class, readIntExpr(readerMode, tokenValueRead)); + if (rawType.isPrimitive()) { + return assignRecord(object, id, value); + } + return new Expression.If( + tryReadNullExpr(readerMode), + assignRecord(object, id, new Expression.Null(TypeRef.of(Integer.class), false)), + assignRecord(object, id, value)); + } + + private static Expression readRecordLong( + Class rawType, int id, int readerMode, Expression object, boolean tokenValueRead) { + Expression value = box(Long.class, readLongExpr(readerMode, tokenValueRead)); + if (rawType.isPrimitive()) { + return assignRecord(object, id, value); + } + return new Expression.If( + tryReadNullExpr(readerMode), + assignRecord(object, id, new Expression.Null(TypeRef.of(Long.class), false)), + assignRecord(object, id, value)); + } + + private static Expression readRecordEnum( + int id, int readerMode, Expression object, boolean tokenValueRead) { + return new Expression.If( + tryReadNullExpr(readerMode), + assignRecord(object, id, new Expression.Null(TypeRef.of(Object.class), false)), + assignRecord(object, id, readEnumValue(Object.class, id, readerMode, tokenValueRead))); + } + + private static Expression readRecordObject( + Class type, JsonFieldInfo property, int id, int readerMode, Expression object) { + if (property.readRawType() == Object.class + || !(property.readTypeInfo().codec() instanceof BaseObjectCodec)) { + return assignRecord( + object, + id, + new Expression.Invoke( + fieldRef("p" + id, JsonFieldInfo.class), + "readValue", + TypeRef.of(Object.class), + true, + readerRef(readerMode), + typeResolverRef())); + } + return new Expression.If( + tryReadNullExpr(readerMode), + assignRecord(object, id, new Expression.Null(TypeRef.of(property.readRawType()), false)), + assignRecord(object, id, readObjectValue(type, property, id, readerMode))); + } + + private static Expression readRecordCollection( + JsonFieldInfo property, int id, int readerMode, Expression object) { + if (readerMode == GENERIC_READER) { + return assignRecord(object, id, readResolvedValue(property, id, readerMode)); + } + return new Expression.If( + tryReadNullExpr(readerMode), + assignRecord(object, id, new Expression.Null(TypeRef.of(property.readRawType()), false)), + assignRecord(object, id, readCollectionValue(property, id, readerMode))); + } + + private static Expression readBoolean( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + Class rawType, + int readerMode, + Expression object, + boolean tokenValueRead) { + if (rawType.isPrimitive()) { + return builder.setField(property, object, readBooleanExpr(readerMode, tokenValueRead)); + } + return new Expression.If( + tryReadNullExpr(readerMode), + builder.setNull(property, object), + builder.setField( + property, object, box(Boolean.class, readBooleanExpr(readerMode, tokenValueRead)))); + } + + private static Expression readInt( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + Class rawType, + int readerMode, + Expression object, + boolean tokenValueRead) { + if (rawType.isPrimitive()) { + return builder.setField(property, object, readIntExpr(readerMode, tokenValueRead)); + } + return new Expression.If( + tryReadNullExpr(readerMode), + builder.setNull(property, object), + builder.setField( + property, object, box(Integer.class, readIntExpr(readerMode, tokenValueRead)))); + } + + private static Expression readLong( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + Class rawType, + int readerMode, + Expression object, + boolean tokenValueRead) { + if (rawType.isPrimitive()) { + return builder.setField(property, object, readLongExpr(readerMode, tokenValueRead)); + } + return new Expression.If( + tryReadNullExpr(readerMode), + builder.setNull(property, object), + builder.setField( + property, object, box(Long.class, readLongExpr(readerMode, tokenValueRead)))); + } + + private static Expression readEnum( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + int id, + int readerMode, + Expression object, + boolean tokenValueRead) { + return new Expression.If( + tryReadNullExpr(readerMode), + builder.setNull(property, object), + readEnumField(builder, property, id, readerMode, object, tokenValueRead)); + } + + private static Expression readEnumField( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + int id, + int readerMode, + Expression object, + boolean tokenValueRead) { + Expression fallback = + builder.setField( + property, + object, + readEnumValue(property.readRawType(), id, readerMode, tokenValueRead, true)); + if (!tokenValueRead || (readerMode != LATIN1_READER && readerMode != UTF8_READER)) { + return fallback; + } + Enum[] constants = (Enum[]) property.readRawType().getEnumConstants(); + for (int i = constants.length - 1; i >= 0; i--) { + Enum constant = constants[i]; + String token = "\"" + constant.name() + "\""; + if (!JsonAsciiToken.isPackable(token)) { + continue; + } + fallback = + new Expression.If( + tryReadNextStringToken(readerMode, token), + builder.setField(property, object, new Expression.EnumExpression(constant)), + fallback); + } + return fallback; + } + + private static Expression readResolvedField( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + int id, + int readerMode, + Expression object) { + return builder.setField(property, object, readResolvedValue(property, id, readerMode)); + } + + private static Expression readCollection( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + int id, + int readerMode, + Expression object) { + if (readerMode == GENERIC_READER) { + return readResolvedField(builder, property, id, readerMode, object); + } + return new Expression.If( + tryReadNullExpr(readerMode), + builder.setNull(property, object), + builder.setField(property, object, readCollectionValue(property, id, readerMode))); + } + + private static Expression readObject( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo property, + int id, + int readerMode, + Expression object) { + if (property.readRawType() == Object.class + || !(property.readTypeInfo().codec() instanceof BaseObjectCodec)) { + return new Expression.Invoke( + fieldRef("p" + id, JsonFieldInfo.class), + "read", + readerRef(readerMode), + object, + typeResolverRef()); + } + return new Expression.If( + tryReadNullExpr(readerMode), + builder.setNull(property, object), + builder.setField(property, object, readObjectValue(type, property, id, readerMode))); + } + + private static Expression assignRecord(Expression object, int id, Expression value) { + return new Expression.AssignArrayElem(object, value, Expression.Literal.ofInt(id)); + } + + private static Expression box(Class boxedType, Expression value) { + return new Expression.StaticInvoke( + boxedType, "valueOf", "", TypeRef.of(boxedType), false, true, false, value); + } + + private static Expression readEnumValue( + Class enumType, int id, int readerMode, boolean tokenValueRead) { + return readEnumValue(enumType, id, readerMode, tokenValueRead, false); + } + + private static Expression readEnumValue( + Class enumType, int id, int readerMode, boolean tokenValueRead, boolean hashFallback) { + return new Expression.Cast( + new Expression.Invoke( + fieldRef("r" + id, JsonCodec.class), + readEnumMethod(readerMode, tokenValueRead, hashFallback), + "", + TypeRef.of(Object.class), + true, + false, + readerRef(readerMode)), + TypeRef.of(enumType)); + } + + private static Expression readResolvedValue(JsonFieldInfo property, int id, int readerMode) { + return new Expression.Cast( + new Expression.Invoke( + fieldRef("r" + id, JsonCodec.class), + readObjectMethod(readerMode), + TypeRef.of(Object.class), + true, + readerRef(readerMode), + fieldRef("t" + id, JsonTypeInfo.class), + typeResolverRef()), + TypeRef.of(property.readRawType())); + } + + private static Expression readCollectionValue(JsonFieldInfo property, int id, int readerMode) { + return new Expression.Cast( + new Expression.Invoke( + fieldRef("r" + id, CollectionCodec.class), + readObjectNonNullMethod(readerMode), + TypeRef.of(Object.class), + true, + readerRef(readerMode), + fieldRef("t" + id, JsonTypeInfo.class), + typeResolverRef()), + TypeRef.of(property.readRawType())); + } + + private static Expression readObjectValue( + Class type, JsonFieldInfo property, int id, int readerMode) { + Expression codec = + property.readRawType() == type ? ownerRef() : fieldRef("c" + id, BaseObjectCodec.class); + return new Expression.Cast( + new Expression.Invoke( + codec, + readObjectNonNullMethod(readerMode), + TypeRef.of(Object.class), + true, + readerRef(readerMode), + fieldRef("t" + id, JsonTypeInfo.class), + typeResolverRef()), + TypeRef.of(property.readRawType())); + } + + private static String readEnumMethod(int readerMode, boolean tokenValueRead) { + return readEnumMethod(readerMode, tokenValueRead, false); + } + + private static String readEnumMethod( + int readerMode, boolean tokenValueRead, boolean hashFallback) { + switch (readerMode) { + case LATIN1_READER: + return tokenValueRead + ? (hashFallback ? "readLatin1EnumHashToken" : "readLatin1EnumToken") + : "readNextLatin1Enum"; + case UTF16_READER: + return "readNextUtf16Enum"; + case UTF8_READER: + return tokenValueRead + ? (hashFallback ? "readUtf8EnumHashToken" : "readUtf8EnumToken") + : "readNextUtf8Enum"; + default: + return "readEnum"; + } + } + + private static String readObjectMethod(int readerMode) { + switch (readerMode) { + case LATIN1_READER: + return "readLatin1"; + case UTF16_READER: + return "readUtf16"; + case UTF8_READER: + return "readUtf8"; + default: + return "read"; + } + } + + private static String readObjectNonNullMethod(int readerMode) { + switch (readerMode) { + case LATIN1_READER: + return "readLatin1NonNull"; + case UTF16_READER: + return "readUtf16NonNull"; + case UTF8_READER: + return "readUtf8NonNull"; + default: + return "read"; + } + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java new file mode 100644 index 0000000000..689dfa2951 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -0,0 +1,578 @@ +/* + * 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.codegen; + +import org.apache.fory.codegen.Code; +import org.apache.fory.codegen.CodegenContext; +import org.apache.fory.codegen.Expression; +import org.apache.fory.codegen.Expression.Reference; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.JsonCodec; +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.GeneratedObjectWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.StringObjectWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.json.writer.Utf8ObjectWriter; +import org.apache.fory.reflect.TypeRef; + +final class JsonWriterCodegen { + private final JsonCodegen codegen; + private final boolean writeNullFields; + + JsonWriterCodegen(JsonCodegen codegen) { + this.codegen = codegen; + this.writeNullFields = codegen.writeNullFields; + } + + private Class codecFieldType(JsonCodec codec) { + return codegen.codecFieldType(codec); + } + + private static boolean usesWriteCodec(JsonFieldInfo property) { + return JsonCodegen.usesWriteCodec(property); + } + + private static Reference typeResolverRef() { + return new Reference("typeResolver", TypeRef.of(JsonTypeResolver.class)); + } + + private static Reference fieldRef(String name, Class type) { + return Reference.fieldRef(name, TypeRef.of(type)); + } + + private static Expression eq(Expression left, Expression right) { + return new Expression.Comparator("==", left, right, true); + } + + private static Expression ne(Expression left, Expression right) { + return new Expression.Comparator("!=", left, right, true); + } + + private static void addGeneratedConstructor( + CodegenContext ctx, Expression expression, Object... params) { + ctx.clearExprState(); + Code.ExprCode body = expression.genCode(ctx); + String code = body.code(); + code = code == null ? "" : ctx.optimizeMethodCode(code); + ctx.addConstructor(code, params); + } + + private static void addGeneratedMethod( + CodegenContext ctx, + String modifier, + String name, + Expression expression, + Class returnType, + Object... params) { + ctx.clearExprState(); + Code.ExprCode body = expression.genCode(ctx); + String code = body.code(); + code = code == null ? "" : ctx.optimizeMethodCode(code); + ctx.addMethod(modifier, name, code, returnType, params); + } + + String genWriterCode( + JsonGeneratedCodecBuilder builder, + String className, + Class type, + JsonFieldInfo[] properties, + boolean utf8) { + CodegenContext ctx = builder.context(); + ctx.addImports(StringJsonWriter.class, Utf8JsonWriter.class); + ctx.extendsClasses(ctx.type(GeneratedObjectWriter.class)); + ctx.implementsInterfaces(ctx.type(utf8 ? Utf8ObjectWriter.class : StringObjectWriter.class)); + boolean objectStartFused = canFuseObjectStart(properties); + boolean[] useInfo = new boolean[properties.length]; + boolean[] usePrefix = new boolean[properties.length]; + for (int i = 0; i < properties.length; i++) { + JsonFieldInfo property = properties[i]; + useInfo[i] = true; + usePrefix[i] = usesPrefix(property); + if (useInfo[i]) { + ctx.addField(JsonFieldInfo.class, "p" + i); + } + if (usesWriteCodec(property)) { + ctx.addField(codecFieldType(property.writeTypeInfo().codec()), "c" + i); + } + if (usePrefix[i]) { + if (utf8) { + ctx.addField(byte[].class, "u" + i); + ctx.addField(byte[].class, "uc" + i); + } else { + ctx.addField(byte[].class, "s" + i); + ctx.addField(byte[].class, "sc" + i); + } + } + } + addGeneratedConstructor( + ctx, + writerConstructorExpression(properties, utf8), + JsonFieldInfo[].class, + "properties", + JsonCodec[].class, + "codecs"); + addGeneratedMethod( + ctx, + "public", + utf8 ? "writeUtf8" : "writeString", + writeExpression(builder, type, properties, utf8, objectStartFused), + void.class, + utf8 ? Utf8JsonWriter.class : StringJsonWriter.class, + "writer", + Object.class, + "value", + JsonTypeResolver.class, + "typeResolver"); + return ctx.genCode(); + } + + private boolean usesPrefix(JsonFieldInfo property) { + JsonFieldKind kind = property.writeKind(); + return kind != JsonFieldKind.BOOLEAN && kind != JsonFieldKind.ENUM + || writeNullFields && !property.writeRawType().isPrimitive(); + } + + private Expression writerConstructorExpression(JsonFieldInfo[] properties, boolean utf8) { + Expression.ListExpression expressions = new Expression.ListExpression(); + Reference propertiesRef = new Reference("properties", TypeRef.of(JsonFieldInfo[].class)); + Reference codecsRef = new Reference("codecs", TypeRef.of(JsonCodec[].class)); + expressions.add(new Expression.SuperCall(propertiesRef, codecsRef)); + for (int i = 0; i < properties.length; i++) { + Expression id = Expression.Literal.ofInt(i); + Expression property = new Expression.ArrayValue(propertiesRef, id); + expressions.add( + new Expression.Assign( + new Reference("this.p" + i, TypeRef.of(JsonFieldInfo.class)), property)); + if (usesWriteCodec(properties[i])) { + Class codecType = codecFieldType(properties[i].writeTypeInfo().codec()); + expressions.add( + new Expression.Assign( + new Reference("this.c" + i, TypeRef.of(codecType)), + new Expression.Cast( + new Expression.ArrayValue(codecsRef, id), TypeRef.of(codecType)))); + } + if (usesPrefix(properties[i])) { + if (utf8) { + expressions.add( + new Expression.Assign( + new Reference("this.u" + i, TypeRef.of(byte[].class)), + new Expression.Invoke(property, "utf8NamePrefix", TypeRef.of(byte[].class)))); + expressions.add( + new Expression.Assign( + new Reference("this.uc" + i, TypeRef.of(byte[].class)), + new Expression.Invoke( + property, "utf8CommaNamePrefix", TypeRef.of(byte[].class)))); + } else { + expressions.add( + new Expression.Assign( + new Reference("this.s" + i, TypeRef.of(byte[].class)), + new Expression.Invoke(property, "stringNamePrefix", TypeRef.of(byte[].class)))); + expressions.add( + new Expression.Assign( + new Reference("this.sc" + i, TypeRef.of(byte[].class)), + new Expression.Invoke( + property, "stringCommaNamePrefix", TypeRef.of(byte[].class)))); + } + } + } + return expressions; + } + + private Expression writeExpression( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + boolean utf8, + boolean objectStartFused) { + Expression object = + new Expression.Variable( + "object", + new Expression.Cast( + new Reference("value", TypeRef.of(Object.class)), TypeRef.of(type))); + Expression.ListExpression expressions = new Expression.ListExpression(); + expressions.add(object); + Expression index = null; + if (!objectStartFused) { + expressions.add(new Expression.Invoke(writerRef(utf8), "writeObjectStart")); + index = new Expression.Variable("index", Expression.Literal.ofInt(0)); + expressions.add(index); + } + boolean commaKnown = objectStartFused; + for (int i = 0; i < properties.length; i++) { + if (objectStartFused && i == 0) { + expressions.add( + writeObjectStartPrimitive( + properties[i], builder.fieldValue(properties[i], object), utf8)); + } else { + expressions.add(writeProp(builder, properties[i], i, utf8, commaKnown, index, object)); + } + if (writeNullFields || properties[i].writeRawType().isPrimitive()) { + commaKnown = true; + } + } + expressions.add(new Expression.Invoke(writerRef(utf8), "writeObjectEnd")); + return expressions; + } + + private static boolean canFuseObjectStart(JsonFieldInfo[] properties) { + if (properties.length == 0 || !properties[0].writeRawType().isPrimitive()) { + return false; + } + switch (properties[0].writeKind()) { + case BYTE: + case SHORT: + case INT: + case LONG: + return true; + default: + return false; + } + } + + private static Expression writeObjectStartPrimitive( + JsonFieldInfo property, Expression value, boolean utf8) { + switch (property.writeKind()) { + case BYTE: + case SHORT: + case INT: + return new Expression.Invoke( + writerRef(utf8), "writeObjectIntField", prefixRef(utf8, false, 0), value); + case LONG: + return new Expression.Invoke( + writerRef(utf8), "writeObjectLongField", prefixRef(utf8, false, 0), value); + default: + throw new ForyJsonException( + "Unsupported generated object-start kind " + property.writeKind()); + } + } + + private Expression writeProp( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + int id, + boolean utf8, + boolean commaKnown, + Expression index, + Expression object) { + Class rawType = property.writeRawType(); + if (rawType.isPrimitive()) { + return writePrimitive( + property, id, builder.fieldValue(property, object), utf8, commaKnown, index); + } + Expression value = + new Expression.Variable( + "v" + id, + new Expression.Cast(builder.fieldValue(property, object), TypeRef.of(rawType))); + Expression nullValue = new Expression.Null(TypeRef.of(rawType), false); + if (writeNullFields) { + if (isPrefixValue(property.writeKind())) { + return new Expression.ListExpression( + value, + new Expression.If( + eq(value, nullValue), + new Expression.ListExpression( + writeFieldName(id, utf8, commaKnown, index), + new Expression.Invoke(writerRef(utf8), "writeNull")), + writeValue(property, id, value, utf8, commaKnown, index))); + } + return new Expression.ListExpression( + value, + writeFieldName(id, utf8, commaKnown, index), + new Expression.If( + eq(value, nullValue), + new Expression.Invoke(writerRef(utf8), "writeNull"), + writeValue(property, id, value, utf8, true, index))); + } + Expression write = + isPrefixValue(property.writeKind()) + ? writeValue(property, id, value, utf8, commaKnown, index) + : new Expression.ListExpression( + writeFieldName(id, utf8, commaKnown, index), + writeValue(property, id, value, utf8, true, index)); + return new Expression.ListExpression(value, new Expression.If(ne(value, nullValue), write)); + } + + private Expression writePrimitive( + JsonFieldInfo property, + int id, + Expression value, + boolean utf8, + boolean commaKnown, + Expression index) { + switch (property.writeKind()) { + case BOOLEAN: + return writeRawFieldValue( + utf8, commaKnown, index, booleanFieldValue(id, value, utf8, commaKnown, index)); + case BYTE: + case SHORT: + case INT: + return writeNumberField(id, value, false, utf8, commaKnown, index); + case LONG: + return writeNumberField(id, value, true, utf8, commaKnown, index); + default: + return new Expression.ListExpression( + writeFieldName(id, utf8, commaKnown, index), + writePrimitiveScalar(property.writeKind(), value, utf8)); + } + } + + private static Expression writeNumberField( + int id, + Expression value, + boolean longValue, + boolean utf8, + boolean commaKnown, + Expression index) { + String writerMethod = longValue ? "writeLongField" : "writeIntField"; + if (commaKnown) { + return new Expression.Invoke(writerRef(utf8), writerMethod, prefixRef(utf8, true, id), value); + } + Expression.ListExpression expressions = + new Expression.ListExpression( + new Expression.Invoke( + writerRef(utf8), + writerMethod, + prefixRef(utf8, false, id), + prefixRef(utf8, true, id), + index, + value)); + expressions.add(increment(index)); + return expressions; + } + + private static Expression writeStringField( + int id, Expression value, boolean utf8, boolean commaKnown, Expression index) { + if (commaKnown) { + return new Expression.Invoke( + writerRef(utf8), "writeStringField", prefixRef(utf8, true, id), value); + } + Expression.ListExpression expressions = + new Expression.ListExpression( + new Expression.Invoke( + writerRef(utf8), + "writeStringField", + prefixRef(utf8, false, id), + prefixRef(utf8, true, id), + index, + value)); + expressions.add(increment(index)); + return expressions; + } + + private static Expression writeFieldName( + int id, boolean utf8, boolean commaKnown, Expression index) { + Expression prefix = + commaKnown + ? prefixRef(utf8, true, id) + : new Expression.Ternary( + eq(index, Expression.Literal.ofInt(0)), + prefixRef(utf8, false, id), + prefixRef(utf8, true, id), + true, + TypeRef.of(byte[].class)); + Expression.ListExpression expressions = + new Expression.ListExpression( + new Expression.Invoke(writerRef(utf8), "writeRawValue", prefix)); + if (!commaKnown) { + expressions.add(increment(index)); + } + return expressions; + } + + private Expression writeValue( + JsonFieldInfo property, + int id, + Expression value, + boolean utf8, + boolean commaKnown, + Expression index) { + JsonFieldKind kind = property.writeKind(); + switch (kind) { + case BOOLEAN: + return writeRawFieldValue( + utf8, + commaKnown, + index, + booleanFieldValue( + id, + new Expression.Invoke(value, "booleanValue", TypeRef.of(boolean.class)).inline(), + utf8, + commaKnown, + index)); + case BYTE: + case SHORT: + case INT: + return writeNumberField( + id, + new Expression.Invoke(value, "intValue", TypeRef.of(int.class)).inline(), + false, + utf8, + commaKnown, + index); + case LONG: + return writeNumberField( + id, + new Expression.Invoke(value, "longValue", TypeRef.of(long.class)).inline(), + true, + utf8, + commaKnown, + index); + case STRING: + return writeStringField(id, value, utf8, commaKnown, index); + case ENUM: + return writeRawFieldValue( + utf8, commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index)); + case FLOAT: + case DOUBLE: + case CHAR: + return writeScalar(kind, value, utf8); + case ARRAY: + case MAP: + return writeCodec(id, value, utf8); + case COLLECTION: + if (property.writeElementRawType() == String.class) { + return writeStringCollection(value, utf8); + } + return writeCodec(id, value, utf8); + default: + return writeCodec(id, value, utf8); + } + } + + private static Expression writeRawFieldValue( + boolean utf8, boolean commaKnown, Expression index, Expression value) { + Expression.ListExpression expressions = + new Expression.ListExpression( + new Expression.Invoke(writerRef(utf8), "writeRawValue", value)); + if (!commaKnown) { + expressions.add(increment(index)); + } + return expressions; + } + + private static Expression booleanFieldValue( + int id, Expression value, boolean utf8, boolean commaKnown, Expression index) { + return new Expression.Invoke( + fieldRef("p" + id, JsonFieldInfo.class), + utf8 ? "utf8BooleanFieldValue" : "stringBooleanFieldValue", + TypeRef.of(byte[].class), + value, + commaFlag(commaKnown, index)) + .inline(); + } + + private static Expression enumFieldValue( + int id, Expression value, boolean utf8, boolean commaKnown, Expression index) { + return new Expression.Invoke( + fieldRef("p" + id, JsonFieldInfo.class), + utf8 ? "utf8EnumFieldValue" : "stringEnumFieldValue", + TypeRef.of(byte[].class), + value, + commaFlag(commaKnown, index)) + .inline(); + } + + private static Expression writeCodec(int id, Expression value, boolean utf8) { + return new Expression.Invoke( + fieldRef("c" + id, JsonCodec.class), + utf8 ? "writeUtf8" : "writeString", + writerRef(utf8), + value, + typeResolverRef()); + } + + private static Expression writeStringCollection(Expression value, boolean utf8) { + return new Expression.ListExpression( + new Expression.Invoke(writerRef(utf8), "writeArrayStart"), + new Expression.ForEach( + value, + TypeRef.of(String.class), + true, + (index, element) -> + new Expression.Invoke(writerRef(utf8), "writeStringElement", index, element)), + new Expression.Invoke(writerRef(utf8), "writeArrayEnd")); + } + + private Expression writeScalar(JsonFieldKind kind, Expression value, boolean utf8) { + switch (kind) { + case FLOAT: + return new Expression.Invoke( + writerRef(utf8), + "writeFloat", + new Expression.Invoke(value, "floatValue", TypeRef.of(float.class)).inline()); + case DOUBLE: + return new Expression.Invoke( + writerRef(utf8), + "writeDouble", + new Expression.Invoke(value, "doubleValue", TypeRef.of(double.class)).inline()); + case CHAR: + return new Expression.Invoke( + writerRef(utf8), + "writeChar", + new Expression.Invoke(value, "charValue", TypeRef.of(char.class)).inline()); + default: + throw new ForyJsonException("Unsupported generated scalar kind " + kind); + } + } + + private Expression writePrimitiveScalar(JsonFieldKind kind, Expression value, boolean utf8) { + switch (kind) { + case FLOAT: + return new Expression.Invoke(writerRef(utf8), "writeFloat", value); + case DOUBLE: + return new Expression.Invoke(writerRef(utf8), "writeDouble", value); + case CHAR: + return new Expression.Invoke(writerRef(utf8), "writeChar", value); + default: + throw new ForyJsonException("Unsupported generated primitive kind " + kind); + } + } + + private static Reference writerRef(boolean utf8) { + return new Reference( + "writer", TypeRef.of(utf8 ? Utf8JsonWriter.class : StringJsonWriter.class)); + } + + private static Reference prefixRef(boolean utf8, boolean comma, int id) { + String prefix = utf8 ? (comma ? "uc" : "u") : (comma ? "sc" : "s"); + return fieldRef(prefix + id, byte[].class); + } + + private static Expression commaFlag(boolean commaKnown, Expression index) { + return commaKnown ? Expression.Literal.True : ne(index, Expression.Literal.ofInt(0)); + } + + private static Expression increment(Expression value) { + return new Expression.Assign(value, new Expression.Add(value, Expression.Literal.ofInt(1))); + } + + private static boolean isPrefixValue(JsonFieldKind kind) { + return kind == JsonFieldKind.BOOLEAN + || kind == JsonFieldKind.BYTE + || kind == JsonFieldKind.SHORT + || kind == JsonFieldKind.INT + || kind == JsonFieldKind.LONG + || kind == JsonFieldKind.STRING + || kind == JsonFieldKind.ENUM; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java new file mode 100644 index 0000000000..44b504e865 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java @@ -0,0 +1,67 @@ +/* + * 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; + +public final class JsonAsciiToken { + private static final int MAX_SUFFIX_LENGTH = 3; + + private JsonAsciiToken() {} + + public static boolean isPackable(String token) { + int length = token.length(); + if (length == 0 || suffixLength(length) > MAX_SUFFIX_LENGTH) { + return false; + } + for (int i = 0; i < length; i++) { + char ch = token.charAt(i); + if (ch == 0 || ch > 0xFF) { + return false; + } + } + return true; + } + + public static long prefix(String token) { + int prefixLength = Math.min(token.length(), Long.BYTES); + long value = 0; + for (int i = 0; i < prefixLength; i++) { + value |= (long) (token.charAt(i) & 0xFF) << (i << 3); + } + return value; + } + + public static long prefixMask(int tokenLength) { + int prefixLength = Math.min(tokenLength, Long.BYTES); + return prefixLength == Long.BYTES ? -1L : (1L << (prefixLength << 3)) - 1; + } + + public static int suffix(String token) { + int suffixLength = suffixLength(token.length()); + int value = 0; + for (int i = 0; i < suffixLength; i++) { + value |= (token.charAt(i + Long.BYTES) & 0xFF) << (i << 3); + } + return value; + } + + public static int suffixLength(int tokenLength) { + return Math.max(0, tokenLength - Long.BYTES); + } +} 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 new file mode 100644 index 0000000000..5bed687f76 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java @@ -0,0 +1,213 @@ +/* + * 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.Field; +import org.apache.fory.reflect.FieldAccessor; + +public abstract class JsonFieldAccessor { + public Object getObject(Object target) { + throw new UnsupportedOperationException(); + } + + public abstract Field field(); + + public abstract FieldAccessor coreAccessor(); + + public boolean getBoolean(Object target) { + return (Boolean) getObject(target); + } + + public byte getByte(Object target) { + return (Byte) getObject(target); + } + + public short getShort(Object target) { + return (Short) getObject(target); + } + + public int getInt(Object target) { + return (Integer) getObject(target); + } + + public long getLong(Object target) { + return (Long) getObject(target); + } + + public float getFloat(Object target) { + return (Float) getObject(target); + } + + public double getDouble(Object target) { + return (Double) getObject(target); + } + + public char getChar(Object target) { + return (Character) getObject(target); + } + + public void putObject(Object target, Object value) { + throw new UnsupportedOperationException(); + } + + public void putBoolean(Object target, boolean value) { + putObject(target, value); + } + + public void putByte(Object target, byte value) { + putObject(target, value); + } + + public void putShort(Object target, short value) { + putObject(target, value); + } + + public void putInt(Object target, int value) { + putObject(target, value); + } + + public void putLong(Object target, long value) { + putObject(target, value); + } + + public void putFloat(Object target, float value) { + putObject(target, value); + } + + public void putDouble(Object target, double value) { + putObject(target, value); + } + + public void putChar(Object target, char value) { + putObject(target, value); + } + + public static JsonFieldAccessor forField(Field field) { + return new FieldJsonAccessor(FieldAccessor.createAccessor(field)); + } + + private static final class FieldJsonAccessor extends JsonFieldAccessor { + private final FieldAccessor accessor; + + private FieldJsonAccessor(FieldAccessor accessor) { + this.accessor = accessor; + } + + @Override + public FieldAccessor coreAccessor() { + return accessor; + } + + @Override + public Field field() { + return accessor.getField(); + } + + @Override + public Object getObject(Object target) { + return accessor.getObject(target); + } + + @Override + public boolean getBoolean(Object target) { + return accessor.getBoolean(target); + } + + @Override + public byte getByte(Object target) { + return accessor.getByte(target); + } + + @Override + public short getShort(Object target) { + return accessor.getShort(target); + } + + @Override + public int getInt(Object target) { + return accessor.getInt(target); + } + + @Override + public long getLong(Object target) { + return accessor.getLong(target); + } + + @Override + public float getFloat(Object target) { + return accessor.getFloat(target); + } + + @Override + public double getDouble(Object target) { + return accessor.getDouble(target); + } + + @Override + public char getChar(Object target) { + return accessor.getChar(target); + } + + @Override + public void putObject(Object target, Object value) { + accessor.putObject(target, value); + } + + @Override + public void putBoolean(Object target, boolean value) { + accessor.putBoolean(target, value); + } + + @Override + public void putByte(Object target, byte value) { + accessor.putByte(target, value); + } + + @Override + public void putShort(Object target, short value) { + accessor.putShort(target, value); + } + + @Override + public void putInt(Object target, int value) { + accessor.putInt(target, value); + } + + @Override + public void putLong(Object target, long value) { + accessor.putLong(target, value); + } + + @Override + public void putFloat(Object target, float value) { + accessor.putFloat(target, value); + } + + @Override + public void putDouble(Object target, double value) { + accessor.putDouble(target, value); + } + + @Override + public void putChar(Object target, char value) { + accessor.putChar(target, value); + } + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java new file mode 100644 index 0000000000..01a31a374a --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -0,0 +1,1110 @@ +/* + * 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.Field; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Map; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.CodecUtils; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonStringEscaper; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; + +public final class JsonFieldInfo { + private static final int KIND_BOOLEAN = 1; + private static final int KIND_BYTE = 2; + private static final int KIND_SHORT = 3; + private static final int KIND_INT = 4; + private static final int KIND_LONG = 5; + private static final int KIND_FLOAT = 6; + private static final int KIND_DOUBLE = 7; + private static final int KIND_CHAR = 8; + private static final int KIND_STRING = 9; + private static final int KIND_ENUM = 10; + private static final int KIND_ARRAY = 11; + private static final int KIND_COLLECTION = 12; + private static final int KIND_MAP = 13; + private static final int KIND_OBJECT = 14; + private static final byte[] TRUE_BYTES = "true".getBytes(StandardCharsets.ISO_8859_1); + private static final byte[] FALSE_BYTES = "false".getBytes(StandardCharsets.ISO_8859_1); + + private final String name; + private final Field writeField; + private final Type writeType; + private final Class writeRawType; + private final Type readType; + private final Class readRawType; + private final JsonFieldKind writeKind; + private final JsonFieldKind readKind; + private final int writeKindId; + private final JsonFieldAccessor writeAccessor; + private final JsonFieldAccessor readAccessor; + private final Type writeElementType; + private final Type readElementType; + private final Type writeMapValueType; + private final Class writeArrayComponentType; + private final Class writeElementRawType; + private final Class readElementRawType; + private final byte[] stringNamePrefix; + private final byte[] stringCommaNamePrefix; + private final byte[] utf8NamePrefix; + private final byte[] utf8CommaNamePrefix; + private final byte[][] stringEnumValues; + private final byte[][] stringElementEnumValues; + private final byte[][] stringEnumNameValues; + private final byte[][] stringEnumCommaValues; + private final byte[][] utf8EnumValues; + private final byte[][] utf8ElementEnumValues; + private final byte[][] utf8EnumNameValues; + private final byte[][] utf8EnumCommaValues; + private final byte[] stringTrueNameToken; + private final byte[] stringTrueCommaToken; + private final byte[] stringFalseNameToken; + private final byte[] stringFalseCommaToken; + private final byte[] utf8TrueNameToken; + private final byte[] utf8TrueCommaToken; + private final byte[] utf8FalseNameToken; + private final byte[] utf8FalseCommaToken; + private final long nameHash; + private int readIndex = -1; + private JsonTypeInfo writeTypeInfo; + private JsonTypeInfo readTypeInfo; + private JsonTypeInfo readElementTypeInfo; + + public JsonFieldInfo( + String name, + Field writeField, + Field readField, + JsonFieldAccessor writeAccessor, + JsonFieldAccessor readAccessor) { + this.name = name; + nameHash = JsonFieldNameHash.hash(name); + this.writeField = writeField; + this.writeType = fieldType(writeField); + this.writeRawType = fieldRawType(writeField); + this.readType = fieldType(readField); + this.readRawType = fieldRawType(readField); + this.writeAccessor = writeAccessor; + this.readAccessor = readAccessor; + writeKind = writeRawType == null ? null : kind(writeRawType); + readKind = readRawType == null ? null : kind(readRawType); + writeKindId = writeKind == null ? 0 : kindId(writeKind); + writeElementType = + writeKind == JsonFieldKind.COLLECTION ? CodecUtils.elementType(writeType) : null; + readElementType = + readKind == JsonFieldKind.COLLECTION ? CodecUtils.elementType(readType) : null; + writeMapValueType = writeKind == JsonFieldKind.MAP ? CodecUtils.mapValueType(writeType) : null; + writeArrayComponentType = + writeKind == JsonFieldKind.ARRAY ? writeRawType.getComponentType() : null; + writeElementRawType = writeElementType == null ? null : knownRawType(writeElementType); + readElementRawType = readElementType == null ? null : knownRawType(readElementType); + String stringPrefix = JsonStringEscaper.escapedNamePrefix(name, true); + String utf8Prefix = JsonStringEscaper.escapedNamePrefix(name, false); + stringNamePrefix = stringPrefix.getBytes(StandardCharsets.ISO_8859_1); + stringCommaNamePrefix = ("," + stringPrefix).getBytes(StandardCharsets.ISO_8859_1); + utf8NamePrefix = utf8Prefix.getBytes(StandardCharsets.UTF_8); + utf8CommaNamePrefix = ("," + utf8Prefix).getBytes(StandardCharsets.UTF_8); + stringEnumValues = writeKind == JsonFieldKind.ENUM ? stringEnumValues(writeRawType) : null; + stringEnumNameValues = + writeKind == JsonFieldKind.ENUM ? fieldValues(stringNamePrefix, stringEnumValues) : null; + stringEnumCommaValues = + writeKind == JsonFieldKind.ENUM + ? fieldValues(stringCommaNamePrefix, stringEnumValues) + : null; + stringElementEnumValues = + writeElementRawType != null && writeElementRawType.isEnum() + ? stringEnumValues(writeElementRawType) + : null; + utf8EnumValues = writeKind == JsonFieldKind.ENUM ? enumValues(writeRawType) : null; + utf8EnumNameValues = + writeKind == JsonFieldKind.ENUM ? fieldValues(utf8NamePrefix, utf8EnumValues) : null; + utf8EnumCommaValues = + writeKind == JsonFieldKind.ENUM ? fieldValues(utf8CommaNamePrefix, utf8EnumValues) : null; + utf8ElementEnumValues = + writeElementRawType != null && writeElementRawType.isEnum() + ? enumValues(writeElementRawType) + : null; + if (writeKind == JsonFieldKind.BOOLEAN) { + stringTrueNameToken = join(stringNamePrefix, TRUE_BYTES); + stringTrueCommaToken = join(stringCommaNamePrefix, TRUE_BYTES); + stringFalseNameToken = join(stringNamePrefix, FALSE_BYTES); + stringFalseCommaToken = join(stringCommaNamePrefix, FALSE_BYTES); + utf8TrueNameToken = join(utf8NamePrefix, TRUE_BYTES); + utf8TrueCommaToken = join(utf8CommaNamePrefix, TRUE_BYTES); + utf8FalseNameToken = join(utf8NamePrefix, FALSE_BYTES); + utf8FalseCommaToken = join(utf8CommaNamePrefix, FALSE_BYTES); + } else { + stringTrueNameToken = null; + stringTrueCommaToken = null; + stringFalseNameToken = null; + stringFalseCommaToken = null; + utf8TrueNameToken = null; + utf8TrueCommaToken = null; + utf8FalseNameToken = null; + utf8FalseCommaToken = null; + } + } + + public String name() { + return name; + } + + public long nameHash() { + return nameHash; + } + + public Field writeField() { + return writeField; + } + + public Type writeType() { + return writeType; + } + + public Class writeRawType() { + return writeRawType; + } + + public JsonFieldKind writeKind() { + return writeKind; + } + + public JsonFieldAccessor writeAccessor() { + return writeAccessor; + } + + public Type writeElementType() { + return writeElementType; + } + + public Type readElementType() { + return readElementType; + } + + public Class writeElementRawType() { + return writeElementRawType; + } + + public Class readElementRawType() { + return readElementRawType; + } + + public Type writeMapValueType() { + return writeMapValueType; + } + + public Class writeArrayComponentType() { + return writeArrayComponentType; + } + + public Type readType() { + return readType; + } + + public Field readField() { + return readAccessor == null ? null : readAccessor.field(); + } + + public Class readRawType() { + return readRawType; + } + + public JsonFieldKind readKind() { + return readKind; + } + + public JsonFieldAccessor readAccessor() { + return readAccessor; + } + + private static Type fieldType(Field field) { + return field == null ? null : field.getGenericType(); + } + + private static Class fieldRawType(Field field) { + return field == null ? null : field.getType(); + } + + public void resolveTypes(JsonTypeResolver typeResolver) { + if (writeRawType != null) { + writeTypeInfo = typeResolver.getTypeInfo(writeType, writeRawType); + } + if (readRawType != null) { + readTypeInfo = typeResolver.getTypeInfo(readType, readRawType); + } + if (readElementRawType != null) { + readElementTypeInfo = typeResolver.getTypeInfo(readElementType, readElementRawType); + } + } + + public void read(JsonReader reader, Object object, JsonTypeResolver typeResolver) { + readTypeInfo.codec().readField(reader, object, readAccessor, readTypeInfo, typeResolver); + } + + public Object readValue(JsonReader reader, JsonTypeResolver typeResolver) { + return readTypeInfo.codec().read(reader, readTypeInfo, typeResolver); + } + + public int readIndex() { + return readIndex; + } + + public void setReadIndex(int readIndex) { + this.readIndex = readIndex; + } + + public JsonTypeInfo writeTypeInfo() { + return writeTypeInfo; + } + + public JsonTypeInfo readTypeInfo() { + return readTypeInfo; + } + + public JsonTypeInfo readElementTypeInfo() { + return readElementTypeInfo; + } + + public byte[] stringNamePrefix() { + return stringNamePrefix; + } + + public byte[] stringCommaNamePrefix() { + return stringCommaNamePrefix; + } + + public byte[] utf8NamePrefix() { + return utf8NamePrefix; + } + + public byte[] utf8CommaNamePrefix() { + return utf8CommaNamePrefix; + } + + public byte[] utf8EnumValue(Enum value) { + return utf8EnumValues[value.ordinal()]; + } + + public byte[] utf8EnumFieldValue(Enum value, boolean comma) { + return (comma ? utf8EnumCommaValues : utf8EnumNameValues)[value.ordinal()]; + } + + public byte[] utf8BooleanFieldValue(boolean value, boolean comma) { + return value + ? (comma ? utf8TrueCommaToken : utf8TrueNameToken) + : (comma ? utf8FalseCommaToken : utf8FalseNameToken); + } + + public byte[] stringEnumValue(Enum value) { + return stringEnumValues[value.ordinal()]; + } + + public byte[] stringEnumFieldValue(Enum value, boolean comma) { + return (comma ? stringEnumCommaValues : stringEnumNameValues)[value.ordinal()]; + } + + public byte[] stringBooleanFieldValue(boolean value, boolean comma) { + return value + ? (comma ? stringTrueCommaToken : stringTrueNameToken) + : (comma ? stringFalseCommaToken : stringFalseNameToken); + } + + public byte[] utf8ElementEnumValue(Enum value) { + return utf8ElementEnumValues[value.ordinal()]; + } + + public byte[] stringElementEnumValue(Enum value) { + return stringElementEnumValues[value.ordinal()]; + } + + public boolean write(JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + switch (writeKind) { + case BOOLEAN: + if (!writeRawType.isPrimitive()) { + return writeScalar(writer, object, index); + } + writer.writeComma(index); + writer.writeFieldName(this); + writer.writeBoolean(writeAccessor.getBoolean(object)); + return true; + case BYTE: + if (!writeRawType.isPrimitive()) { + return writeScalar(writer, object, index); + } + writer.writeComma(index); + writer.writeFieldName(this); + writer.writeInt(writeAccessor.getByte(object)); + return true; + case SHORT: + if (!writeRawType.isPrimitive()) { + return writeScalar(writer, object, index); + } + writer.writeComma(index); + writer.writeFieldName(this); + writer.writeInt(writeAccessor.getShort(object)); + return true; + case INT: + if (!writeRawType.isPrimitive()) { + return writeScalar(writer, object, index); + } + writer.writeComma(index); + writer.writeFieldName(this); + writer.writeInt(writeAccessor.getInt(object)); + return true; + case LONG: + if (!writeRawType.isPrimitive()) { + return writeScalar(writer, object, index); + } + writer.writeComma(index); + writer.writeFieldName(this); + writer.writeLong(writeAccessor.getLong(object)); + return true; + case FLOAT: + if (!writeRawType.isPrimitive()) { + return writeScalar(writer, object, index); + } + writer.writeComma(index); + writer.writeFieldName(this); + writer.writeFloat(writeAccessor.getFloat(object)); + return true; + case DOUBLE: + if (!writeRawType.isPrimitive()) { + return writeScalar(writer, object, index); + } + writer.writeComma(index); + writer.writeFieldName(this); + writer.writeDouble(writeAccessor.getDouble(object)); + return true; + case CHAR: + if (!writeRawType.isPrimitive()) { + return writeScalar(writer, object, index); + } + writer.writeComma(index); + writer.writeFieldName(this); + writer.writeChar(writeAccessor.getChar(object)); + return true; + case STRING: + return writeString(writer, object, index); + case ENUM: + return writeEnum(writer, object, index); + case ARRAY: + return writeArray(writer, object, typeResolver, index); + case COLLECTION: + return writeCollection(writer, object, typeResolver, index); + case MAP: + return writeMap(writer, object, typeResolver, index); + case OBJECT: + return writePojo(writer, object, typeResolver, index); + default: + return writeObject(writer, object, typeResolver, index); + } + } + + public boolean writeString( + StringJsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + switch (writeKindId) { + case KIND_BOOLEAN: + if (!writeRawType.isPrimitive()) { + return writeStringScalar(writer, object, index); + } + writer.writeRawValue(stringBooleanFieldValue(writeAccessor.getBoolean(object), index != 0)); + return true; + case KIND_BYTE: + if (!writeRawType.isPrimitive()) { + return writeStringScalar(writer, object, index); + } + writer.writeIntField( + stringNamePrefix, stringCommaNamePrefix, index, writeAccessor.getByte(object)); + return true; + case KIND_SHORT: + if (!writeRawType.isPrimitive()) { + return writeStringScalar(writer, object, index); + } + writer.writeIntField( + stringNamePrefix, stringCommaNamePrefix, index, writeAccessor.getShort(object)); + return true; + case KIND_INT: + if (!writeRawType.isPrimitive()) { + return writeStringScalar(writer, object, index); + } + writer.writeIntField( + stringNamePrefix, stringCommaNamePrefix, index, writeAccessor.getInt(object)); + return true; + case KIND_LONG: + if (!writeRawType.isPrimitive()) { + return writeStringScalar(writer, object, index); + } + writer.writeLongField( + stringNamePrefix, stringCommaNamePrefix, index, writeAccessor.getLong(object)); + return true; + case KIND_FLOAT: + if (!writeRawType.isPrimitive()) { + return writeStringScalar(writer, object, index); + } + writer.writeFieldName(this, index); + writer.writeFloat(writeAccessor.getFloat(object)); + return true; + case KIND_DOUBLE: + if (!writeRawType.isPrimitive()) { + return writeStringScalar(writer, object, index); + } + writer.writeFieldName(this, index); + writer.writeDouble(writeAccessor.getDouble(object)); + return true; + case KIND_CHAR: + if (!writeRawType.isPrimitive()) { + return writeStringScalar(writer, object, index); + } + writer.writeFieldName(this, index); + writer.writeChar(writeAccessor.getChar(object)); + return true; + case KIND_STRING: + return writeStringText(writer, object, index); + case KIND_ENUM: + return writeStringEnum(writer, object, index); + case KIND_ARRAY: + return writeStringArray(writer, object, typeResolver, index); + case KIND_COLLECTION: + return writeStringCollection(writer, object, typeResolver, index); + case KIND_MAP: + return writeStringMap(writer, object, typeResolver, index); + case KIND_OBJECT: + return writeStringPojo(writer, object, typeResolver, index); + default: + return writeObject(writer, object, typeResolver, index); + } + } + + private boolean writeString(JsonWriter writer, Object object, int index) { + String value = (String) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeComma(index); + writer.writeFieldName(this); + if (value == null) { + writer.writeNull(); + } else { + writer.writeString(value); + } + return true; + } + + public boolean writeUtf8( + Utf8JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + switch (writeKindId) { + case KIND_BOOLEAN: + if (!writeRawType.isPrimitive()) { + return writeUtf8Scalar(writer, object, index); + } + writer.writeRawValue(utf8BooleanFieldValue(writeAccessor.getBoolean(object), index != 0)); + return true; + case KIND_BYTE: + if (!writeRawType.isPrimitive()) { + return writeUtf8Scalar(writer, object, index); + } + writer.writeIntField( + utf8NamePrefix, utf8CommaNamePrefix, index, writeAccessor.getByte(object)); + return true; + case KIND_SHORT: + if (!writeRawType.isPrimitive()) { + return writeUtf8Scalar(writer, object, index); + } + writer.writeIntField( + utf8NamePrefix, utf8CommaNamePrefix, index, writeAccessor.getShort(object)); + return true; + case KIND_INT: + if (!writeRawType.isPrimitive()) { + return writeUtf8Scalar(writer, object, index); + } + writer.writeIntField( + utf8NamePrefix, utf8CommaNamePrefix, index, writeAccessor.getInt(object)); + return true; + case KIND_LONG: + if (!writeRawType.isPrimitive()) { + return writeUtf8Scalar(writer, object, index); + } + writer.writeLongField( + utf8NamePrefix, utf8CommaNamePrefix, index, writeAccessor.getLong(object)); + return true; + case KIND_FLOAT: + if (!writeRawType.isPrimitive()) { + return writeUtf8Scalar(writer, object, index); + } + writer.writeFieldName(this, index); + writer.writeFloat(writeAccessor.getFloat(object)); + return true; + case KIND_DOUBLE: + if (!writeRawType.isPrimitive()) { + return writeUtf8Scalar(writer, object, index); + } + writer.writeFieldName(this, index); + writer.writeDouble(writeAccessor.getDouble(object)); + return true; + case KIND_CHAR: + if (!writeRawType.isPrimitive()) { + return writeUtf8Scalar(writer, object, index); + } + writer.writeFieldName(this, index); + writer.writeChar(writeAccessor.getChar(object)); + return true; + case KIND_STRING: + return writeUtf8String(writer, object, index); + case KIND_ENUM: + return writeUtf8Enum(writer, object, index); + case KIND_ARRAY: + return writeUtf8Array(writer, object, typeResolver, index); + case KIND_COLLECTION: + return writeUtf8Collection(writer, object, typeResolver, index); + case KIND_MAP: + return writeUtf8Map(writer, object, typeResolver, index); + case KIND_OBJECT: + return writeUtf8Pojo(writer, object, typeResolver, index); + default: + return writeUtf8Object(writer, object, typeResolver, index); + } + } + + private boolean writeObject( + JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeComma(index); + writer.writeFieldName(this); + writeTypeInfo.codec().write(writer, value, typeResolver); + return true; + } + + private boolean writeScalar(JsonWriter writer, Object object, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeComma(index); + writer.writeFieldName(this); + writeScalarValue(writer, value); + return true; + } + + private boolean writeStringScalar(StringJsonWriter writer, Object object, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + if (value == null) { + writer.writeFieldName(this, index); + writer.writeNull(); + return true; + } + switch (writeKind) { + case BOOLEAN: + writer.writeRawValue(stringBooleanFieldValue(((Boolean) value).booleanValue(), index != 0)); + return true; + case BYTE: + writer.writeIntField( + stringNamePrefix, stringCommaNamePrefix, index, ((Byte) value).intValue()); + return true; + case SHORT: + writer.writeIntField( + stringNamePrefix, stringCommaNamePrefix, index, ((Short) value).intValue()); + return true; + case INT: + writer.writeIntField( + stringNamePrefix, stringCommaNamePrefix, index, ((Integer) value).intValue()); + return true; + case LONG: + writer.writeLongField( + stringNamePrefix, stringCommaNamePrefix, index, ((Long) value).longValue()); + return true; + default: + writer.writeFieldName(this, index); + writeScalarValue(writer, value); + return true; + } + } + + private boolean writeUtf8Scalar(Utf8JsonWriter writer, Object object, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + if (value == null) { + writer.writeFieldName(this, index); + writer.writeNull(); + return true; + } + switch (writeKind) { + case BOOLEAN: + writer.writeRawValue(utf8BooleanFieldValue(((Boolean) value).booleanValue(), index != 0)); + return true; + case BYTE: + writer.writeIntField(utf8NamePrefix, utf8CommaNamePrefix, index, ((Byte) value).intValue()); + return true; + case SHORT: + writer.writeIntField( + utf8NamePrefix, utf8CommaNamePrefix, index, ((Short) value).intValue()); + return true; + case INT: + writer.writeIntField( + utf8NamePrefix, utf8CommaNamePrefix, index, ((Integer) value).intValue()); + return true; + case LONG: + writer.writeLongField( + utf8NamePrefix, utf8CommaNamePrefix, index, ((Long) value).longValue()); + return true; + default: + writer.writeFieldName(this, index); + writeScalarValue(writer, value); + return true; + } + } + + private boolean writeStringText(StringJsonWriter writer, Object object, int index) { + String value = (String) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + if (value == null) { + writer.writeFieldName(this, index); + writer.writeNull(); + } else { + writer.writeStringField(stringNamePrefix, stringCommaNamePrefix, index, value); + } + return true; + } + + private boolean writeStringEnum(StringJsonWriter writer, Object object, int index) { + Enum value = (Enum) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + if (value == null) { + writer.writeFieldName(this, index); + writer.writeNull(); + } else { + writer.writeRawValue(stringEnumFieldValue(value, index != 0)); + } + return true; + } + + private boolean writeStringArray( + StringJsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeFieldName(this, index); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().writeString(writer, value, typeResolver); + } + return true; + } + + private boolean writeStringCollection( + StringJsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Collection value = (Collection) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeFieldName(this, index); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().writeString(writer, value, typeResolver); + } + return true; + } + + private boolean writeStringMap( + StringJsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Map value = (Map) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeFieldName(this, index); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().writeString(writer, value, typeResolver); + } + return true; + } + + private boolean writeStringPojo( + StringJsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeFieldName(this, index); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().writeString(writer, value, typeResolver); + } + return true; + } + + private void writeScalarValue(JsonWriter writer, Object value) { + if (value == null) { + writer.writeNull(); + return; + } + switch (writeKind) { + case BOOLEAN: + writer.writeBoolean(((Boolean) value).booleanValue()); + return; + case BYTE: + writer.writeInt(((Byte) value).intValue()); + return; + case SHORT: + writer.writeInt(((Short) value).intValue()); + return; + case INT: + writer.writeInt(((Integer) value).intValue()); + return; + case LONG: + writer.writeLong(((Long) value).longValue()); + return; + case FLOAT: + writer.writeFloat(((Float) value).floatValue()); + return; + case DOUBLE: + writer.writeDouble(((Double) value).doubleValue()); + return; + case CHAR: + writer.writeChar(((Character) value).charValue()); + return; + default: + throw new ForyJsonException("Not a scalar JSON field " + name); + } + } + + private boolean writeUtf8Object( + Utf8JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeFieldName(this, index); + writeTypeInfo.codec().writeUtf8(writer, value, typeResolver); + return true; + } + + private boolean writeUtf8String(Utf8JsonWriter writer, Object object, int index) { + String value = (String) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + if (value == null) { + writer.writeFieldName(this, index); + writer.writeNull(); + } else { + writer.writeStringField(utf8NamePrefix, utf8CommaNamePrefix, index, value); + } + return true; + } + + private boolean writeEnum(JsonWriter writer, Object object, int index) { + Enum value = (Enum) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeComma(index); + writer.writeFieldName(this); + if (value == null) { + writer.writeNull(); + } else { + writer.writeString(value.name()); + } + return true; + } + + private boolean writeUtf8Enum(Utf8JsonWriter writer, Object object, int index) { + Enum value = (Enum) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + if (value == null) { + writer.writeFieldName(this, index); + writer.writeNull(); + } else { + writer.writeRawValue(utf8EnumFieldValue(value, index != 0)); + } + return true; + } + + private boolean writeArray( + JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeComma(index); + writer.writeFieldName(this); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().write(writer, value, typeResolver); + } + return true; + } + + private boolean writeUtf8Array( + Utf8JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeFieldName(this, index); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().writeUtf8(writer, value, typeResolver); + } + return true; + } + + private boolean writeCollection( + JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Collection value = (Collection) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeComma(index); + writer.writeFieldName(this); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().write(writer, value, typeResolver); + } + return true; + } + + private boolean writeUtf8Collection( + Utf8JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Collection value = (Collection) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeFieldName(this, index); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().writeUtf8(writer, value, typeResolver); + } + return true; + } + + private boolean writeMap( + JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Map value = (Map) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeComma(index); + writer.writeFieldName(this); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().write(writer, value, typeResolver); + } + return true; + } + + private boolean writeUtf8Map( + Utf8JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Map value = (Map) writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeFieldName(this, index); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().writeUtf8(writer, value, typeResolver); + } + return true; + } + + private boolean writePojo( + JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeComma(index); + writer.writeFieldName(this); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().write(writer, value, typeResolver); + } + return true; + } + + private boolean writeUtf8Pojo( + Utf8JsonWriter writer, Object object, JsonTypeResolver typeResolver, int index) { + Object value = writeAccessor.getObject(object); + if (value == null && !writer.writeNullFields()) { + return false; + } + writer.writeFieldName(this, index); + if (value == null) { + writer.writeNull(); + } else { + writeTypeInfo.codec().writeUtf8(writer, value, typeResolver); + } + return true; + } + + private static JsonFieldKind kind(Class rawType) { + if (rawType == boolean.class || rawType == Boolean.class) { + return JsonFieldKind.BOOLEAN; + } else if (rawType == byte.class || rawType == Byte.class) { + return JsonFieldKind.BYTE; + } else if (rawType == short.class || rawType == Short.class) { + return JsonFieldKind.SHORT; + } else if (rawType == int.class || rawType == Integer.class) { + return JsonFieldKind.INT; + } else if (rawType == long.class || rawType == Long.class) { + return JsonFieldKind.LONG; + } else if (rawType == float.class || rawType == Float.class) { + return JsonFieldKind.FLOAT; + } else if (rawType == double.class || rawType == Double.class) { + return JsonFieldKind.DOUBLE; + } else if (rawType == char.class || rawType == Character.class) { + return JsonFieldKind.CHAR; + } else if (rawType == String.class) { + return JsonFieldKind.STRING; + } else if (rawType.isEnum()) { + return JsonFieldKind.ENUM; + } else if (rawType.isArray()) { + return JsonFieldKind.ARRAY; + } else if (java.util.Collection.class.isAssignableFrom(rawType)) { + return JsonFieldKind.COLLECTION; + } else if (java.util.Map.class.isAssignableFrom(rawType)) { + return JsonFieldKind.MAP; + } + return JsonFieldKind.OBJECT; + } + + private static int kindId(JsonFieldKind kind) { + switch (kind) { + case BOOLEAN: + return KIND_BOOLEAN; + case BYTE: + return KIND_BYTE; + case SHORT: + return KIND_SHORT; + case INT: + return KIND_INT; + case LONG: + return KIND_LONG; + case FLOAT: + return KIND_FLOAT; + case DOUBLE: + return KIND_DOUBLE; + case CHAR: + return KIND_CHAR; + case STRING: + return KIND_STRING; + case ENUM: + return KIND_ENUM; + case ARRAY: + return KIND_ARRAY; + case COLLECTION: + return KIND_COLLECTION; + case MAP: + return KIND_MAP; + case OBJECT: + return KIND_OBJECT; + default: + throw new ForyJsonException("Unsupported JSON field kind " + kind); + } + } + + private static Class knownRawType(Type type) { + Class rawType = CodecUtils.rawType(type, null); + return rawType == Object.class ? null : rawType; + } + + private static boolean isScalarType(Class rawType) { + return rawType == String.class + || rawType == Boolean.class + || rawType == Byte.class + || rawType == Short.class + || rawType == Integer.class + || rawType == Long.class + || rawType == Float.class + || rawType == Double.class + || rawType == Character.class + || rawType.isPrimitive() + || rawType.isArray() + || Collection.class.isAssignableFrom(rawType) + || Map.class.isAssignableFrom(rawType); + } + + private static byte[][] enumValues(Class enumType) { + Object[] constants = enumType.getEnumConstants(); + byte[][] values = new byte[constants.length][]; + for (Object constant : constants) { + Enum enumValue = (Enum) constant; + values[enumValue.ordinal()] = JsonStringEscaper.utf8Value(enumValue.name()); + } + return values; + } + + private static byte[][] stringEnumValues(Class enumType) { + Object[] constants = enumType.getEnumConstants(); + byte[][] values = new byte[constants.length][]; + for (Object constant : constants) { + Enum enumValue = (Enum) constant; + values[enumValue.ordinal()] = JsonStringEscaper.stringValue(enumValue.name()); + } + return values; + } + + private static byte[][] fieldValues(byte[] prefix, byte[][] values) { + byte[][] fieldValues = new byte[values.length][]; + for (int i = 0; i < values.length; i++) { + fieldValues[i] = join(prefix, values[i]); + } + return fieldValues; + } + + private static byte[] join(byte[] prefix, byte[] token) { + byte[] joined = new byte[prefix.length + token.length]; + System.arraycopy(prefix, 0, joined, 0, prefix.length); + System.arraycopy(token, 0, joined, prefix.length, token.length); + return joined; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java new file mode 100644 index 0000000000..9f3e59dbe9 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java @@ -0,0 +1,37 @@ +/* + * 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; + +public enum JsonFieldKind { + BOOLEAN, + BYTE, + SHORT, + INT, + LONG, + FLOAT, + DOUBLE, + CHAR, + STRING, + ENUM, + ARRAY, + COLLECTION, + MAP, + OBJECT +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldNameHash.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldNameHash.java new file mode 100644 index 0000000000..fcc950d424 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldNameHash.java @@ -0,0 +1,71 @@ +/* + * 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; + +public final class JsonFieldNameHash { + public static final long MAGIC_HASH_CODE = 0xcbf29ce484222325L; + public static final long MAGIC_PRIME = 0x100000001b3L; + + private JsonFieldNameHash() {} + + public static long hash(String name) { + int length = name.length(); + if (length > 0 && length <= Long.BYTES) { + boolean latin1 = true; + long value = 0; + for (int i = 0; i < length; i++) { + char ch = name.charAt(i); + if (ch > 0xFF || ch == 0) { + latin1 = false; + break; + } + value |= ((long) ch) << (i << 3); + } + if (latin1 && value != 0) { + return value; + } + } + long hash = MAGIC_HASH_CODE; + for (int i = 0; i < length; i++) { + hash = update(hash, name.charAt(i)); + } + return hash; + } + + public static long update(long hash, char ch) { + return (hash ^ ch) * MAGIC_PRIME; + } + + public static long value(long value, int length, char ch) { + return value | (((long) ch) << (length << 3)); + } + + public static long hashPacked(long value, int length) { + long hash = MAGIC_HASH_CODE; + for (int i = 0; i < length; i++) { + hash = update(hash, (char) ((value >>> (i << 3)) & 0xFF)); + } + return hash; + } + + public static long finish(long hash, long value, int length, boolean latin1) { + return latin1 && length > 0 && length <= Long.BYTES && value != 0 ? value : hash; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldTable.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldTable.java new file mode 100644 index 0000000000..e11bd58829 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldTable.java @@ -0,0 +1,102 @@ +/* + * 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 org.apache.fory.json.ForyJsonException; + +public final class JsonFieldTable { + private final String[] tableNames; + private final long[] tableHashes; + private final JsonFieldInfo[] tableFields; + private final int[] tableIndexes; + private final int tableMask; + + public JsonFieldTable(JsonFieldInfo[] readFields) { + int tableSize = 1; + while (tableSize < readFields.length * 4) { + tableSize <<= 1; + } + tableNames = new String[tableSize]; + tableHashes = new long[tableSize]; + tableFields = new JsonFieldInfo[tableSize]; + tableIndexes = new int[tableSize]; + tableMask = tableSize - 1; + for (int i = 0; i < readFields.length; i++) { + JsonFieldInfo field = readFields[i]; + put(field, i); + } + } + + public JsonFieldInfo get(long hash) { + JsonFieldInfo[] localFields = tableFields; + long[] localHashes = tableHashes; + int mask = tableMask; + int index = index(hash, mask); + while (true) { + JsonFieldInfo field = localFields[index]; + if (field == null) { + return null; + } + if (localHashes[index] == hash) { + return field; + } + index = (index + 1) & mask; + } + } + + public int index(long hash) { + long[] localHashes = tableHashes; + int[] localIndexes = tableIndexes; + JsonFieldInfo[] localFields = tableFields; + int mask = tableMask; + int index = index(hash, mask); + while (true) { + if (localFields[index] == null) { + return -1; + } + if (localHashes[index] == hash) { + return localIndexes[index]; + } + index = (index + 1) & mask; + } + } + + private static int index(long hash, int mask) { + long spread = hash ^ (hash >>> 32); + return ((int) spread) & mask; + } + + private void put(JsonFieldInfo field, int fieldIndex) { + String name = field.name(); + long hash = field.nameHash(); + int index = index(hash, tableMask); + while (tableFields[index] != null) { + if (tableHashes[index] == hash) { + throw new ForyJsonException( + "JSON field hash collision between " + tableNames[index] + " and " + name); + } + index = (index + 1) & tableMask; + } + tableNames[index] = name; + tableHashes[index] = hash; + tableFields[index] = field; + tableIndexes[index] = fieldIndex; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java new file mode 100644 index 0000000000..1cb3552200 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java @@ -0,0 +1,648 @@ +/* + * 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.reader; + +import org.apache.fory.json.ForyJson; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.meta.JsonFieldTable; + +public abstract class JsonReader { + protected int position; + private int depth; + private int maxDepth = ForyJson.DEFAULT_MAX_DEPTH; + + protected abstract int length(); + + protected abstract char charAt(int index); + + public abstract String readString(); + + public final void resetDepth(int maxDepth) { + this.maxDepth = maxDepth; + depth = 0; + } + + public final void clearDepth() { + depth = 0; + } + + public final void enterDepth() { + int nextDepth = depth + 1; + if (nextDepth > maxDepth) { + throw error("JSON max depth " + maxDepth + " exceeded"); + } + depth = nextDepth; + } + + public final void exitDepth() { + depth--; + } + + public String readNullableString() { + return tryReadNull() ? null : readString(); + } + + public final void skipWhitespace() { + while (position < length()) { + char ch = charAt(position); + if (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t') { + position++; + } else { + return; + } + } + } + + public final boolean consume(char expected) { + skipWhitespace(); + if (position < length() && charAt(position) == expected) { + position++; + return true; + } + return false; + } + + public final void expect(char expected) { + if (!consume(expected)) { + throw error("Expected '" + expected + "'"); + } + } + + public final boolean consumeCommaOrEndObject() { + skipWhitespace(); + if (position < length()) { + char ch = charAt(position); + if (ch == ',') { + position++; + return true; + } + if (ch == '}') { + position++; + return false; + } + } + throw error("Expected ',' or '}'"); + } + + public final boolean consumeCommaOrEndArray() { + skipWhitespace(); + if (position < length()) { + char ch = charAt(position); + if (ch == ',') { + position++; + return true; + } + if (ch == ']') { + position++; + return false; + } + } + throw error("Expected ',' or ']'"); + } + + public final boolean peekNull() { + skipWhitespace(); + return startsWith("null"); + } + + public final char peekToken() { + skipWhitespace(); + if (position >= length()) { + throw error("Expected token"); + } + return charAt(position); + } + + public final void readNull() { + skipWhitespace(); + if (!startsWith("null")) { + throw error("Expected null"); + } + position += 4; + } + + public final boolean tryReadNull() { + skipWhitespace(); + if (startsWith("null")) { + position += 4; + return true; + } + return false; + } + + public final boolean readBoolean() { + skipWhitespace(); + if (startsWith("true")) { + position += 4; + return true; + } else if (startsWith("false")) { + position += 5; + return false; + } + throw error("Expected boolean"); + } + + public final String readNumber() { + skipWhitespace(); + int start = position; + if (position < length() && charAt(position) == '-') { + position++; + } + readIntegerDigits(); + if (position < length() && charAt(position) == '.') { + position++; + readDigits(); + } + if (position < length() && (charAt(position) == 'e' || charAt(position) == 'E')) { + position++; + if (position < length() && (charAt(position) == '+' || charAt(position) == '-')) { + position++; + } + readDigits(); + } + if (start == position) { + throw error("Expected number"); + } + return slice(start, position); + } + + public final int readInt() { + skipWhitespace(); + int start = position; + int result = 0; + int limit = -Integer.MAX_VALUE; + boolean negative = false; + if (position < length() && charAt(position) == '-') { + negative = true; + limit = Integer.MIN_VALUE; + position++; + } + if (position >= length()) { + throw error("Expected digit"); + } + char ch = charAt(position); + if (ch == '0') { + position++; + rejectLeadingDigit(); + rejectFractionOrExponent(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + int multmin = limit / 10; + while (position < length()) { + ch = charAt(position); + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result < multmin) { + throw error("Integer overflow"); + } + result *= 10; + if (result < limit + digit) { + throw error("Integer overflow"); + } + result -= digit; + position++; + } + if (start == position || (negative && start + 1 == position)) { + throw error("Expected digit"); + } + rejectFractionOrExponent(); + return negative ? result : -result; + } + + public final long readLong() { + skipWhitespace(); + int start = position; + long result = 0; + long limit = -Long.MAX_VALUE; + boolean negative = false; + if (position < length() && charAt(position) == '-') { + negative = true; + limit = Long.MIN_VALUE; + position++; + } + if (position >= length()) { + throw error("Expected digit"); + } + char ch = charAt(position); + if (ch == '0') { + position++; + rejectLeadingDigit(); + rejectFractionOrExponent(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + long multmin = limit / 10; + while (position < length()) { + ch = charAt(position); + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result < multmin) { + throw error("Long overflow"); + } + result *= 10; + if (result < limit + digit) { + throw error("Long overflow"); + } + result -= digit; + position++; + } + if (start == position || (negative && start + 1 == position)) { + throw error("Expected digit"); + } + rejectFractionOrExponent(); + return negative ? result : -result; + } + + public int readFieldNameInt() { + try { + return Integer.parseInt(readString()); + } catch (NumberFormatException e) { + throw new ForyJsonException("Invalid integer field name at JSON position " + position, e); + } + } + + public long readFieldNameLong() { + try { + return Long.parseLong(readString()); + } catch (NumberFormatException e) { + throw new ForyJsonException("Invalid long field name at JSON position " + position, e); + } + } + + public JsonFieldInfo readField(JsonFieldTable table) { + return table.get(readFieldNameHash()); + } + + public int readFieldIndex(JsonFieldTable table) { + return table.index(readFieldNameHash()); + } + + public int readFieldIndex(JsonFieldTable table, long expectedHash, int expectedIndex) { + long hash = readFieldNameHash(); + return hash == expectedHash ? expectedIndex : table.index(hash); + } + + public long readFieldNameHash() { + return readQuotedStringHash(); + } + + public long readStringHash() { + return readQuotedStringHash(); + } + + private long readQuotedStringHash() { + skipWhitespace(); + if (position >= length() || charAt(position++) != '"') { + throw error("Expected string"); + } + long hash = JsonFieldNameHash.MAGIC_HASH_CODE; + long value = 0; + int nameLength = 0; + boolean latin1 = true; + while (position < length()) { + char ch = charAt(position++); + if (ch == '"') { + return JsonFieldNameHash.finish(hash, value, nameLength, latin1); + } + if (ch == '\\') { + ch = readEscapedFieldNameChar(); + if (Character.isHighSurrogate(ch)) { + if (latin1) { + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, ch); + nameLength++; + if (position + 2 > length() || charAt(position) != '\\' || charAt(position + 1) != 'u') { + throw error("Unpaired high surrogate escape"); + } + position += 2; + char low = readUnicodeEscape(); + if (!Character.isLowSurrogate(low)) { + throw error("Unpaired high surrogate escape"); + } + hash = JsonFieldNameHash.update(hash, low); + nameLength++; + } else if (Character.isLowSurrogate(ch)) { + throw error("Unpaired low surrogate escape"); + } else { + if (latin1) { + if (ch <= 0xFF && ch != 0 && nameLength < Long.BYTES) { + value = JsonFieldNameHash.value(value, nameLength, ch); + nameLength++; + continue; + } + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, ch); + nameLength++; + } + continue; + } + if (ch < 0x20) { + throw error("Control character in string"); + } + if (Character.isHighSurrogate(ch)) { + if (position >= length() || !Character.isLowSurrogate(charAt(position))) { + throw error("Unpaired high surrogate in string"); + } + if (latin1) { + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, ch); + hash = JsonFieldNameHash.update(hash, charAt(position++)); + nameLength += 2; + continue; + } + if (Character.isLowSurrogate(ch)) { + throw error("Unpaired low surrogate in string"); + } + if (latin1) { + if (ch <= 0xFF && ch != 0 && nameLength < Long.BYTES) { + value = JsonFieldNameHash.value(value, nameLength, ch); + nameLength++; + continue; + } + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, ch); + nameLength++; + } + throw error("Unterminated string"); + } + + public final void skipValue() { + skipWhitespace(); + if (position >= length()) { + throw error("Expected value"); + } + char ch = charAt(position); + if (ch == '"') { + readString(); + } else if (ch == '{') { + skipObject(); + } else if (ch == '[') { + skipArray(); + } else if (startsWith("true")) { + position += 4; + } else if (startsWith("false")) { + position += 5; + } else if (startsWith("null")) { + position += 4; + } else { + readNumber(); + } + } + + public final void finish() { + skipWhitespace(); + if (position != length()) { + throw error("Trailing content"); + } + } + + protected final ForyJsonException error(String message) { + return new ForyJsonException(message + " at JSON position " + position); + } + + protected final void appendEscape(StringBuilder builder) { + if (position >= length()) { + throw error("Unterminated escape"); + } + char escaped = charAt(position++); + switch (escaped) { + case '"': + case '\\': + case '/': + builder.append(escaped); + return; + case 'b': + builder.append('\b'); + return; + case 'f': + builder.append('\f'); + return; + case 'n': + builder.append('\n'); + return; + case 'r': + builder.append('\r'); + return; + case 't': + builder.append('\t'); + return; + case 'u': + appendUnicodeEscape(builder); + return; + default: + throw error("Invalid escape"); + } + } + + private void skipObject() { + enterDepth(); + expect('{'); + if (consume('}')) { + exitDepth(); + return; + } + do { + skipWhitespace(); + readString(); + expect(':'); + skipValue(); + } while (consume(',')); + expect('}'); + exitDepth(); + } + + private void skipArray() { + enterDepth(); + expect('['); + if (consume(']')) { + exitDepth(); + return; + } + do { + skipValue(); + } while (consume(',')); + expect(']'); + exitDepth(); + } + + private boolean startsWith(String value) { + int end = position + value.length(); + if (end > length()) { + return false; + } + for (int i = 0; i < value.length(); i++) { + if (charAt(position + i) != value.charAt(i)) { + return false; + } + } + return true; + } + + private void readIntegerDigits() { + if (position >= length()) { + throw error("Expected digit"); + } + char ch = charAt(position); + if (ch == '0') { + position++; + if (position < length()) { + ch = charAt(position); + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + } + return; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + while (position < length()) { + ch = charAt(position); + if (ch >= '0' && ch <= '9') { + position++; + } else { + break; + } + } + } + + private void readDigits() { + int start = position; + while (position < length()) { + char ch = charAt(position); + if (ch >= '0' && ch <= '9') { + position++; + } else { + break; + } + } + if (start == position) { + throw error("Expected digit"); + } + } + + private void rejectLeadingDigit() { + if (position < length()) { + char ch = charAt(position); + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + } + } + + private void rejectFractionOrExponent() { + if (position < length()) { + char ch = charAt(position); + if (ch == '.' || ch == 'e' || ch == 'E') { + throw error("Expected integer"); + } + } + } + + private void appendUnicodeEscape(StringBuilder builder) { + char ch = readUnicodeEscape(); + if (Character.isHighSurrogate(ch)) { + if (position + 2 > length() || charAt(position) != '\\' || charAt(position + 1) != 'u') { + throw error("Unpaired high surrogate escape"); + } + position += 2; + char low = readUnicodeEscape(); + if (!Character.isLowSurrogate(low)) { + throw error("Unpaired high surrogate escape"); + } + builder.append(ch); + builder.append(low); + } else if (Character.isLowSurrogate(ch)) { + throw error("Unpaired low surrogate escape"); + } else { + builder.append(ch); + } + } + + protected final char readEscapedFieldNameChar() { + if (position >= length()) { + throw error("Unterminated escape"); + } + char escaped = charAt(position++); + switch (escaped) { + case '"': + case '\\': + case '/': + return escaped; + case 'b': + return '\b'; + case 'f': + return '\f'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case 'u': + return readUnicodeEscape(); + default: + throw error("Invalid escape"); + } + } + + protected final char readUnicodeEscape() { + if (position + 4 > length()) { + throw error("Short unicode escape"); + } + int value = 0; + for (int i = 0; i < 4; i++) { + value = (value << 4) | hexValue(charAt(position++)); + } + return (char) value; + } + + private int hexValue(char ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } else if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } else if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } + throw error("Invalid hex digit"); + } + + protected abstract String slice(int start, int end); +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java new file mode 100644 index 0000000000..8a9ae15696 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java @@ -0,0 +1,1152 @@ +/* + * 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.reader; + +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.meta.JsonFieldTable; +import org.apache.fory.memory.LittleEndian; +import org.apache.fory.serializer.StringSerializer; + +public final class Latin1JsonReader extends JsonReader { + private static final byte[] EMPTY_BYTES = new byte[0]; + private static final long BYTE_ONES = 0x0101010101010101L; + private static final int INT_BYTE_ONES = 0x01010101; + private static final long BYTE_HIGH_BITS = 0x8080808080808080L; + private static final int INT_BYTE_HIGH_BITS = 0x80808080; + private static final long BACKSLASH_BYTES = 0x5c5c5c5c5c5c5c5cL; + private static final int INT_BACKSLASH_BYTES = 0x5c5c5c5c; + private static final long CONTROL_LIMIT_BYTES = 0x2020202020202020L; + private static final int INT_CONTROL_LIMIT_BYTES = 0x20202020; + private static final long QUOTE_BYTES = 0x2222222222222222L; + private static final int INT_QUOTE_BYTES = 0x22222222; + + // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. + // Latin1 string content and field-name hashing must keep unsigned byte conversion. + private byte[] input; + + public Latin1JsonReader() { + input = EMPTY_BYTES; + } + + public Latin1JsonReader(String input) { + reset(input); + } + + public Latin1JsonReader reset(String input) { + if (!StringSerializer.isBytesBackedString()) { + throw new IllegalStateException("Latin1JsonReader requires byte-backed strings"); + } + byte coder = StringSerializer.getStringCoder(input); + if (!StringSerializer.isLatin1Coder(coder)) { + throw new IllegalArgumentException("Latin1JsonReader requires a Latin1 string"); + } + this.input = StringSerializer.getStringBytes(input); + position = 0; + return this; + } + + public void clear() { + input = EMPTY_BYTES; + position = 0; + } + + public boolean consumeToken(char expected) { + skipWhitespaceFast(); + if (position < input.length && input[position] == expected) { + position++; + return true; + } + return false; + } + + public boolean consumeNextToken(char expected) { + if (position < input.length && input[position] == expected) { + position++; + return true; + } + return consumeToken(expected); + } + + public void expectToken(char expected) { + if (!consumeToken(expected)) { + throw error("Expected '" + expected + "'"); + } + } + + public void expectNextToken(char expected) { + if (position < input.length && input[position] == expected) { + position++; + return; + } + expectNextTokenSlow(expected); + } + + private void expectNextTokenSlow(char expected) { + expectToken(expected); + } + + public boolean consumeNextCommaOrEndObject() { + if (position < input.length) { + int ch = input[position]; + if (ch == ',') { + position++; + return true; + } + if (ch == '}') { + position++; + return false; + } + if (!isWhitespace(ch)) { + return consumeNextCommaOrEndObjectSlow(); + } + } + return consumeNextCommaOrEndObjectSlow(); + } + + private boolean consumeNextCommaOrEndObjectSlow() { + skipWhitespaceFast(); + if (position < input.length) { + int ch = input[position]; + if (ch == ',') { + position++; + return true; + } + if (ch == '}') { + position++; + return false; + } + } + throw error("Expected ',' or '}'"); + } + + public boolean consumeNextCommaOrEndArray() { + if (position < input.length) { + int ch = input[position]; + if (ch == ',') { + position++; + return true; + } + if (ch == ']') { + position++; + return false; + } + if (!isWhitespace(ch)) { + return consumeNextCommaOrEndArraySlow(); + } + } + return consumeNextCommaOrEndArraySlow(); + } + + private boolean consumeNextCommaOrEndArraySlow() { + skipWhitespaceFast(); + if (position < input.length) { + int ch = input[position]; + if (ch == ',') { + position++; + return true; + } + if (ch == ']') { + position++; + return false; + } + } + throw error("Expected ',' or ']'"); + } + + public boolean tryReadNullToken() { + skipWhitespaceFast(); + return tryReadNullLiteral(); + } + + public boolean tryReadNextNullToken() { + if (position < input.length) { + int ch = input[position]; + if (ch == 'n') { + return tryReadNullLiteral(); + } + if (!isWhitespace(ch)) { + return false; + } + } + return tryReadNullToken(); + } + + private boolean tryReadNullLiteral() { + if (startsWithAscii("null")) { + position += 4; + return true; + } + return false; + } + + public boolean readBooleanValue() { + skipWhitespaceFast(); + return readBooleanToken(); + } + + public boolean readNextBooleanValue() { + if (position < input.length && !isWhitespace(input[position])) { + return readBooleanToken(); + } + return readBooleanValue(); + } + + public boolean readBooleanTokenValue() { + return readBooleanToken(); + } + + private boolean readBooleanToken() { + if (startsWithAscii("true")) { + position += 4; + return true; + } else if (startsWithAscii("false")) { + position += 5; + return false; + } + throw error("Expected boolean"); + } + + public int readIntValue() { + skipWhitespaceFast(); + return readIntToken(); + } + + public int readNextIntValue() { + if (position < input.length && !isWhitespace(input[position])) { + return readIntToken(); + } + return readIntValue(); + } + + public int readIntTokenValue() { + return readIntToken(); + } + + private int readIntToken() { + byte[] bytes = input; + int offset = position; + int inputLength = bytes.length; + if (offset >= inputLength) { + throw error("Expected digit"); + } + int ch = bytes[offset]; + if (ch == '-') { + return readNegativeIntToken(offset); + } + if (ch == '0') { + position = offset + 1; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + int result = ch - '0'; + offset++; + int safeEnd = offset + 8; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + while (offset < safeEnd) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + result = result * 10 + (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readPositiveIntTail(bytes, offset, inputLength, result); + } + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private int readPositiveIntTail(byte[] bytes, int offset, int inputLength, int result) { + while (offset < inputLength) { + int ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + long value = (long) result * 10 + (ch - '0'); + if (value > Integer.MAX_VALUE) { + position = offset; + throw error("Integer overflow"); + } + result = (int) value; + offset++; + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private int readNegativeIntToken(int start) { + position = start + 1; + int result = 0; + int limit = Integer.MIN_VALUE; + if (position >= input.length) { + throw error("Expected digit"); + } + int ch = input[position]; + if (ch == '0') { + position++; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + int multmin = limit / 10; + while (position < input.length) { + ch = input[position]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result < multmin) { + throw error("Integer overflow"); + } + result *= 10; + if (result < Integer.MIN_VALUE + digit) { + throw error("Integer overflow"); + } + result -= digit; + position++; + } + rejectFractionOrExponentFast(); + return result; + } + + public long readLongValue() { + skipWhitespaceFast(); + return readLongToken(); + } + + public long readNextLongValue() { + if (position < input.length && !isWhitespace(input[position])) { + return readLongToken(); + } + return readLongValue(); + } + + public long readLongTokenValue() { + return readLongToken(); + } + + private long readLongToken() { + byte[] bytes = input; + int offset = position; + int inputLength = bytes.length; + if (offset >= inputLength) { + throw error("Expected digit"); + } + int ch = bytes[offset]; + if (ch == '-') { + return readNegativeLongToken(offset); + } + if (ch == '0') { + position = offset + 1; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + long result = ch - '0'; + offset++; + int safeEnd = offset + 17; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + while (offset < safeEnd) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + result = result * 10 + (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readPositiveLongTail(bytes, offset, inputLength, result); + } + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, long result) { + while (offset < inputLength) { + int ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result > (Long.MAX_VALUE - digit) / 10) { + position = offset; + throw error("Long overflow"); + } + result = result * 10 + digit; + offset++; + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readNegativeLongToken(int start) { + position = start + 1; + long result = 0; + long limit = Long.MIN_VALUE; + if (position >= input.length) { + throw error("Expected digit"); + } + int ch = input[position]; + if (ch == '0') { + position++; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + long multmin = limit / 10; + while (position < input.length) { + ch = input[position]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result < multmin) { + throw error("Long overflow"); + } + result *= 10; + if (result < Long.MIN_VALUE + digit) { + throw error("Long overflow"); + } + result -= digit; + position++; + } + rejectFractionOrExponentFast(); + return result; + } + + @Override + public int readFieldNameInt() { + skipWhitespaceFast(); + int nameStart = position; + if (position >= input.length || input[position++] != '"') { + throw error("Expected string"); + } + int result = 0; + int limit = -Integer.MAX_VALUE; + boolean negative = false; + if (position < input.length && input[position] == '-') { + negative = true; + limit = Integer.MIN_VALUE; + position++; + } + if (position >= input.length) { + throw error("Unterminated string"); + } + int ch = input[position]; + if (ch == '\\') { + position = nameStart; + return super.readFieldNameInt(); + } + if (ch == '0') { + position++; + return readZeroIntName(nameStart); + } + if (ch < '1' || ch > '9') { + throw error("Expected integer field name"); + } + int multmin = limit / 10; + do { + int digit = ch - '0'; + if (result < multmin) { + throw error("Integer overflow"); + } + result *= 10; + if (result < limit + digit) { + throw error("Integer overflow"); + } + result -= digit; + position++; + if (position >= input.length) { + throw error("Unterminated string"); + } + ch = input[position]; + } while (ch >= '0' && ch <= '9'); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameInt(); + } + if (ch != '"') { + throw error("Expected integer field name"); + } + position++; + return negative ? result : -result; + } + + @Override + public long readFieldNameLong() { + skipWhitespaceFast(); + int nameStart = position; + if (position >= input.length || input[position++] != '"') { + throw error("Expected string"); + } + long result = 0; + long limit = -Long.MAX_VALUE; + boolean negative = false; + if (position < input.length && input[position] == '-') { + negative = true; + limit = Long.MIN_VALUE; + position++; + } + if (position >= input.length) { + throw error("Unterminated string"); + } + int ch = input[position]; + if (ch == '\\') { + position = nameStart; + return super.readFieldNameLong(); + } + if (ch == '0') { + position++; + return readZeroLongName(nameStart); + } + if (ch < '1' || ch > '9') { + throw error("Expected long field name"); + } + long multmin = limit / 10; + do { + int digit = ch - '0'; + if (result < multmin) { + throw error("Long overflow"); + } + result *= 10; + if (result < limit + digit) { + throw error("Long overflow"); + } + result -= digit; + position++; + if (position >= input.length) { + throw error("Unterminated string"); + } + ch = input[position]; + } while (ch >= '0' && ch <= '9'); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameLong(); + } + if (ch != '"') { + throw error("Expected long field name"); + } + position++; + return negative ? result : -result; + } + + @Override + protected int length() { + return input.length; + } + + @Override + protected char charAt(int index) { + return (char) (input[index] & 0xFF); + } + + @Override + public String readString() { + skipWhitespaceFast(); + return readStringToken(); + } + + @Override + public String readNullableString() { + skipWhitespaceFast(); + if (tryReadNullLiteral()) { + return null; + } + return readStringToken(); + } + + public String readNextNullableString() { + if (position < input.length) { + int ch = input[position]; + if (ch == '"') { + return readStringToken(); + } + if (ch == 'n' && tryReadNullLiteral()) { + return null; + } + if (!isWhitespace(ch)) { + return readStringToken(); + } + } + return readNullableString(); + } + + public String readNullableStringToken() { + if (position < input.length) { + int ch = input[position]; + if (ch == '"') { + return readStringToken(); + } + if (ch == 'n' && tryReadNullLiteral()) { + return null; + } + } + return readStringToken(); + } + + private String readStringToken() { + byte[] bytes = input; + int inputLength = bytes.length; + if (position >= inputLength || bytes[position++] != '"') { + throw error("Expected string"); + } + int start = position; + int offset = start; + int wordEnd = inputLength - Long.BYTES; + while (offset <= wordEnd) { + long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask == 0) { + offset += Long.BYTES; + continue; + } + int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); + int ch = bytes[stop] & 0xFF; + if (ch == '"') { + position = stop + 1; + return newLatin1String(start, stop); + } + return readStringStop(start, stop, ch); + } + if (offset + Integer.BYTES <= inputLength) { + int stopMask = stringStopMask(LittleEndian.getInt32(bytes, offset)); + if (stopMask == 0) { + offset += Integer.BYTES; + } else { + int stop = offset + (Integer.numberOfTrailingZeros(stopMask) >>> 3); + int ch = bytes[stop] & 0xFF; + if (ch == '"') { + position = stop + 1; + return newLatin1String(start, stop); + } + return readStringStop(start, stop, ch); + } + } + while (offset < inputLength) { + int ch = bytes[offset++] & 0xFF; + if (ch == '"') { + position = offset; + return newLatin1String(start, offset - 1); + } + if (ch == '\\') { + return readStringStop(start, offset - 1, ch); + } + if (ch < 0x20) { + position = offset; + throw error("Control character in string"); + } + } + throw error("Unterminated string"); + } + + private String readStringStop(int start, int stop, int ch) { + position = stop + 1; + if (ch == '\\') { + StringBuilder builder = newStringBuilder(start, stop); + appendLatin1(builder, start, stop); + appendEscape(builder); + return readStringTail(builder); + } + throw error("Control character in string"); + } + + private StringBuilder newStringBuilder(int start, int stop) { + return new StringBuilder(Math.max(16, stop - start + 16)); + } + + private static long stringStopMask(long word) { + return byteMatchMask(word, QUOTE_BYTES) + | byteMatchMask(word, BACKSLASH_BYTES) + | ((word - CONTROL_LIMIT_BYTES) & ~word & BYTE_HIGH_BITS); + } + + private static int stringStopMask(int word) { + return byteMatchMask(word, INT_QUOTE_BYTES) + | byteMatchMask(word, INT_BACKSLASH_BYTES) + | ((word - INT_CONTROL_LIMIT_BYTES) & ~word & INT_BYTE_HIGH_BITS); + } + + private static long byteMatchMask(long word, long repeatedByte) { + long match = word ^ repeatedByte; + return (match - BYTE_ONES) & ~match & BYTE_HIGH_BITS; + } + + private static int byteMatchMask(int word, int repeatedByte) { + int match = word ^ repeatedByte; + return (match - INT_BYTE_ONES) & ~match & INT_BYTE_HIGH_BITS; + } + + @Override + public JsonFieldInfo readField(JsonFieldTable table) { + return table.get(readFieldNameHash()); + } + + @Override + public int readFieldIndex(JsonFieldTable table) { + return table.index(readFieldNameHash()); + } + + @Override + public int readFieldIndex(JsonFieldTable table, long expectedHash, int expectedIndex) { + long hash = readFieldNameHash(); + return hash == expectedHash ? expectedIndex : table.index(hash); + } + + @Override + public long readFieldNameHash() { + return readQuotedStringHash(); + } + + public boolean tryReadFieldNameColon(long expectedHash, long expectedMask, int expectedLength) { + int mark = position; + skipWhitespaceFast(); + return tryReadFieldNameColonAt(mark, expectedHash, expectedMask, expectedLength); + } + + public boolean tryReadNextFieldNameColon( + long expectedHash, long expectedMask, int expectedLength) { + int mark = position; + if (mark < input.length) { + int ch = input[mark]; + if (ch == '"') { + return tryReadFieldNameColonAt(mark, expectedHash, expectedMask, expectedLength); + } + if (!isWhitespace(ch)) { + return false; + } + } + return tryReadFieldNameColon(expectedHash, expectedMask, expectedLength); + } + + public boolean tryReadNextFieldNameToken0(long prefix, long prefixMask, int tokenLength) { + return tryReadNextRawToken0(prefix, prefixMask, tokenLength); + } + + public boolean tryReadNextStringToken0(long prefix, long prefixMask, int tokenLength) { + return tryReadNextRawToken0(prefix, prefixMask, tokenLength); + } + + private boolean tryReadNextRawToken0(long prefix, long prefixMask, int tokenLength) { + byte[] bytes = input; + int mark = position; + if (mark + Long.BYTES <= bytes.length + && (LittleEndian.getInt64(bytes, mark) & prefixMask) == prefix) { + position = mark + tokenLength; + return true; + } + return false; + } + + public boolean tryReadNextFieldNameToken1( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken1(prefix, prefixMask, suffix, tokenLength); + } + + public boolean tryReadNextStringToken1( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken1(prefix, prefixMask, suffix, tokenLength); + } + + private boolean tryReadNextRawToken1(long prefix, long prefixMask, int suffix, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && (LittleEndian.getInt64(bytes, mark) & prefixMask) == prefix + && (bytes[suffixOffset] & 0xFF) == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + public boolean tryReadNextFieldNameToken2( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken2(prefix, prefixMask, suffix, tokenLength); + } + + public boolean tryReadNextStringToken2( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken2(prefix, prefixMask, suffix, tokenLength); + } + + private boolean tryReadNextRawToken2(long prefix, long prefixMask, int suffix, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && (LittleEndian.getInt64(bytes, mark) & prefixMask) == prefix + && ((bytes[suffixOffset] & 0xFF) | ((bytes[suffixOffset + 1] & 0xFF) << 8)) == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + public boolean tryReadNextFieldNameToken3( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken3(prefix, prefixMask, suffix, tokenLength); + } + + public boolean tryReadNextStringToken3( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken3(prefix, prefixMask, suffix, tokenLength); + } + + private boolean tryReadNextRawToken3(long prefix, long prefixMask, int suffix, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && (LittleEndian.getInt64(bytes, mark) & prefixMask) == prefix + && ((bytes[suffixOffset] & 0xFF) + | ((bytes[suffixOffset + 1] & 0xFF) << 8) + | ((bytes[suffixOffset + 2] & 0xFF) << 16)) + == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + private boolean tryReadFieldNameColonAt( + int mark, long expectedHash, long expectedMask, int expectedLength) { + byte[] bytes = input; + int offset = position; + int nameOffset = offset + 1; + int quoteOffset = nameOffset + expectedLength; + if (quoteOffset < bytes.length && bytes[offset] == '"') { + if (nameOffset + Long.BYTES <= bytes.length) { + if ((LittleEndian.getInt64(bytes, nameOffset) & expectedMask) == expectedHash + && bytes[quoteOffset] == '"') { + int colonOffset = quoteOffset + 1; + if (colonOffset < bytes.length && bytes[colonOffset] == ':') { + position = colonOffset + 1; + } else { + readFieldNameColon(colonOffset); + } + return true; + } + // Full raw-word misses cannot match this generated packed-name probe. Escaped field names + // are handled by the hash fallback after this direct probe fails. + position = mark; + return false; + } + offset = nameOffset; + long value = 0; + for (int i = 0; i < expectedLength; i++) { + int ch = bytes[offset++] & 0xFF; + if (ch == 0 || ch == '"' || ch == '\\' || ch < 0x20) { + position = mark; + return false; + } + value = JsonFieldNameHash.value(value, i, (char) ch); + } + if (value == expectedHash && bytes[offset] == '"') { + int colonOffset = offset + 1; + if (colonOffset < bytes.length && bytes[colonOffset] == ':') { + position = colonOffset + 1; + } else { + readFieldNameColon(colonOffset); + } + return true; + } + } + position = mark; + return false; + } + + private void readFieldNameColon(int colonOffset) { + position = colonOffset; + expectNextToken(':'); + } + + @Override + public long readStringHash() { + return readQuotedStringHash(); + } + + public long readPackedStringHash() { + skipWhitespaceFast(); + return readPackedStringHashToken(); + } + + public long readNextPackedStringHash() { + if (position < input.length && !isWhitespace(input[position])) { + return readPackedStringHashToken(); + } + return readPackedStringHash(); + } + + public long readPackedStringHashTokenValue() { + return readPackedStringHashToken(); + } + + private long readPackedStringHashToken() { + int mark = position; + byte[] bytes = input; + int length = bytes.length; + int offset = position; + if (offset < length && bytes[offset++] == '"') { + long value = 0; + int nameLength = 0; + while (offset < length) { + int ch = bytes[offset++] & 0xFF; + if (ch == '"') { + if (nameLength > 0) { + position = offset; + return value; + } + break; + } + if (ch == 0 || ch == '\\' || ch < 0x20 || nameLength >= Long.BYTES) { + break; + } + value = JsonFieldNameHash.value(value, nameLength++, (char) ch); + } + } + return readQuotedStringHashFromMark(mark); + } + + private long readQuotedStringHashFromMark(int mark) { + position = mark; + return readQuotedStringHashToken(); + } + + private long readQuotedStringHash() { + skipWhitespaceFast(); + return readQuotedStringHashToken(); + } + + private long readQuotedStringHashToken() { + byte[] bytes = input; + int length = bytes.length; + if (position >= length || bytes[position++] != '"') { + throw error("Expected string"); + } + long hash = JsonFieldNameHash.MAGIC_HASH_CODE; + long value = 0; + int nameLength = 0; + boolean latin1 = true; + while (position < length) { + int ch = bytes[position++] & 0xFF; + if (ch == '"') { + return JsonFieldNameHash.finish(hash, value, nameLength, latin1); + } + if (ch == '\\') { + char escaped = readEscapedFieldNameChar(); + if (Character.isHighSurrogate(escaped)) { + if (latin1) { + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, escaped); + nameLength++; + if (position + 2 > length() || charAt(position) != '\\' || charAt(position + 1) != 'u') { + throw error("Unpaired high surrogate escape"); + } + position += 2; + char low = readUnicodeEscape(); + if (!Character.isLowSurrogate(low)) { + throw error("Unpaired high surrogate escape"); + } + hash = JsonFieldNameHash.update(hash, low); + nameLength++; + } else if (Character.isLowSurrogate(escaped)) { + throw error("Unpaired low surrogate escape"); + } else { + if (latin1) { + if (escaped <= 0xFF && escaped != 0 && nameLength < Long.BYTES) { + value = JsonFieldNameHash.value(value, nameLength, escaped); + nameLength++; + continue; + } + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, escaped); + nameLength++; + } + continue; + } + if (ch < 0x20) { + throw error("Control character in string"); + } + if (latin1) { + if (ch != 0 && nameLength < Long.BYTES) { + value = JsonFieldNameHash.value(value, nameLength, (char) ch); + nameLength++; + continue; + } + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, (char) ch); + nameLength++; + } + throw error("Unterminated string"); + } + + @Override + protected String slice(int start, int end) { + return newLatin1String(start, end); + } + + private String newLatin1String(int start, int end) { + int length = end - start; + byte[] bytes = new byte[length]; + System.arraycopy(input, start, bytes, 0, length); + return StringSerializer.newLatin1StringZeroCopy(bytes); + } + + private String readStringTail(StringBuilder builder) { + while (position < input.length) { + int ch = input[position++] & 0xFF; + if (ch == '"') { + return builder.toString(); + } else if (ch == '\\') { + appendEscape(builder); + } else if (ch < 0x20) { + throw error("Control character in string"); + } else { + builder.append((char) ch); + } + } + throw error("Unterminated string"); + } + + private void appendLatin1(StringBuilder builder, int start, int end) { + for (int i = start; i < end; i++) { + builder.append((char) (input[i] & 0xFF)); + } + } + + private void skipWhitespaceFast() { + while (position < input.length) { + int ch = input[position]; + if (isWhitespace(ch)) { + position++; + } else { + return; + } + } + } + + private static boolean isWhitespace(int ch) { + return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; + } + + private boolean startsWithAscii(String value) { + int end = position + value.length(); + if (end > input.length) { + return false; + } + for (int i = 0; i < value.length(); i++) { + if (input[position + i] != value.charAt(i)) { + return false; + } + } + return true; + } + + private void rejectLeadingDigitFast() { + if (position < input.length) { + int ch = input[position]; + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + } + } + + private void rejectFractionOrExponentFast() { + if (position < input.length) { + int ch = input[position]; + if (ch == '.' || ch == 'e' || ch == 'E') { + throw error("Expected integer"); + } + } + } + + private int readZeroIntName(int nameStart) { + if (position >= input.length) { + throw error("Unterminated string"); + } + int ch = input[position]; + if (ch == '\\') { + position = nameStart; + return super.readFieldNameInt(); + } + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + if (ch != '"') { + throw error("Expected integer field name"); + } + position++; + return 0; + } + + private long readZeroLongName(int nameStart) { + if (position >= input.length) { + throw error("Unterminated string"); + } + int ch = input[position]; + if (ch == '\\') { + position = nameStart; + return super.readFieldNameLong(); + } + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + if (ch != '"') { + throw error("Expected long field name"); + } + position++; + return 0L; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1ObjectReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1ObjectReader.java new file mode 100644 index 0000000000..8d50d2d185 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1ObjectReader.java @@ -0,0 +1,27 @@ +/* + * 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.reader; + +import org.apache.fory.json.codec.BaseObjectCodec; +import org.apache.fory.json.resolver.JsonTypeResolver; + +public interface Latin1ObjectReader { + Object readLatin1(Latin1JsonReader reader, BaseObjectCodec owner, JsonTypeResolver typeResolver); +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/ObjectReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/ObjectReader.java new file mode 100644 index 0000000000..4c3675726d --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/ObjectReader.java @@ -0,0 +1,27 @@ +/* + * 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.reader; + +import org.apache.fory.json.codec.BaseObjectCodec; +import org.apache.fory.json.resolver.JsonTypeResolver; + +public interface ObjectReader { + Object read(JsonReader reader, BaseObjectCodec owner, JsonTypeResolver typeResolver); +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java new file mode 100644 index 0000000000..b2f0f6a058 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java @@ -0,0 +1,947 @@ +/* + * 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.reader; + +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.meta.JsonFieldTable; +import org.apache.fory.serializer.StringSerializer; + +public final class Utf16JsonReader extends JsonReader { + private String input; + private byte[] bytes; + private int length; + + public Utf16JsonReader() { + input = ""; + bytes = null; + length = 0; + } + + public Utf16JsonReader(String input) { + reset(input); + } + + public Utf16JsonReader reset(String input) { + this.input = input; + if (StringSerializer.isBytesBackedString()) { + byte coder = StringSerializer.getStringCoder(input); + if (StringSerializer.isUtf16Coder(coder)) { + bytes = StringSerializer.getStringBytes(input); + length = bytes.length >>> 1; + position = 0; + return this; + } + } + bytes = null; + length = input.length(); + position = 0; + return this; + } + + public void clear() { + input = ""; + bytes = null; + length = 0; + position = 0; + } + + public boolean consumeToken(char expected) { + skipWhitespaceFast(); + if (position < length && charAtFast(position) == expected) { + position++; + return true; + } + return false; + } + + public boolean consumeNextToken(char expected) { + if (position < length && charAtFast(position) == expected) { + position++; + return true; + } + return consumeToken(expected); + } + + public void expectToken(char expected) { + if (!consumeToken(expected)) { + throw error("Expected '" + expected + "'"); + } + } + + public void expectNextToken(char expected) { + if (position < length && charAtFast(position) == expected) { + position++; + return; + } + expectNextTokenSlow(expected); + } + + private void expectNextTokenSlow(char expected) { + expectToken(expected); + } + + public boolean consumeNextCommaOrEndObject() { + int inputLength = length; + if (position < inputLength) { + char ch = charAtFast(position); + if (ch == ',') { + position++; + return true; + } + if (ch == '}') { + position++; + return false; + } + if (!isWhitespace(ch)) { + return consumeNextCommaOrEndObjectSlow(inputLength); + } + } + return consumeNextCommaOrEndObjectSlow(inputLength); + } + + private boolean consumeNextCommaOrEndObjectSlow(int inputLength) { + skipWhitespaceFast(); + if (position < inputLength) { + char ch = charAtFast(position); + if (ch == ',') { + position++; + return true; + } + if (ch == '}') { + position++; + return false; + } + } + throw error("Expected ',' or '}'"); + } + + public boolean consumeNextCommaOrEndArray() { + int inputLength = length; + if (position < inputLength) { + char ch = charAtFast(position); + if (ch == ',') { + position++; + return true; + } + if (ch == ']') { + position++; + return false; + } + if (!isWhitespace(ch)) { + return consumeNextCommaOrEndArraySlow(inputLength); + } + } + return consumeNextCommaOrEndArraySlow(inputLength); + } + + private boolean consumeNextCommaOrEndArraySlow(int inputLength) { + skipWhitespaceFast(); + if (position < inputLength) { + char ch = charAtFast(position); + if (ch == ',') { + position++; + return true; + } + if (ch == ']') { + position++; + return false; + } + } + throw error("Expected ',' or ']'"); + } + + public boolean tryReadNullToken() { + skipWhitespaceFast(); + return tryReadNullLiteral(); + } + + public boolean tryReadNextNullToken() { + if (position < length) { + char ch = charAtFast(position); + if (ch == 'n') { + return tryReadNullLiteral(); + } + if (!isWhitespace(ch)) { + return false; + } + } + return tryReadNullToken(); + } + + private boolean tryReadNullLiteral() { + if (startsWithAscii("null")) { + position += 4; + return true; + } + return false; + } + + public boolean readBooleanValue() { + skipWhitespaceFast(); + return readBooleanToken(); + } + + public boolean readNextBooleanValue() { + if (position < length && !isWhitespace(charAtFast(position))) { + return readBooleanToken(); + } + return readBooleanValue(); + } + + private boolean readBooleanToken() { + if (startsWithAscii("true")) { + position += 4; + return true; + } else if (startsWithAscii("false")) { + position += 5; + return false; + } + throw error("Expected boolean"); + } + + public int readIntValue() { + skipWhitespaceFast(); + return readIntToken(); + } + + public int readNextIntValue() { + if (position < length && !isWhitespace(charAtFast(position))) { + return readIntToken(); + } + return readIntValue(); + } + + private int readIntToken() { + int offset = position; + int inputLength = length; + if (offset >= inputLength) { + throw error("Expected digit"); + } + char ch = charAtFast(offset); + if (ch == '-') { + return readNegativeIntToken(offset); + } + if (ch == '0') { + position = offset + 1; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + int result = ch - '0'; + offset++; + int safeEnd = offset + 8; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + while (offset < safeEnd) { + ch = charAtFast(offset); + if (ch < '0' || ch > '9') { + break; + } + result = result * 10 + (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = charAtFast(offset); + if (ch >= '0' && ch <= '9') { + return readPositiveIntTail(offset, inputLength, result); + } + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private int readPositiveIntTail(int offset, int inputLength, int result) { + while (offset < inputLength) { + char ch = charAtFast(offset); + if (ch < '0' || ch > '9') { + break; + } + long value = (long) result * 10 + (ch - '0'); + if (value > Integer.MAX_VALUE) { + position = offset; + throw error("Integer overflow"); + } + result = (int) value; + offset++; + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private int readNegativeIntToken(int start) { + position = start + 1; + int result = 0; + int limit = Integer.MIN_VALUE; + if (position >= length) { + throw error("Expected digit"); + } + char ch = charAtFast(position); + if (ch == '0') { + position++; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + int multmin = limit / 10; + while (position < length) { + ch = charAtFast(position); + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result < multmin) { + throw error("Integer overflow"); + } + result *= 10; + if (result < Integer.MIN_VALUE + digit) { + throw error("Integer overflow"); + } + result -= digit; + position++; + } + rejectFractionOrExponentFast(); + return result; + } + + public long readLongValue() { + skipWhitespaceFast(); + return readLongToken(); + } + + public long readNextLongValue() { + if (position < length && !isWhitespace(charAtFast(position))) { + return readLongToken(); + } + return readLongValue(); + } + + private long readLongToken() { + int offset = position; + int inputLength = length; + if (offset >= inputLength) { + throw error("Expected digit"); + } + char ch = charAtFast(offset); + if (ch == '-') { + return readNegativeLongToken(offset); + } + if (ch == '0') { + position = offset + 1; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + long result = ch - '0'; + offset++; + int safeEnd = offset + 17; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + while (offset < safeEnd) { + ch = charAtFast(offset); + if (ch < '0' || ch > '9') { + break; + } + result = result * 10 + (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = charAtFast(offset); + if (ch >= '0' && ch <= '9') { + return readPositiveLongTail(offset, inputLength, result); + } + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readPositiveLongTail(int offset, int inputLength, long result) { + while (offset < inputLength) { + char ch = charAtFast(offset); + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result > (Long.MAX_VALUE - digit) / 10) { + position = offset; + throw error("Long overflow"); + } + result = result * 10 + digit; + offset++; + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readNegativeLongToken(int start) { + position = start + 1; + long result = 0; + long limit = Long.MIN_VALUE; + if (position >= length) { + throw error("Expected digit"); + } + char ch = charAtFast(position); + if (ch == '0') { + position++; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + long multmin = limit / 10; + while (position < length) { + ch = charAtFast(position); + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result < multmin) { + throw error("Long overflow"); + } + result *= 10; + if (result < Long.MIN_VALUE + digit) { + throw error("Long overflow"); + } + result -= digit; + position++; + } + rejectFractionOrExponentFast(); + return result; + } + + @Override + public int readFieldNameInt() { + skipWhitespaceFast(); + int nameStart = position; + if (position >= length || charAtFast(position++) != '"') { + throw error("Expected string"); + } + int result = 0; + int limit = -Integer.MAX_VALUE; + boolean negative = false; + if (position < length && charAtFast(position) == '-') { + negative = true; + limit = Integer.MIN_VALUE; + position++; + } + if (position >= length) { + throw error("Unterminated string"); + } + char ch = charAtFast(position); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameInt(); + } + if (ch == '0') { + position++; + return readZeroIntName(nameStart); + } + if (ch < '1' || ch > '9') { + throw error("Expected integer field name"); + } + int multmin = limit / 10; + do { + int digit = ch - '0'; + if (result < multmin) { + throw error("Integer overflow"); + } + result *= 10; + if (result < limit + digit) { + throw error("Integer overflow"); + } + result -= digit; + position++; + if (position >= length) { + throw error("Unterminated string"); + } + ch = charAtFast(position); + } while (ch >= '0' && ch <= '9'); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameInt(); + } + if (ch != '"') { + throw error("Expected integer field name"); + } + position++; + return negative ? result : -result; + } + + @Override + public long readFieldNameLong() { + skipWhitespaceFast(); + int nameStart = position; + if (position >= length || charAtFast(position++) != '"') { + throw error("Expected string"); + } + long result = 0; + long limit = -Long.MAX_VALUE; + boolean negative = false; + if (position < length && charAtFast(position) == '-') { + negative = true; + limit = Long.MIN_VALUE; + position++; + } + if (position >= length) { + throw error("Unterminated string"); + } + char ch = charAtFast(position); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameLong(); + } + if (ch == '0') { + position++; + return readZeroLongName(nameStart); + } + if (ch < '1' || ch > '9') { + throw error("Expected long field name"); + } + long multmin = limit / 10; + do { + int digit = ch - '0'; + if (result < multmin) { + throw error("Long overflow"); + } + result *= 10; + if (result < limit + digit) { + throw error("Long overflow"); + } + result -= digit; + position++; + if (position >= length) { + throw error("Unterminated string"); + } + ch = charAtFast(position); + } while (ch >= '0' && ch <= '9'); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameLong(); + } + if (ch != '"') { + throw error("Expected long field name"); + } + position++; + return negative ? result : -result; + } + + @Override + protected int length() { + return length; + } + + @Override + protected char charAt(int index) { + return charAtFast(index); + } + + @Override + public String readString() { + skipWhitespaceFast(); + return readStringToken(); + } + + @Override + public String readNullableString() { + skipWhitespaceFast(); + if (tryReadNullLiteral()) { + return null; + } + return readStringToken(); + } + + public String readNextNullableString() { + if (position < length) { + char ch = charAtFast(position); + if (ch == '"') { + return readStringToken(); + } + if (ch == 'n' && tryReadNullLiteral()) { + return null; + } + if (!isWhitespace(ch)) { + return readStringToken(); + } + } + return readNullableString(); + } + + private String readStringToken() { + if (position >= length || charAtFast(position++) != '"') { + throw error("Expected string"); + } + int start = position; + StringBuilder builder = null; + while (position < length) { + char ch = charAtFast(position++); + if (ch == '"') { + if (builder == null) { + return input.substring(start, position - 1); + } + builder.append(input, start, position - 1); + return builder.toString(); + } else if (ch == '\\') { + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(input, start, position - 1); + appendEscape(builder); + start = position; + } else if (ch < 0x20) { + throw error("Control character in string"); + } else if (Character.isHighSurrogate(ch)) { + if (position >= length || !Character.isLowSurrogate(charAtFast(position))) { + throw error("Unpaired high surrogate in string"); + } + position++; + } else if (Character.isLowSurrogate(ch)) { + throw error("Unpaired low surrogate in string"); + } + } + throw error("Unterminated string"); + } + + @Override + public JsonFieldInfo readField(JsonFieldTable table) { + return table.get(readFieldNameHash()); + } + + @Override + public int readFieldIndex(JsonFieldTable table) { + return table.index(readFieldNameHash()); + } + + @Override + public int readFieldIndex(JsonFieldTable table, long expectedHash, int expectedIndex) { + long hash = readFieldNameHash(); + return hash == expectedHash ? expectedIndex : table.index(hash); + } + + @Override + public long readFieldNameHash() { + return readQuotedStringHash(); + } + + public boolean tryReadFieldNameColon(long expectedHash, long expectedMask, int expectedLength) { + int mark = position; + skipWhitespaceFast(); + return tryReadFieldNameColonAt(mark, expectedHash, expectedMask, expectedLength); + } + + public boolean tryReadNextFieldNameColon( + long expectedHash, long expectedMask, int expectedLength) { + int mark = position; + if (mark < length) { + char ch = charAtFast(mark); + if (ch == '"') { + return tryReadFieldNameColonAt(mark, expectedHash, expectedMask, expectedLength); + } + if (!isWhitespace(ch)) { + return false; + } + } + return tryReadFieldNameColon(expectedHash, expectedMask, expectedLength); + } + + private boolean tryReadFieldNameColonAt( + int mark, long expectedHash, long expectedMask, int expectedLength) { + int offset = position; + int end = offset + expectedLength + 1; + if (end < length && charAtFast(offset++) == '"') { + long value = 0; + for (int i = 0; i < expectedLength; i++) { + char ch = charAtFast(offset++); + if (ch == 0 || ch == '"' || ch == '\\' || ch < 0x20 || ch > 0xFF) { + position = mark; + return false; + } + value = JsonFieldNameHash.value(value, i, ch); + } + if (value == expectedHash && charAtFast(offset) == '"') { + int colonOffset = offset + 1; + if (colonOffset < length && charAtFast(colonOffset) == ':') { + position = colonOffset + 1; + } else { + readFieldNameColon(colonOffset); + } + return true; + } + } + position = mark; + return false; + } + + private void readFieldNameColon(int colonOffset) { + position = colonOffset; + expectNextToken(':'); + } + + @Override + public long readStringHash() { + return readQuotedStringHash(); + } + + public long readPackedStringHash() { + skipWhitespaceFast(); + return readPackedStringHashToken(); + } + + public long readNextPackedStringHash() { + if (position < length && !isWhitespace(charAtFast(position))) { + return readPackedStringHashToken(); + } + return readPackedStringHash(); + } + + private long readPackedStringHashToken() { + int mark = position; + int inputLength = length; + int offset = position; + if (offset < inputLength && charAtFast(offset++) == '"') { + long value = 0; + int nameLength = 0; + while (offset < inputLength) { + char ch = charAtFast(offset++); + if (ch == '"') { + if (nameLength > 0) { + position = offset; + return value; + } + break; + } + if (ch == 0 || ch == '\\' || ch < 0x20 || ch > 0xFF || nameLength >= Long.BYTES) { + break; + } + value = JsonFieldNameHash.value(value, nameLength++, ch); + } + } + return readQuotedStringHashFromMark(mark); + } + + private long readQuotedStringHashFromMark(int mark) { + position = mark; + return readQuotedStringHashToken(); + } + + private long readQuotedStringHash() { + skipWhitespaceFast(); + return readQuotedStringHashToken(); + } + + private long readQuotedStringHashToken() { + int inputLength = length; + if (position >= inputLength || charAtFast(position++) != '"') { + throw error("Expected string"); + } + long hash = JsonFieldNameHash.MAGIC_HASH_CODE; + long value = 0; + int nameLength = 0; + boolean latin1 = true; + while (position < inputLength) { + char ch = charAtFast(position++); + if (ch == '"') { + return JsonFieldNameHash.finish(hash, value, nameLength, latin1); + } + if (ch == '\\') { + ch = readEscapedFieldNameChar(); + if (Character.isHighSurrogate(ch)) { + if (latin1) { + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, ch); + nameLength++; + if (position + 2 > inputLength + || charAtFast(position) != '\\' + || charAtFast(position + 1) != 'u') { + throw error("Unpaired high surrogate escape"); + } + position += 2; + char low = readUnicodeEscape(); + if (!Character.isLowSurrogate(low)) { + throw error("Unpaired high surrogate escape"); + } + hash = JsonFieldNameHash.update(hash, low); + nameLength++; + } else if (Character.isLowSurrogate(ch)) { + throw error("Unpaired low surrogate escape"); + } else { + if (latin1) { + if (ch <= 0xFF && ch != 0 && nameLength < Long.BYTES) { + value = JsonFieldNameHash.value(value, nameLength, ch); + nameLength++; + continue; + } + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, ch); + nameLength++; + } + continue; + } + if (ch < 0x20) { + throw error("Control character in string"); + } + if (Character.isHighSurrogate(ch)) { + if (position >= inputLength || !Character.isLowSurrogate(charAtFast(position))) { + throw error("Unpaired high surrogate in string"); + } + if (latin1) { + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, ch); + hash = JsonFieldNameHash.update(hash, charAtFast(position++)); + nameLength += 2; + continue; + } + if (Character.isLowSurrogate(ch)) { + throw error("Unpaired low surrogate in string"); + } + if (latin1) { + if (ch <= 0xFF && ch != 0 && nameLength < Long.BYTES) { + value = JsonFieldNameHash.value(value, nameLength, ch); + nameLength++; + continue; + } + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, ch); + nameLength++; + } + throw error("Unterminated string"); + } + + @Override + protected String slice(int start, int end) { + return input.substring(start, end); + } + + private void skipWhitespaceFast() { + int inputLength = length; + while (position < inputLength) { + char ch = charAtFast(position); + if (isWhitespace(ch)) { + position++; + } else { + return; + } + } + } + + private static boolean isWhitespace(char ch) { + return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; + } + + private boolean startsWithAscii(String value) { + int end = position + value.length(); + if (end > length) { + return false; + } + for (int i = 0; i < value.length(); i++) { + if (charAtFast(position + i) != value.charAt(i)) { + return false; + } + } + return true; + } + + private void rejectLeadingDigitFast() { + if (position < length) { + char ch = charAtFast(position); + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + } + } + + private void rejectFractionOrExponentFast() { + if (position < length) { + char ch = charAtFast(position); + if (ch == '.' || ch == 'e' || ch == 'E') { + throw error("Expected integer"); + } + } + } + + private int readZeroIntName(int nameStart) { + if (position >= length) { + throw error("Unterminated string"); + } + char ch = charAtFast(position); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameInt(); + } + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + if (ch != '"') { + throw error("Expected integer field name"); + } + position++; + return 0; + } + + private long readZeroLongName(int nameStart) { + if (position >= length) { + throw error("Unterminated string"); + } + char ch = charAtFast(position); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameLong(); + } + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + if (ch != '"') { + throw error("Expected long field name"); + } + position++; + return 0L; + } + + private char charAtFast(int index) { + byte[] localBytes = bytes; + return localBytes == null + ? input.charAt(index) + : StringSerializer.getBytesChar(localBytes, index << 1); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16ObjectReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16ObjectReader.java new file mode 100644 index 0000000000..c06a14bc77 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16ObjectReader.java @@ -0,0 +1,27 @@ +/* + * 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.reader; + +import org.apache.fory.json.codec.BaseObjectCodec; +import org.apache.fory.json.resolver.JsonTypeResolver; + +public interface Utf16ObjectReader { + Object readUtf16(Utf16JsonReader reader, BaseObjectCodec owner, JsonTypeResolver typeResolver); +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java new file mode 100644 index 0000000000..df345945ea --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java @@ -0,0 +1,1236 @@ +/* + * 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.reader; + +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.meta.JsonFieldTable; +import org.apache.fory.memory.LittleEndian; +import org.apache.fory.serializer.StringSerializer; + +public final class Utf8JsonReader extends JsonReader { + private static final byte[] EMPTY_BYTES = new byte[0]; + private static final long BYTE_ONES = 0x0101010101010101L; + private static final int INT_BYTE_ONES = 0x01010101; + private static final long BYTE_HIGH_BITS = 0x8080808080808080L; + private static final int INT_BYTE_HIGH_BITS = 0x80808080; + private static final long BACKSLASH_BYTES = 0x5c5c5c5c5c5c5c5cL; + private static final int INT_BACKSLASH_BYTES = 0x5c5c5c5c; + private static final long CONTROL_LIMIT_BYTES = 0x2020202020202020L; + private static final int INT_CONTROL_LIMIT_BYTES = 0x20202020; + private static final long QUOTE_BYTES = 0x2222222222222222L; + private static final int INT_QUOTE_BYTES = 0x22222222; + + // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. + // UTF-8 string decoding must keep unsigned byte conversion for non-ASCII content. + private byte[] input; + + public Utf8JsonReader() { + input = EMPTY_BYTES; + } + + public Utf8JsonReader(byte[] input) { + this.input = input; + } + + public Utf8JsonReader reset(byte[] input) { + this.input = input; + position = 0; + return this; + } + + public void clear() { + input = EMPTY_BYTES; + position = 0; + } + + public boolean consumeToken(char expected) { + skipWhitespaceFast(); + if (position < input.length && input[position] == expected) { + position++; + return true; + } + return false; + } + + public boolean consumeNextToken(char expected) { + if (position < input.length && input[position] == expected) { + position++; + return true; + } + return consumeToken(expected); + } + + public void expectToken(char expected) { + if (!consumeToken(expected)) { + throw error("Expected '" + expected + "'"); + } + } + + public void expectNextToken(char expected) { + if (position < input.length && input[position] == expected) { + position++; + return; + } + expectNextTokenSlow(expected); + } + + private void expectNextTokenSlow(char expected) { + expectToken(expected); + } + + public boolean consumeNextCommaOrEndObject() { + if (position < input.length) { + int ch = input[position]; + if (ch == ',') { + position++; + return true; + } + if (ch == '}') { + position++; + return false; + } + if (!isWhitespace(ch)) { + return consumeNextCommaOrEndObjectSlow(); + } + } + return consumeNextCommaOrEndObjectSlow(); + } + + private boolean consumeNextCommaOrEndObjectSlow() { + skipWhitespaceFast(); + if (position < input.length) { + int ch = input[position]; + if (ch == ',') { + position++; + return true; + } + if (ch == '}') { + position++; + return false; + } + } + throw error("Expected ',' or '}'"); + } + + public boolean consumeNextCommaOrEndArray() { + if (position < input.length) { + int ch = input[position]; + if (ch == ',') { + position++; + return true; + } + if (ch == ']') { + position++; + return false; + } + if (!isWhitespace(ch)) { + return consumeNextCommaOrEndArraySlow(); + } + } + return consumeNextCommaOrEndArraySlow(); + } + + private boolean consumeNextCommaOrEndArraySlow() { + skipWhitespaceFast(); + if (position < input.length) { + int ch = input[position]; + if (ch == ',') { + position++; + return true; + } + if (ch == ']') { + position++; + return false; + } + } + throw error("Expected ',' or ']'"); + } + + public boolean tryReadNullToken() { + skipWhitespaceFast(); + return tryReadNullLiteral(); + } + + public boolean tryReadNextNullToken() { + if (position < input.length) { + int ch = input[position]; + if (ch == 'n') { + return tryReadNullLiteral(); + } + if (!isWhitespace(ch)) { + return false; + } + } + return tryReadNullToken(); + } + + private boolean tryReadNullLiteral() { + if (startsWithAscii("null")) { + position += 4; + return true; + } + return false; + } + + public boolean readBooleanValue() { + skipWhitespaceFast(); + return readBooleanToken(); + } + + public boolean readNextBooleanValue() { + if (position < input.length && !isWhitespace(input[position])) { + return readBooleanToken(); + } + return readBooleanValue(); + } + + public boolean readBooleanTokenValue() { + return readBooleanToken(); + } + + private boolean readBooleanToken() { + if (startsWithAscii("true")) { + position += 4; + return true; + } else if (startsWithAscii("false")) { + position += 5; + return false; + } + throw error("Expected boolean"); + } + + public int readIntValue() { + skipWhitespaceFast(); + return readIntToken(); + } + + public int readNextIntValue() { + if (position < input.length && !isWhitespace(input[position])) { + return readIntToken(); + } + return readIntValue(); + } + + public int readIntTokenValue() { + return readIntToken(); + } + + private int readIntToken() { + byte[] bytes = input; + int offset = position; + int inputLength = bytes.length; + if (offset >= inputLength) { + throw error("Expected digit"); + } + int ch = bytes[offset]; + if (ch == '-') { + return readNegativeIntToken(offset); + } + if (ch == '0') { + position = offset + 1; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + int result = ch - '0'; + offset++; + int safeEnd = offset + 8; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + while (offset < safeEnd) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + result = result * 10 + (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readPositiveIntTail(bytes, offset, inputLength, result); + } + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private int readPositiveIntTail(byte[] bytes, int offset, int inputLength, int result) { + while (offset < inputLength) { + int ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + long value = (long) result * 10 + (ch - '0'); + if (value > Integer.MAX_VALUE) { + position = offset; + throw error("Integer overflow"); + } + result = (int) value; + offset++; + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private int readNegativeIntToken(int start) { + position = start + 1; + int result = 0; + int limit = Integer.MIN_VALUE; + if (position >= input.length) { + throw error("Expected digit"); + } + int ch = input[position]; + if (ch == '0') { + position++; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + int multmin = limit / 10; + while (position < input.length) { + ch = input[position]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result < multmin) { + throw error("Integer overflow"); + } + result *= 10; + if (result < Integer.MIN_VALUE + digit) { + throw error("Integer overflow"); + } + result -= digit; + position++; + } + rejectFractionOrExponentFast(); + return result; + } + + public long readLongValue() { + skipWhitespaceFast(); + return readLongToken(); + } + + public long readNextLongValue() { + if (position < input.length && !isWhitespace(input[position])) { + return readLongToken(); + } + return readLongValue(); + } + + public long readLongTokenValue() { + return readLongToken(); + } + + private long readLongToken() { + byte[] bytes = input; + int offset = position; + int inputLength = bytes.length; + if (offset >= inputLength) { + throw error("Expected digit"); + } + int ch = bytes[offset]; + if (ch == '-') { + return readNegativeLongToken(offset); + } + if (ch == '0') { + position = offset + 1; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + long result = ch - '0'; + offset++; + int safeEnd = offset + 17; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + while (offset < safeEnd) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + result = result * 10 + (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readPositiveLongTail(bytes, offset, inputLength, result); + } + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, long result) { + while (offset < inputLength) { + int ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result > (Long.MAX_VALUE - digit) / 10) { + position = offset; + throw error("Long overflow"); + } + result = result * 10 + digit; + offset++; + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readNegativeLongToken(int start) { + position = start + 1; + long result = 0; + long limit = Long.MIN_VALUE; + if (position >= input.length) { + throw error("Expected digit"); + } + int ch = input[position]; + if (ch == '0') { + position++; + rejectLeadingDigitFast(); + rejectFractionOrExponentFast(); + return 0; + } + if (ch < '1' || ch > '9') { + throw error("Expected digit"); + } + long multmin = limit / 10; + while (position < input.length) { + ch = input[position]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (result < multmin) { + throw error("Long overflow"); + } + result *= 10; + if (result < Long.MIN_VALUE + digit) { + throw error("Long overflow"); + } + result -= digit; + position++; + } + rejectFractionOrExponentFast(); + return result; + } + + @Override + public int readFieldNameInt() { + skipWhitespaceFast(); + int nameStart = position; + if (position >= input.length || input[position++] != '"') { + throw error("Expected string"); + } + int result = 0; + int limit = -Integer.MAX_VALUE; + boolean negative = false; + if (position < input.length && input[position] == '-') { + negative = true; + limit = Integer.MIN_VALUE; + position++; + } + if (position >= input.length) { + throw error("Unterminated string"); + } + int ch = input[position]; + if (ch == '\\') { + position = nameStart; + return super.readFieldNameInt(); + } + if (ch == '0') { + position++; + return readZeroIntName(nameStart); + } + if (ch < '1' || ch > '9') { + throw error("Expected integer field name"); + } + int multmin = limit / 10; + do { + int digit = ch - '0'; + if (result < multmin) { + throw error("Integer overflow"); + } + result *= 10; + if (result < limit + digit) { + throw error("Integer overflow"); + } + result -= digit; + position++; + if (position >= input.length) { + throw error("Unterminated string"); + } + ch = input[position]; + } while (ch >= '0' && ch <= '9'); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameInt(); + } + if (ch != '"') { + throw error("Expected integer field name"); + } + position++; + return negative ? result : -result; + } + + @Override + public long readFieldNameLong() { + skipWhitespaceFast(); + int nameStart = position; + if (position >= input.length || input[position++] != '"') { + throw error("Expected string"); + } + long result = 0; + long limit = -Long.MAX_VALUE; + boolean negative = false; + if (position < input.length && input[position] == '-') { + negative = true; + limit = Long.MIN_VALUE; + position++; + } + if (position >= input.length) { + throw error("Unterminated string"); + } + int ch = input[position]; + if (ch == '\\') { + position = nameStart; + return super.readFieldNameLong(); + } + if (ch == '0') { + position++; + return readZeroLongName(nameStart); + } + if (ch < '1' || ch > '9') { + throw error("Expected long field name"); + } + long multmin = limit / 10; + do { + int digit = ch - '0'; + if (result < multmin) { + throw error("Long overflow"); + } + result *= 10; + if (result < limit + digit) { + throw error("Long overflow"); + } + result -= digit; + position++; + if (position >= input.length) { + throw error("Unterminated string"); + } + ch = input[position]; + } while (ch >= '0' && ch <= '9'); + if (ch == '\\') { + position = nameStart; + return super.readFieldNameLong(); + } + if (ch != '"') { + throw error("Expected long field name"); + } + position++; + return negative ? result : -result; + } + + @Override + protected int length() { + return input.length; + } + + @Override + protected char charAt(int index) { + return (char) (input[index] & 0xFF); + } + + @Override + public String readString() { + skipWhitespaceFast(); + return readStringToken(); + } + + @Override + public String readNullableString() { + skipWhitespaceFast(); + if (tryReadNullLiteral()) { + return null; + } + return readStringToken(); + } + + public String readNextNullableString() { + if (position < input.length) { + int ch = input[position]; + if (ch == '"') { + return readStringToken(); + } + if (ch == 'n' && tryReadNullLiteral()) { + return null; + } + if (!isWhitespace(ch)) { + return readStringToken(); + } + } + return readNullableString(); + } + + public String readNullableStringToken() { + if (position < input.length) { + int ch = input[position]; + if (ch == '"') { + return readStringToken(); + } + if (ch == 'n' && tryReadNullLiteral()) { + return null; + } + } + return readStringToken(); + } + + private String readStringToken() { + byte[] bytes = input; + int inputLength = bytes.length; + if (position >= inputLength || bytes[position++] != '"') { + throw error("Expected string"); + } + int start = position; + int offset = start; + int wordEnd = inputLength - Long.BYTES; + while (offset <= wordEnd) { + long stopMask = stringStopMask(LittleEndian.getInt64(bytes, offset)); + if (stopMask == 0) { + offset += Long.BYTES; + continue; + } + int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3); + int b = bytes[stop]; + if (b == '"') { + position = stop + 1; + return newLatin1String(start, stop); + } + return readStringStop(start, stop, b); + } + if (offset + Integer.BYTES <= inputLength) { + int stopMask = stringStopMask(LittleEndian.getInt32(bytes, offset)); + if (stopMask == 0) { + offset += Integer.BYTES; + } else { + int stop = offset + (Integer.numberOfTrailingZeros(stopMask) >>> 3); + int b = bytes[stop]; + if (b == '"') { + position = stop + 1; + return newLatin1String(start, stop); + } + return readStringStop(start, stop, b); + } + } + while (offset < inputLength) { + int b = bytes[offset++]; + if (b == '"') { + position = offset; + return newLatin1String(start, offset - 1); + } + if (b == '\\') { + return readStringStop(start, offset - 1, b); + } + if (b < 0) { + return readStringStop(start, offset - 1, b); + } + if (b < 0x20) { + position = offset; + throw error("Control character in string"); + } + } + throw error("Unterminated string"); + } + + private String readStringStop(int start, int stop, int b) { + position = stop + 1; + if (b >= 0 && b < 0x20) { + throw error("Control character in string"); + } + StringBuilder builder = newStringBuilder(start, stop); + appendAscii(builder, start, stop); + if (b == '\\') { + appendEscape(builder); + return readStringTail(builder); + } + if (b < 0) { + appendUtf8(builder, b & 0xFF); + return readStringTail(builder); + } + appendUtf8(builder, b); + return readStringTail(builder); + } + + private StringBuilder newStringBuilder(int start, int stop) { + return new StringBuilder(Math.max(16, stop - start + 16)); + } + + private static long stringStopMask(long word) { + return (word & BYTE_HIGH_BITS) + | byteMatchMask(word, QUOTE_BYTES) + | byteMatchMask(word, BACKSLASH_BYTES) + | ((word - CONTROL_LIMIT_BYTES) & ~word & BYTE_HIGH_BITS); + } + + private static int stringStopMask(int word) { + return (word & INT_BYTE_HIGH_BITS) + | byteMatchMask(word, INT_QUOTE_BYTES) + | byteMatchMask(word, INT_BACKSLASH_BYTES) + | ((word - INT_CONTROL_LIMIT_BYTES) & ~word & INT_BYTE_HIGH_BITS); + } + + private static long byteMatchMask(long word, long repeatedByte) { + long match = word ^ repeatedByte; + return (match - BYTE_ONES) & ~match & BYTE_HIGH_BITS; + } + + private static int byteMatchMask(int word, int repeatedByte) { + int match = word ^ repeatedByte; + return (match - INT_BYTE_ONES) & ~match & INT_BYTE_HIGH_BITS; + } + + @Override + public JsonFieldInfo readField(JsonFieldTable table) { + return table.get(readFieldNameHash()); + } + + @Override + public int readFieldIndex(JsonFieldTable table) { + return table.index(readFieldNameHash()); + } + + @Override + public int readFieldIndex(JsonFieldTable table, long expectedHash, int expectedIndex) { + long hash = readFieldNameHash(); + return hash == expectedHash ? expectedIndex : table.index(hash); + } + + @Override + public long readFieldNameHash() { + return readQuotedStringHash(); + } + + public boolean tryReadFieldNameColon(long expectedHash, long expectedMask, int expectedLength) { + int mark = position; + skipWhitespaceFast(); + return tryReadFieldNameColonAt(mark, expectedHash, expectedMask, expectedLength); + } + + public boolean tryReadNextFieldNameColon( + long expectedHash, long expectedMask, int expectedLength) { + int mark = position; + if (mark < input.length) { + int ch = input[mark]; + if (ch == '"') { + return tryReadFieldNameColonAt(mark, expectedHash, expectedMask, expectedLength); + } + if (!isWhitespace(ch)) { + return false; + } + } + return tryReadFieldNameColon(expectedHash, expectedMask, expectedLength); + } + + public boolean tryReadNextFieldNameToken0(long prefix, long prefixMask, int tokenLength) { + return tryReadNextRawToken0(prefix, prefixMask, tokenLength); + } + + public boolean tryReadNextStringToken0(long prefix, long prefixMask, int tokenLength) { + return tryReadNextRawToken0(prefix, prefixMask, tokenLength); + } + + private boolean tryReadNextRawToken0(long prefix, long prefixMask, int tokenLength) { + byte[] bytes = input; + int mark = position; + if (mark + Long.BYTES <= bytes.length + && (LittleEndian.getInt64(bytes, mark) & prefixMask) == prefix) { + position = mark + tokenLength; + return true; + } + return false; + } + + public boolean tryReadNextFieldNameToken1( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken1(prefix, prefixMask, suffix, tokenLength); + } + + public boolean tryReadNextStringToken1( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken1(prefix, prefixMask, suffix, tokenLength); + } + + private boolean tryReadNextRawToken1(long prefix, long prefixMask, int suffix, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && (LittleEndian.getInt64(bytes, mark) & prefixMask) == prefix + && bytes[suffixOffset] == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + public boolean tryReadNextFieldNameToken2( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken2(prefix, prefixMask, suffix, tokenLength); + } + + public boolean tryReadNextStringToken2( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken2(prefix, prefixMask, suffix, tokenLength); + } + + private boolean tryReadNextRawToken2(long prefix, long prefixMask, int suffix, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && (LittleEndian.getInt64(bytes, mark) & prefixMask) == prefix + && ((bytes[suffixOffset] & 0xFF) | ((bytes[suffixOffset + 1] & 0xFF) << 8)) == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + public boolean tryReadNextFieldNameToken3( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken3(prefix, prefixMask, suffix, tokenLength); + } + + public boolean tryReadNextStringToken3( + long prefix, long prefixMask, int suffix, int tokenLength) { + return tryReadNextRawToken3(prefix, prefixMask, suffix, tokenLength); + } + + private boolean tryReadNextRawToken3(long prefix, long prefixMask, int suffix, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && (LittleEndian.getInt64(bytes, mark) & prefixMask) == prefix + && ((bytes[suffixOffset] & 0xFF) + | ((bytes[suffixOffset + 1] & 0xFF) << 8) + | ((bytes[suffixOffset + 2] & 0xFF) << 16)) + == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + private boolean tryReadFieldNameColonAt( + int mark, long expectedHash, long expectedMask, int expectedLength) { + byte[] bytes = input; + int offset = position; + int nameOffset = offset + 1; + int quoteOffset = nameOffset + expectedLength; + if (quoteOffset < bytes.length && bytes[offset] == '"') { + if (nameOffset + Long.BYTES <= bytes.length) { + if ((LittleEndian.getInt64(bytes, nameOffset) & expectedMask) == expectedHash + && bytes[quoteOffset] == '"') { + int colonOffset = quoteOffset + 1; + if (colonOffset < bytes.length && bytes[colonOffset] == ':') { + position = colonOffset + 1; + } else { + readFieldNameColon(colonOffset); + } + return true; + } + // Full raw-word misses cannot match this generated packed-name probe. Escaped and UTF8 + // field names are handled by the hash fallback after this direct probe fails. + position = mark; + return false; + } + offset = nameOffset; + long value = 0; + for (int i = 0; i < expectedLength; i++) { + int ch = bytes[offset++]; + if (ch == 0 || ch == '"' || ch == '\\' || ch < 0x20) { + position = mark; + return false; + } + value = JsonFieldNameHash.value(value, i, (char) ch); + } + if (value == expectedHash && bytes[offset] == '"') { + int colonOffset = offset + 1; + if (colonOffset < bytes.length && bytes[colonOffset] == ':') { + position = colonOffset + 1; + } else { + readFieldNameColon(colonOffset); + } + return true; + } + } + position = mark; + return false; + } + + private void readFieldNameColon(int colonOffset) { + position = colonOffset; + expectNextToken(':'); + } + + @Override + public long readStringHash() { + return readQuotedStringHash(); + } + + public long readPackedStringHash() { + skipWhitespaceFast(); + return readPackedStringHashToken(); + } + + public long readNextPackedStringHash() { + if (position < input.length && !isWhitespace(input[position])) { + return readPackedStringHashToken(); + } + return readPackedStringHash(); + } + + public long readPackedStringHashTokenValue() { + return readPackedStringHashToken(); + } + + private long readPackedStringHashToken() { + int mark = position; + byte[] bytes = input; + int length = bytes.length; + int offset = position; + if (offset < length && bytes[offset++] == '"') { + long value = 0; + int nameLength = 0; + while (offset < length) { + int ch = bytes[offset++]; + if (ch == '"') { + if (nameLength > 0) { + position = offset; + return value; + } + break; + } + if (ch == 0 || ch == '\\' || ch < 0x20 || nameLength >= Long.BYTES) { + break; + } + value = JsonFieldNameHash.value(value, nameLength++, (char) ch); + } + } + return readQuotedStringHashFromMark(mark); + } + + private long readQuotedStringHashFromMark(int mark) { + position = mark; + return readQuotedStringHashToken(); + } + + private long readQuotedStringHash() { + skipWhitespaceFast(); + return readQuotedStringHashToken(); + } + + private long readQuotedStringHashToken() { + byte[] bytes = input; + int length = bytes.length; + if (position >= length || bytes[position++] != '"') { + throw error("Expected string"); + } + long hash = JsonFieldNameHash.MAGIC_HASH_CODE; + long value = 0; + int nameLength = 0; + boolean latin1 = true; + while (position < length) { + int b = bytes[position++] & 0xFF; + if (b == '"') { + return JsonFieldNameHash.finish(hash, value, nameLength, latin1); + } + if (b == '\\') { + char escaped = readEscapedFieldNameChar(); + if (Character.isHighSurrogate(escaped)) { + if (latin1) { + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, escaped); + nameLength++; + if (position + 2 > length() || charAt(position) != '\\' || charAt(position + 1) != 'u') { + throw error("Unpaired high surrogate escape"); + } + position += 2; + char low = readUnicodeEscape(); + if (!Character.isLowSurrogate(low)) { + throw error("Unpaired high surrogate escape"); + } + hash = JsonFieldNameHash.update(hash, low); + nameLength++; + } else if (Character.isLowSurrogate(escaped)) { + throw error("Unpaired low surrogate escape"); + } else { + if (latin1) { + if (escaped <= 0xFF && escaped != 0 && nameLength < Long.BYTES) { + value = JsonFieldNameHash.value(value, nameLength, escaped); + nameLength++; + continue; + } + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, escaped); + nameLength++; + } + continue; + } + if (b < 0x20) { + throw error("Control character in string"); + } + if (b < 0x80) { + if (latin1) { + if (b != 0 && nameLength < Long.BYTES) { + value = JsonFieldNameHash.value(value, nameLength, (char) b); + nameLength++; + continue; + } + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, (char) b); + nameLength++; + continue; + } + int codePoint = readUtf8CodePoint(b); + if (codePoint <= 0xFFFF) { + char ch = (char) codePoint; + if (latin1) { + if (ch <= 0xFF && ch != 0 && nameLength < Long.BYTES) { + value = JsonFieldNameHash.value(value, nameLength, ch); + nameLength++; + continue; + } + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, ch); + nameLength++; + } else { + if (latin1) { + hash = JsonFieldNameHash.hashPacked(value, nameLength); + latin1 = false; + } + hash = JsonFieldNameHash.update(hash, Character.highSurrogate(codePoint)); + hash = JsonFieldNameHash.update(hash, Character.lowSurrogate(codePoint)); + nameLength += 2; + } + } + throw error("Unterminated string"); + } + + @Override + protected String slice(int start, int end) { + return newLatin1String(start, end); + } + + private String newLatin1String(int start, int end) { + int length = end - start; + byte[] bytes = new byte[length]; + System.arraycopy(input, start, bytes, 0, length); + return StringSerializer.newLatin1StringZeroCopy(bytes); + } + + private String readStringTail(StringBuilder builder) { + while (position < input.length) { + int b = input[position++] & 0xFF; + if (b == '"') { + return builder.toString(); + } else if (b == '\\') { + appendEscape(builder); + } else if (b < 0x20) { + throw error("Control character in string"); + } else if (b < 0x80) { + builder.append((char) b); + } else { + appendUtf8(builder, b); + } + } + throw error("Unterminated string"); + } + + private void appendAscii(StringBuilder builder, int start, int end) { + for (int i = start; i < end; i++) { + builder.append((char) (input[i] & 0xFF)); + } + } + + private void appendUtf8(StringBuilder builder, int first) { + int codePoint = readUtf8CodePoint(first); + if (codePoint <= 0xFFFF) { + builder.append((char) codePoint); + } else { + builder.append(Character.highSurrogate(codePoint)); + builder.append(Character.lowSurrogate(codePoint)); + } + } + + private int readUtf8CodePoint(int first) { + if ((first & 0xE0) == 0xC0) { + int second = continuation(); + int codePoint = ((first & 0x1F) << 6) | second; + if (codePoint < 0x80) { + throw error("Overlong UTF-8 sequence"); + } + return codePoint; + } else if ((first & 0xF0) == 0xE0) { + int second = continuation(); + int third = continuation(); + int codePoint = ((first & 0x0F) << 12) | (second << 6) | third; + if (codePoint < 0x800 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) { + throw error("Invalid UTF-8 sequence"); + } + return codePoint; + } else if ((first & 0xF8) == 0xF0) { + int second = continuation(); + int third = continuation(); + int fourth = continuation(); + int codePoint = ((first & 0x07) << 18) | (second << 12) | (third << 6) | fourth; + if (codePoint < 0x10000 || codePoint > 0x10FFFF) { + throw error("Invalid UTF-8 sequence"); + } + return codePoint; + } + throw error("Invalid UTF-8 sequence"); + } + + private int continuation() { + if (position >= input.length) { + throw error("Short UTF-8 sequence"); + } + int value = input[position++] & 0xFF; + if ((value & 0xC0) != 0x80) { + throw error("Invalid UTF-8 continuation"); + } + return value & 0x3F; + } + + private void skipWhitespaceFast() { + while (position < input.length) { + int ch = input[position]; + if (isWhitespace(ch)) { + position++; + } else { + return; + } + } + } + + private static boolean isWhitespace(int ch) { + return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; + } + + private boolean startsWithAscii(String value) { + int end = position + value.length(); + if (end > input.length) { + return false; + } + for (int i = 0; i < value.length(); i++) { + if (input[position + i] != value.charAt(i)) { + return false; + } + } + return true; + } + + private void rejectLeadingDigitFast() { + if (position < input.length) { + int ch = input[position]; + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + } + } + + private void rejectFractionOrExponentFast() { + if (position < input.length) { + int ch = input[position]; + if (ch == '.' || ch == 'e' || ch == 'E') { + throw error("Expected integer"); + } + } + } + + private int readZeroIntName(int nameStart) { + if (position >= input.length) { + throw error("Unterminated string"); + } + int ch = input[position]; + if (ch == '\\') { + position = nameStart; + return super.readFieldNameInt(); + } + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + if (ch != '"') { + throw error("Expected integer field name"); + } + position++; + return 0; + } + + private long readZeroLongName(int nameStart) { + if (position >= input.length) { + throw error("Unterminated string"); + } + int ch = input[position]; + if (ch == '\\') { + position = nameStart; + return super.readFieldNameLong(); + } + if (ch >= '0' && ch <= '9') { + throw error("Leading zero in number"); + } + if (ch != '"') { + throw error("Expected long field name"); + } + position++; + return 0L; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8ObjectReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8ObjectReader.java new file mode 100644 index 0000000000..7d5e855734 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8ObjectReader.java @@ -0,0 +1,27 @@ +/* + * 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.reader; + +import org.apache.fory.json.codec.BaseObjectCodec; +import org.apache.fory.json.resolver.JsonTypeResolver; + +public interface Utf8ObjectReader { + Object readUtf8(Utf8JsonReader reader, BaseObjectCodec owner, JsonTypeResolver typeResolver); +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java new file mode 100644 index 0000000000..629eae9d3f --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java @@ -0,0 +1,57 @@ +/* + * 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.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.fory.json.codec.JsonCodec; +import org.apache.fory.util.Preconditions; + +/** Registry for user-supplied JSON codecs. */ +public final class CodecRegistry { + private final ConcurrentMap, JsonCodec> codecs; + + public CodecRegistry() { + codecs = new ConcurrentHashMap<>(); + } + + private CodecRegistry(ConcurrentMap, JsonCodec> codecs) { + this.codecs = codecs; + } + + public void register(Class type, JsonCodec codec) { + Preconditions.checkNotNull(type); + Preconditions.checkNotNull(codec); + codecs.put(type, codec); + } + + public JsonCodec get(Class type) { + return codecs.get(type); + } + + public CodecRegistry copy() { + ConcurrentMap, JsonCodec> copied = new ConcurrentHashMap<>(codecs.size()); + for (Map.Entry, JsonCodec> entry : codecs.entrySet()) { + copied.put(entry.getKey(), entry.getValue()); + } + return new CodecRegistry(copied); + } +} 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 new file mode 100644 index 0000000000..0d662ab955 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -0,0 +1,243 @@ +/* + * 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.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Collection; +import java.util.Currency; +import java.util.Date; +import java.util.IdentityHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; +import org.apache.fory.json.codec.ArrayCodec; +import org.apache.fory.json.codec.BaseObjectCodec; +import org.apache.fory.json.codec.CodecUtils; +import org.apache.fory.json.codec.CollectionCodec; +import org.apache.fory.json.codec.JsonCodec; +import org.apache.fory.json.codec.MapCodec; +import org.apache.fory.json.codec.ObjectCodecs; +import org.apache.fory.json.codec.ScalarCodecs; +import org.apache.fory.json.codegen.JsonCodegen; +import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.reflect.TypeRef; +import org.apache.fory.type.BFloat16; +import org.apache.fory.type.Float16; + +/** Shared JSON codec registry used by all local resolvers for one {@code ForyJson}. */ +public final class JsonSharedRegistry { + private final CodecRegistry customCodecs; + private final IdentityHashMap, JsonCodec> exactCodecs; + private final JsonCodegen codegen; + + public JsonSharedRegistry( + boolean codegenEnabled, boolean writeNullFields, CodecRegistry customCodecs) { + this.customCodecs = customCodecs.copy(); + exactCodecs = new IdentityHashMap<>(); + codegen = codegenEnabled ? new JsonCodegen(writeNullFields) : null; + registerExactCodecs(); + } + + public JsonCodec createCodec( + Class rawType, TypeRef typeRef, JsonTypeResolver localResolver) { + JsonCodec customCodec = customCodecs.get(rawType); + if (customCodec != null) { + return customCodec; + } + JsonCodec codec = exactCodecs.get(rawType); + if (codec != null) { + return codec; + } + if (rawType.isEnum()) { + return new ScalarCodecs.EnumCodec(rawType); + } + if (rawType.isArray()) { + return ArrayCodec.create(rawType.getComponentType(), localResolver); + } + if (rawType == Optional.class) { + return new ScalarCodecs.OptionalCodec( + CodecUtils.elementType(typeRef.getType()), localResolver); + } + if (rawType == AtomicReference.class) { + return new ScalarCodecs.AtomicReferenceCodec( + CodecUtils.elementType(typeRef.getType()), localResolver); + } + if (Calendar.class.isAssignableFrom(rawType)) { + return ScalarCodecs.CalendarCodec.INSTANCE; + } + if (Date.class.isAssignableFrom(rawType)) { + return ScalarCodecs.DateCodec.INSTANCE; + } + if (ZoneId.class.isAssignableFrom(rawType)) { + return ScalarCodecs.ZoneIdCodec.INSTANCE; + } + if (ByteBuffer.class.isAssignableFrom(rawType)) { + return ScalarCodecs.ByteBufferCodec.INSTANCE; + } + if (Collection.class.isAssignableFrom(rawType)) { + return CollectionCodec.create(rawType, typeRef, localResolver); + } + if (Map.class.isAssignableFrom(rawType)) { + return MapCodec.create(rawType, typeRef, localResolver); + } + return null; + } + + public JsonFieldKind kind(Class type) { + if (type == boolean.class || type == Boolean.class) { + return JsonFieldKind.BOOLEAN; + } + if (type == byte.class || type == Byte.class) { + return JsonFieldKind.BYTE; + } + if (type == short.class || type == Short.class) { + return JsonFieldKind.SHORT; + } + if (type == int.class || type == Integer.class) { + return JsonFieldKind.INT; + } + if (type == long.class || type == Long.class) { + return JsonFieldKind.LONG; + } + if (type == float.class || type == Float.class) { + return JsonFieldKind.FLOAT; + } + if (type == double.class || type == Double.class) { + return JsonFieldKind.DOUBLE; + } + if (type == char.class || type == Character.class) { + return JsonFieldKind.CHAR; + } + if (type == String.class) { + return JsonFieldKind.STRING; + } + if (type.isEnum()) { + return JsonFieldKind.ENUM; + } + if (type.isArray()) { + return JsonFieldKind.ARRAY; + } + if (Collection.class.isAssignableFrom(type)) { + return JsonFieldKind.COLLECTION; + } + if (Map.class.isAssignableFrom(type)) { + return JsonFieldKind.MAP; + } + return JsonFieldKind.OBJECT; + } + + public ObjectCodecs compileObject(BaseObjectCodec codec, JsonTypeResolver localResolver) { + return codegen == null ? null : codegen.compile(codec, localResolver); + } + + private void registerExactCodecs() { + exactCodecs.put(Object.class, ScalarCodecs.NaturalCodec.INSTANCE); + exactCodecs.put(void.class, ScalarCodecs.VoidCodec.INSTANCE); + exactCodecs.put(Void.class, ScalarCodecs.VoidCodec.INSTANCE); + exactCodecs.put(String.class, ScalarCodecs.StringCodec.INSTANCE); + exactCodecs.put(boolean.class, ScalarCodecs.BooleanCodec.INSTANCE); + exactCodecs.put(Boolean.class, ScalarCodecs.BooleanCodec.INSTANCE); + exactCodecs.put(int.class, ScalarCodecs.IntCodec.INSTANCE); + exactCodecs.put(Integer.class, ScalarCodecs.IntCodec.INSTANCE); + exactCodecs.put(long.class, ScalarCodecs.LongCodec.INSTANCE); + exactCodecs.put(Long.class, ScalarCodecs.LongCodec.INSTANCE); + exactCodecs.put(short.class, ScalarCodecs.ShortCodec.INSTANCE); + exactCodecs.put(Short.class, ScalarCodecs.ShortCodec.INSTANCE); + exactCodecs.put(byte.class, ScalarCodecs.ByteCodec.INSTANCE); + exactCodecs.put(Byte.class, ScalarCodecs.ByteCodec.INSTANCE); + exactCodecs.put(char.class, ScalarCodecs.CharCodec.INSTANCE); + exactCodecs.put(Character.class, ScalarCodecs.CharCodec.INSTANCE); + exactCodecs.put(float.class, ScalarCodecs.FloatCodec.INSTANCE); + exactCodecs.put(Float.class, ScalarCodecs.FloatCodec.INSTANCE); + exactCodecs.put(double.class, ScalarCodecs.DoubleCodec.INSTANCE); + exactCodecs.put(Double.class, ScalarCodecs.DoubleCodec.INSTANCE); + exactCodecs.put(BigInteger.class, ScalarCodecs.BigIntegerCodec.INSTANCE); + exactCodecs.put(BigDecimal.class, ScalarCodecs.BigDecimalCodec.INSTANCE); + exactCodecs.put(Float16.class, ScalarCodecs.Float16Codec.INSTANCE); + exactCodecs.put(BFloat16.class, ScalarCodecs.BFloat16Codec.INSTANCE); + exactCodecs.put(Class.class, ScalarCodecs.ClassCodec.INSTANCE); + exactCodecs.put(StringBuilder.class, ScalarCodecs.StringBuilderCodec.INSTANCE); + exactCodecs.put(StringBuffer.class, ScalarCodecs.StringBufferCodec.INSTANCE); + exactCodecs.put(AtomicBoolean.class, ScalarCodecs.AtomicBooleanCodec.INSTANCE); + exactCodecs.put(AtomicInteger.class, ScalarCodecs.AtomicIntegerCodec.INSTANCE); + exactCodecs.put(AtomicLong.class, ScalarCodecs.AtomicLongCodec.INSTANCE); + exactCodecs.put(Currency.class, ScalarCodecs.CurrencyCodec.INSTANCE); + exactCodecs.put(URI.class, ScalarCodecs.UriCodec.INSTANCE); + exactCodecs.put(URL.class, ScalarCodecs.UrlCodec.INSTANCE); + exactCodecs.put(Pattern.class, ScalarCodecs.PatternCodec.INSTANCE); + exactCodecs.put(UUID.class, ScalarCodecs.UuidCodec.INSTANCE); + exactCodecs.put(Locale.class, ScalarCodecs.LocaleCodec.INSTANCE); + exactCodecs.put(Charset.class, ScalarCodecs.CharsetCodec.INSTANCE); + exactCodecs.put(Date.class, ScalarCodecs.DateCodec.INSTANCE); + exactCodecs.put(java.sql.Date.class, ScalarCodecs.SqlDateCodec.INSTANCE); + exactCodecs.put(java.sql.Time.class, ScalarCodecs.SqlTimeCodec.INSTANCE); + exactCodecs.put(java.sql.Timestamp.class, ScalarCodecs.TimestampCodec.INSTANCE); + exactCodecs.put(Calendar.class, ScalarCodecs.CalendarCodec.INSTANCE); + exactCodecs.put(TimeZone.class, ScalarCodecs.TimeZoneCodec.INSTANCE); + exactCodecs.put(LocalDate.class, ScalarCodecs.LocalDateCodec.INSTANCE); + exactCodecs.put(LocalTime.class, ScalarCodecs.LocalTimeCodec.INSTANCE); + exactCodecs.put(LocalDateTime.class, ScalarCodecs.LocalDateTimeCodec.INSTANCE); + exactCodecs.put(Instant.class, ScalarCodecs.InstantCodec.INSTANCE); + exactCodecs.put(Duration.class, ScalarCodecs.DurationCodec.INSTANCE); + exactCodecs.put(ZoneOffset.class, ScalarCodecs.ZoneOffsetCodec.INSTANCE); + exactCodecs.put(ZoneId.class, ScalarCodecs.ZoneIdCodec.INSTANCE); + exactCodecs.put(ZonedDateTime.class, ScalarCodecs.ZonedDateTimeCodec.INSTANCE); + exactCodecs.put(Year.class, ScalarCodecs.YearCodec.INSTANCE); + exactCodecs.put(YearMonth.class, ScalarCodecs.YearMonthCodec.INSTANCE); + exactCodecs.put(MonthDay.class, ScalarCodecs.MonthDayCodec.INSTANCE); + exactCodecs.put(Period.class, ScalarCodecs.PeriodCodec.INSTANCE); + exactCodecs.put(OffsetTime.class, ScalarCodecs.OffsetTimeCodec.INSTANCE); + exactCodecs.put(OffsetDateTime.class, ScalarCodecs.OffsetDateTimeCodec.INSTANCE); + exactCodecs.put(OptionalInt.class, ScalarCodecs.OptionalIntCodec.INSTANCE); + exactCodecs.put(OptionalLong.class, ScalarCodecs.OptionalLongCodec.INSTANCE); + exactCodecs.put(OptionalDouble.class, ScalarCodecs.OptionalDoubleCodec.INSTANCE); + exactCodecs.put(ByteBuffer.class, ScalarCodecs.ByteBufferCodec.INSTANCE); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java new file mode 100644 index 0000000000..dd61f69221 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java @@ -0,0 +1,69 @@ +/* + * 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.lang.reflect.Type; +import org.apache.fory.json.codec.JsonCodec; +import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.reflect.TypeRef; + +/** Immutable JSON type binding resolved and owned by {@link JsonTypeResolver}. */ +public final class JsonTypeInfo { + private final Type type; + private final TypeRef typeRef; + private final Class rawType; + private final JsonFieldKind kind; + private final JsonCodec codec; + private final boolean primitive; + + JsonTypeInfo( + Type type, TypeRef typeRef, Class rawType, JsonFieldKind kind, JsonCodec codec) { + this.type = type; + this.typeRef = typeRef; + this.rawType = rawType; + this.kind = kind; + this.codec = codec; + primitive = rawType.isPrimitive(); + } + + public Type type() { + return type; + } + + public TypeRef typeRef() { + return typeRef; + } + + public Class rawType() { + return rawType; + } + + public JsonFieldKind kind() { + return kind; + } + + public JsonCodec codec() { + return codec; + } + + public boolean primitive() { + return primitive; + } +} 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 new file mode 100644 index 0000000000..0b23bbb38b --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -0,0 +1,154 @@ +/* + * 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.lang.reflect.Type; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import org.apache.fory.json.codec.BaseObjectCodec; +import org.apache.fory.json.codec.CodecUtils; +import org.apache.fory.json.codec.JsonCodec; +import org.apache.fory.json.codec.ObjectCodec; +import org.apache.fory.json.codec.ObjectCodecs; +import org.apache.fory.reflect.TypeRef; + +/** + * Local JSON type dispatcher and cache used by one borrowed {@code ForyJson} state at a time. + * + *

This cache is limited to schema/static metadata such as resolved codecs and object layouts. + * Runtime JSON values, including non-enumerated string or number values/tokens, must stay uncached. + */ +public final class JsonTypeResolver { + private final IdentityHashMap, BaseObjectCodec> objectCodecs = new IdentityHashMap<>(); + private final Map typeInfos = new HashMap<>(); + private final JsonSharedRegistry sharedRegistry; + + private enum RuntimeObjectKey { + INSTANCE + } + + public JsonTypeResolver(JsonSharedRegistry sharedRegistry) { + this.sharedRegistry = sharedRegistry; + } + + public BaseObjectCodec getObjectCodec(Class type) { + BaseObjectCodec codec = objectCodecs.get(type); + if (codec != null) { + return codec; + } + return buildObjectCodec(type); + } + + public JsonTypeInfo getTypeInfo(Type declaredType, Class fallback) { + Class rawType = CodecUtils.rawType(declaredType, fallback); + Object key = typeInfoKey(declaredType, rawType); + JsonTypeInfo typeInfo = typeInfos.get(key); + if (typeInfo != null) { + return typeInfo; + } + return buildTypeInfo(key, rawType, declaredType); + } + + public JsonTypeInfo getRuntimeTypeInfo(Class runtimeType) { + Object key = runtimeType == Object.class ? RuntimeObjectKey.INSTANCE : runtimeType; + JsonTypeInfo typeInfo = typeInfos.get(key); + if (typeInfo != null) { + return typeInfo; + } + return buildRuntimeTypeInfo(key, runtimeType); + } + + private BaseObjectCodec buildObjectCodec(Class type) { + BaseObjectCodec cached = objectCodecs.get(type); + if (cached != null) { + return cached; + } + ObjectCodec codec = BaseObjectCodec.build(type); + // Codegen may ask for nested object metadata that points back to this type. + // Publishing before compiling keeps recursive ownership in this resolver cache. + objectCodecs.put(type, codec); + try { + codec.resolveTypes(this); + ObjectCodecs codecs = sharedRegistry.compileObject(codec, this); + if (codecs != null) { + BaseObjectCodec generated = codec.withCodecs(codecs); + objectCodecs.put(type, generated); + return generated; + } + return codec; + } catch (RuntimeException | Error e) { + objectCodecs.remove(type, codec); + throw e; + } + } + + private JsonTypeInfo buildTypeInfo(Object key, Class rawType, Type declaredType) { + JsonTypeInfo cached = typeInfos.get(key); + if (cached != null) { + return cached; + } + JsonTypeInfo typeInfo = buildTypeInfo(rawType, declaredType); + typeInfos.put(key, typeInfo); + return typeInfo; + } + + private JsonTypeInfo buildTypeInfo(Class rawType, Type declaredType) { + TypeRef typeRef = typeRef(declaredType, rawType); + JsonCodec codec = sharedRegistry.createCodec(rawType, typeRef, this); + if (codec == null) { + codec = getObjectCodec(rawType); + } + return new JsonTypeInfo(declaredType, typeRef, rawType, sharedRegistry.kind(rawType), codec); + } + + private JsonTypeInfo buildRuntimeTypeInfo(Object key, Class rawType) { + JsonTypeInfo cached = typeInfos.get(key); + if (cached != null) { + return cached; + } + TypeRef typeRef = TypeRef.of(rawType); + JsonCodec codec = + rawType == Object.class + ? getObjectCodec(Object.class) + : sharedRegistry.createCodec(rawType, typeRef, this); + if (codec == null) { + codec = getObjectCodec(rawType); + } + JsonTypeInfo typeInfo = + new JsonTypeInfo(rawType, typeRef, rawType, sharedRegistry.kind(rawType), codec); + typeInfos.put(key, typeInfo); + return typeInfo; + } + + private static Object typeInfoKey(Type declaredType, Class rawType) { + if (declaredType instanceof Class) { + return rawType; + } + return declaredType; + } + + private static TypeRef typeRef(Type declaredType, Class rawType) { + if (declaredType == null || declaredType == Object.class && rawType != Object.class) { + return TypeRef.of(rawType); + } + return TypeRef.of(declaredType); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/GeneratedObjectWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/GeneratedObjectWriter.java new file mode 100644 index 0000000000..130ae08812 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/GeneratedObjectWriter.java @@ -0,0 +1,34 @@ +/* + * 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.writer; + +import org.apache.fory.json.codec.JsonCodec; +import org.apache.fory.json.meta.JsonFieldInfo; + +/** Immutable metadata carrier shared by generated JSON object writers. */ +public abstract class GeneratedObjectWriter { + protected final JsonFieldInfo[] fields; + protected final JsonCodec[] codecs; + + protected GeneratedObjectWriter(JsonFieldInfo[] fields, JsonCodec[] codecs) { + this.fields = fields; + this.codecs = codecs; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonStringEscaper.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonStringEscaper.java new file mode 100644 index 0000000000..4bd812ec18 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonStringEscaper.java @@ -0,0 +1,115 @@ +/* + * 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.writer; + +import java.nio.charset.StandardCharsets; +import org.apache.fory.json.ForyJsonException; + +public final class JsonStringEscaper { + private JsonStringEscaper() {} + + public static String escapedNamePrefix(String name, boolean escapeNonLatin1) { + StringBuilder builder = new StringBuilder(name.length() + 3); + appendQuoted(builder, name, escapeNonLatin1); + builder.append(':'); + return builder.toString(); + } + + public static byte[] stringValue(String value) { + return escapedString(value, true).getBytes(StandardCharsets.ISO_8859_1); + } + + public static byte[] utf8Value(String value) { + return escapedString(value, false).getBytes(StandardCharsets.UTF_8); + } + + private static String escapedString(String value, boolean escapeNonLatin1) { + StringBuilder builder = new StringBuilder(value.length() + 2); + appendQuoted(builder, value, escapeNonLatin1); + return builder.toString(); + } + + private static void appendQuoted(StringBuilder builder, String value, boolean escapeNonLatin1) { + builder.append('"'); + int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + switch (ch) { + case '"': + builder.append("\\\""); + break; + case '\\': + builder.append("\\\\"); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (Character.isHighSurrogate(ch)) { + if (i + 1 >= length) { + throw new ForyJsonException("Unpaired high surrogate in string"); + } + char low = value.charAt(++i); + if (!Character.isLowSurrogate(low)) { + throw new ForyJsonException("Unpaired high surrogate in string"); + } + if (escapeNonLatin1) { + appendUnicodeEscape(builder, ch); + appendUnicodeEscape(builder, low); + } else { + builder.append(ch); + builder.append(low); + } + } else if (Character.isLowSurrogate(ch)) { + throw new ForyJsonException("Unpaired low surrogate in string"); + } else if (ch < 0x20 || escapeNonLatin1 && ch > 0xff) { + appendUnicodeEscape(builder, ch); + } else { + builder.append(ch); + } + } + } + builder.append('"'); + } + + private static void appendUnicodeEscape(StringBuilder builder, char ch) { + builder.append("\\u"); + builder.append(hex((ch >>> 12) & 0xF)); + builder.append(hex((ch >>> 8) & 0xF)); + builder.append(hex((ch >>> 4) & 0xF)); + builder.append(hex(ch & 0xF)); + } + + private static char hex(int value) { + return (char) (value < 10 ? '0' + value : 'a' + value - 10); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonWriter.java new file mode 100644 index 0000000000..f849538978 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/JsonWriter.java @@ -0,0 +1,70 @@ +/* + * 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.writer; + +import org.apache.fory.json.meta.JsonFieldInfo; + +public abstract class JsonWriter { + private final boolean writeNullFields; + + JsonWriter(boolean writeNullFields) { + this.writeNullFields = writeNullFields; + } + + public final boolean writeNullFields() { + return writeNullFields; + } + + public abstract void writeNull(); + + public abstract void writeBoolean(boolean value); + + public abstract void writeInt(int value); + + public abstract void writeLong(long value); + + public abstract void writeFloat(float value); + + public abstract void writeDouble(double value); + + public abstract void writeNumber(String value); + + public abstract void writeChar(char value); + + public abstract void writeString(String value); + + public abstract void writeFieldName(String name); + + public abstract void writeFieldName(JsonFieldInfo field); + + public abstract void writeIntFieldName(int value); + + public abstract void writeLongFieldName(long value); + + public abstract void writeObjectStart(); + + public abstract void writeObjectEnd(); + + public abstract void writeArrayStart(); + + public abstract void writeArrayEnd(); + + public abstract void writeComma(int index); +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java new file mode 100644 index 0000000000..ebe10dcffc --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -0,0 +1,1151 @@ +/* + * 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.writer; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.memory.LittleEndian; +import org.apache.fory.memory.NativeByteOrder; +import org.apache.fory.serializer.StringSerializer; + +public final class StringJsonWriter extends JsonWriter { + private static final byte LATIN1 = 0; + private static final byte UTF16 = 1; + private static final int RETAINED_CAPACITY = 8192; + private static final byte[] MIN_INT_BYTES = "-2147483648".getBytes(StandardCharsets.ISO_8859_1); + private static final byte[] MIN_LONG_BYTES = + "-9223372036854775808".getBytes(StandardCharsets.ISO_8859_1); + private static final long HIGH_BITS = 0x8080808080808080L; + private static final int INT_HIGH_BITS = 0x80808080; + private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; + private static final int INT_ASCII_CONTROL_OFFSET = 0x60606060; + private static final long ONE_BYTES = 0x0101010101010101L; + private static final int INT_ONE_BYTES = 0x01010101; + private static final long QUOTE_BYTES_COMPLEMENT = ~0x2222222222222222L; + private static final int INT_QUOTE_BYTES_COMPLEMENT = ~0x22222222; + private static final long BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C5C5C5C5CL; + private static final int INT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C; + private static final byte[] DIGIT_HUNDREDS = new byte[1000]; + private static final byte[] DIGIT_TENS = new byte[1000]; + private static final byte[] DIGIT_ONES = new byte[1000]; + private static final int[] DIGIT_TRIPLES = new int[1000]; + private static final int[] DIGIT_QUADS = new int[10000]; + private static final long UTF16_BYTE_MASK = 0x00FF00FF00FF00FFL; + private static final long UTF16_PAIR_MASK = 0x0000FFFF0000FFFFL; + private static final boolean STRING_BYTES_BACKED = StringSerializer.isBytesBackedString(); + private static final boolean LITTLE_ENDIAN = NativeByteOrder.IS_LITTLE_ENDIAN; + + static { + for (int i = 0; i < 1000; i++) { + int c0 = '0' + i / 100; + int c1 = '0' + (i / 10) % 10; + int c2 = '0' + i % 10; + DIGIT_HUNDREDS[i] = (byte) c0; + DIGIT_TENS[i] = (byte) c1; + DIGIT_ONES[i] = (byte) c2; + int skip = i < 10 ? 2 : i < 100 ? 1 : 0; + DIGIT_TRIPLES[i] = skip | (c0 << 8) | (c1 << 16) | (c2 << 24); + } + for (int i = 0; i < 10000; i++) { + int high = i / 100; + int low = i - high * 100; + int c0 = '0' + high / 10; + int c1 = '0' + high % 10; + int c2 = '0' + low / 10; + int c3 = '0' + low % 10; + DIGIT_QUADS[i] = c0 | (c1 << 8) | (c2 << 16) | (c3 << 24); + } + } + + private byte[] buffer; + private byte[] scratch; + private byte coder; + private int position; + + public StringJsonWriter(boolean writeNullFields) { + this(writeNullFields, new byte[512]); + } + + public StringJsonWriter(boolean writeNullFields, byte[] buffer) { + super(writeNullFields); + this.buffer = buffer; + scratch = new byte[buffer.length]; + } + + public void reset() { + if (buffer.length > RETAINED_CAPACITY) { + buffer = new byte[RETAINED_CAPACITY]; + } + if (scratch.length > RETAINED_CAPACITY) { + scratch = new byte[RETAINED_CAPACITY]; + } + coder = LATIN1; + position = 0; + } + + public String toJson() { + // The returned String may zero-copy this exact array, so pooled writer storage is never + // exposed. + byte[] bytes = Arrays.copyOf(buffer, position); + return StringSerializer.newBytesStringZeroCopy(coder, bytes); + } + + @Override + public void writeNull() { + writeAscii("null"); + } + + @Override + public void writeBoolean(boolean value) { + writeAscii(value ? "true" : "false"); + } + + @Override + public void writeInt(int value) { + if (coder == LATIN1) { + ensure(11); + writeIntLatin1NoEnsure(value); + return; + } + ensure(22); + writeIntUtf16NoEnsure(value); + } + + @Override + public void writeLong(long value) { + if (coder == LATIN1) { + writeLongLatin1(value); + return; + } + writeLongUtf16(value); + } + + private void writeLongLatin1(long value) { + if (value == Long.MIN_VALUE) { + writeRaw(MIN_LONG_BYTES); + return; + } + ensure(20); + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + return; + } + int start = position; + do { + buffer[position++] = (byte) ('0' + value % 10); + value /= 10; + } while (value != 0); + reverse(start, position - 1); + } + + @Override + public void writeFloat(float value) { + if (!Float.isFinite(value)) { + throw new ForyJsonException("JSON does not support non-finite float " + value); + } + writeAscii(Float.toString(value)); + } + + @Override + public void writeDouble(double value) { + if (!Double.isFinite(value)) { + throw new ForyJsonException("JSON does not support non-finite double " + value); + } + writeAscii(Double.toString(value)); + } + + @Override + public void writeNumber(String value) { + writeAscii(value); + } + + @Override + public void writeChar(char value) { + if (Character.isSurrogate(value)) { + throw new ForyJsonException("JSON char cannot be a surrogate: " + Integer.toHexString(value)); + } + writeByteRaw((byte) '"'); + writeEscapedChar(value); + writeByteRaw((byte) '"'); + } + + @Override + public void writeString(String value) { + if (coder == LATIN1) { + writeStringLatin1(value); + return; + } + writeStringUtf16(value); + } + + private void writeStringLatin1(String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + if (StringSerializer.isLatin1Coder(stringCoder)) { + ensure(bytes.length + 2); + writeLatin1StringNoEnsure(bytes); + return; + } + } + ensure(value.length() * 6 + 2); + writeStringCharsNoEnsure(value); + } + + @Override + public void writeFieldName(String name) { + writeString(name); + writeByteRaw((byte) ':'); + } + + @Override + public void writeFieldName(JsonFieldInfo field) { + writeRaw(field.stringNamePrefix()); + } + + public void writeFieldName(JsonFieldInfo field, int index) { + writeRaw(index == 0 ? field.stringNamePrefix() : field.stringCommaNamePrefix()); + } + + @Override + public void writeIntFieldName(int value) { + writeByteRaw((byte) '"'); + writeInt(value); + writeByteRaw((byte) '"'); + writeByteRaw((byte) ':'); + } + + @Override + public void writeLongFieldName(long value) { + writeByteRaw((byte) '"'); + writeLong(value); + writeByteRaw((byte) '"'); + writeByteRaw((byte) ':'); + } + + public void writeBooleanField( + byte[] namePrefix, byte[] commaNamePrefix, int index, boolean value) { + byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; + if (coder == UTF16) { + writeBooleanFieldUtf16(prefix, value); + return; + } + ensure(prefix.length + 5); + writeRawLatin1NoEnsure(prefix); + writeAsciiLatin1NoEnsure(value ? "true" : "false"); + } + + private void writeBooleanFieldUtf16(byte[] prefix, boolean value) { + ensure((prefix.length + 5) << 1); + writeRawUtf16NoEnsure(prefix); + writeAsciiUtf16NoEnsure(value ? "true" : "false", value ? 4 : 5); + } + + public void writeIntField(byte[] namePrefix, byte[] commaNamePrefix, int index, int value) { + byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; + writeIntField(prefix, value); + } + + public void writeIntField(byte[] prefix, int value) { + if (coder == LATIN1) { + writeIntFieldLatin1(prefix, value); + return; + } + writeIntFieldUtf16(prefix, value); + } + + private void writeIntFieldLatin1(byte[] prefix, int value) { + ensure(prefix.length + 11); + writeRawLatin1NoEnsure(prefix); + writeIntNoEnsure(value); + } + + private void writeIntFieldUtf16(byte[] prefix, int value) { + ensure((prefix.length << 1) + 22); + writeRawUtf16NoEnsure(prefix); + writeIntUtf16NoEnsure(value); + } + + public void writeObjectIntField(byte[] namePrefix, int value) { + if (coder == LATIN1) { + writeObjectIntFieldLatin1(namePrefix, value); + return; + } + writeObjectIntFieldUtf16(namePrefix, value); + } + + private void writeObjectIntFieldLatin1(byte[] namePrefix, int value) { + ensure(namePrefix.length + 12); + buffer[position++] = (byte) '{'; + writeRawLatin1NoEnsure(namePrefix); + writeIntNoEnsure(value); + } + + private void writeObjectIntFieldUtf16(byte[] namePrefix, int value) { + ensure(((namePrefix.length + 1) << 1) + 22); + writeUtf16ByteNoEnsure((byte) '{'); + writeRawUtf16NoEnsure(namePrefix); + writeIntUtf16NoEnsure(value); + } + + public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, long value) { + byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; + writeLongField(prefix, value); + } + + public void writeLongField(byte[] prefix, long value) { + if (coder == LATIN1) { + writeLongFieldLatin1(prefix, value); + return; + } + writeLongFieldUtf16(prefix, value); + } + + private void writeLongFieldLatin1(byte[] prefix, long value) { + ensure(prefix.length + 20); + writeRawLatin1NoEnsure(prefix); + writeLongNoEnsure(value); + } + + private void writeLongFieldUtf16(byte[] prefix, long value) { + ensure((prefix.length << 1) + 40); + writeRawUtf16NoEnsure(prefix); + writeLongUtf16NoEnsure(value); + } + + public void writeObjectLongField(byte[] namePrefix, long value) { + if (coder == LATIN1) { + writeObjectLongFieldLatin1(namePrefix, value); + return; + } + writeObjectLongFieldUtf16(namePrefix, value); + } + + private void writeObjectLongFieldLatin1(byte[] namePrefix, long value) { + ensure(namePrefix.length + 21); + buffer[position++] = (byte) '{'; + writeRawLatin1NoEnsure(namePrefix); + writeLongNoEnsure(value); + } + + private void writeObjectLongFieldUtf16(byte[] namePrefix, long value) { + ensure(((namePrefix.length + 1) << 1) + 40); + writeUtf16ByteNoEnsure((byte) '{'); + writeRawUtf16NoEnsure(namePrefix); + writeLongUtf16NoEnsure(value); + } + + public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { + byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; + writeStringField(prefix, value); + } + + public void writeStringField(byte[] prefix, String value) { + if (coder == LATIN1) { + writeStringFieldLatin1(prefix, value); + return; + } + writeStringFieldUtf16(prefix, value); + } + + private void writeStringFieldLatin1(byte[] prefix, String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + if (StringSerializer.isLatin1Coder(stringCoder)) { + ensure(prefix.length + bytes.length + 2); + writeRawLatin1NoEnsure(prefix); + writeLatin1StringNoEnsure(bytes); + return; + } + } + ensure(prefix.length + value.length() * 6 + 2); + writeRawLatin1NoEnsure(prefix); + writeStringCharsNoEnsure(value); + } + + private void writeStringFieldUtf16(byte[] prefix, String value) { + ensure(prefix.length << 1); + writeRawUtf16NoEnsure(prefix); + writeString(value); + } + + public void writeStringElement(int index, String value) { + int comma = index == 0 ? 0 : 1; + if (value == null) { + writeNullStringElement(comma); + return; + } + if (coder == LATIN1) { + writeStringElementLatin1(comma, value); + return; + } + writeStringElementUtf16(comma, value); + } + + private void writeNullStringElement(int comma) { + if (coder == UTF16) { + ensure((comma + 4) << 1); + if (comma != 0) { + writeUtf16ByteNoEnsure((byte) ','); + } + writeAsciiUtf16NoEnsure("null", 4); + return; + } + ensure(comma + 4); + if (comma != 0) { + buffer[position++] = ','; + } + writeAsciiLatin1NoEnsure("null"); + } + + private void writeStringElementLatin1(int comma, String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + if (StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(value))) { + ensure(comma + bytes.length + 2); + if (comma != 0) { + buffer[position++] = ','; + } + writeLatin1StringNoEnsure(bytes); + return; + } + } + ensure(comma + value.length() * 6 + 2); + if (comma != 0) { + buffer[position++] = ','; + } + writeStringNoEnsure(value); + } + + private void writeStringElementUtf16(int comma, String value) { + ensure(comma << 1); + if (comma != 0) { + writeUtf16ByteNoEnsure((byte) ','); + } + writeStringUtf16(value); + } + + public void writeRawValue(byte[] value) { + writeRaw(value); + } + + @Override + public void writeObjectStart() { + writeByteRaw((byte) '{'); + } + + @Override + public void writeObjectEnd() { + writeByteRaw((byte) '}'); + } + + @Override + public void writeArrayStart() { + writeByteRaw((byte) '['); + } + + @Override + public void writeArrayEnd() { + writeByteRaw((byte) ']'); + } + + @Override + public void writeComma(int index) { + if (index != 0) { + writeByteRaw((byte) ','); + } + } + + private void writeStringNoEnsure(String value) { + if (coder == LATIN1) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + if (StringSerializer.isLatin1Coder(stringCoder)) { + writeLatin1StringNoEnsure(bytes); + return; + } + } + writeStringCharsNoEnsure(value); + return; + } + writeStringUtf16(value); + } + + private void writeStringCharsNoEnsure(String value) { + int length = value.length(); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + int i = 0; + while (i + 4 <= length) { + char c0 = value.charAt(i); + char c1 = value.charAt(i + 1); + char c2 = value.charAt(i + 2); + char c3 = value.charAt(i + 3); + if (isJsonLatin1(c0) && isJsonLatin1(c1) && isJsonLatin1(c2) && isJsonLatin1(c3)) { + bytes[pos] = (byte) c0; + bytes[pos + 1] = (byte) c1; + bytes[pos + 2] = (byte) c2; + bytes[pos + 3] = (byte) c3; + pos += 4; + i += 4; + } else { + break; + } + } + while (i < length) { + char ch = value.charAt(i); + if (isJsonLatin1(ch)) { + bytes[pos++] = (byte) ch; + i++; + } else { + position = pos; + writeStringSlow(value, i, length); + return; + } + } + bytes[pos++] = (byte) '"'; + position = pos; + } + + private void writeLatin1StringNoEnsure(byte[] value) { + int length = value.length; + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + int i = 0; + int upperBound = length & ~15; + for (; i < upperBound; i += 16) { + long word0 = LittleEndian.getInt64(value, i); + long word1 = LittleEndian.getInt64(value, i + 8); + if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { + break; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + pos += 16; + } + upperBound = length & ~7; + for (; i < upperBound; i += 8) { + long word = LittleEndian.getInt64(value, i); + if (!isJsonAsciiWord(word)) { + break; + } + LittleEndian.putInt64(bytes, pos, word); + pos += 8; + } + if (i + 4 <= length) { + int word = LittleEndian.getInt32(value, i); + if (isJsonAsciiInt(word)) { + LittleEndian.putInt32(bytes, pos, word); + pos += 4; + i += 4; + } + } + while (i < length) { + byte ch = value[i]; + if (isJsonLatin1Byte(ch)) { + bytes[pos++] = ch; + i++; + } else { + position = pos; + writeLatin1StringSlow(value, i, length); + return; + } + } + bytes[pos++] = (byte) '"'; + position = pos; + } + + private void writeStringSlow(String value, int index, int length) { + for (int i = index; i < length; i++) { + char ch = value.charAt(i); + if (Character.isHighSurrogate(ch)) { + if (i + 1 >= length) { + throw new ForyJsonException("Unpaired high surrogate in string"); + } + char low = value.charAt(++i); + if (!Character.isLowSurrogate(low)) { + throw new ForyJsonException("Unpaired high surrogate in string"); + } + writeCharRaw(ch); + writeCharRaw(low); + } else if (Character.isLowSurrogate(ch)) { + throw new ForyJsonException("Unpaired low surrogate in string"); + } else { + writeEscapedChar(ch); + } + } + writeByteRaw((byte) '"'); + } + + private void writeLatin1StringSlow(byte[] value, int index, int length) { + for (int i = index; i < length; i++) { + writeEscapedChar((char) (value[i] & 0xff)); + } + writeByteRaw((byte) '"'); + } + + private void writeEscapedChar(char ch) { + switch (ch) { + case '"': + writeAscii("\\\""); + return; + case '\\': + writeAscii("\\\\"); + return; + case '\b': + writeAscii("\\b"); + return; + case '\f': + writeAscii("\\f"); + return; + case '\n': + writeAscii("\\n"); + return; + case '\r': + writeAscii("\\r"); + return; + case '\t': + writeAscii("\\t"); + return; + default: + if (ch < 0x20) { + writeUnicodeEscape(ch); + } else { + writeCharRaw(ch); + } + } + } + + private void writeStringUtf16(String value) { + int length = value.length(); + ensure((length + 2) << 1); + writeUtf16ByteNoEnsure((byte) '"'); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (isJsonUtf16(ch)) { + writeUtf16CharNoEnsure(ch); + } else { + writeStringUtf16Slow(value, i, length); + return; + } + } + writeByteRaw((byte) '"'); + } + + private void writeStringUtf16Slow(String value, int index, int length) { + for (int i = index; i < length; i++) { + char ch = value.charAt(i); + if (Character.isHighSurrogate(ch)) { + if (i + 1 >= length) { + throw new ForyJsonException("Unpaired high surrogate in string"); + } + char low = value.charAt(++i); + if (!Character.isLowSurrogate(low)) { + throw new ForyJsonException("Unpaired high surrogate in string"); + } + ensure(4); + writeUtf16CharNoEnsure(ch); + writeUtf16CharNoEnsure(low); + } else if (Character.isLowSurrogate(ch)) { + throw new ForyJsonException("Unpaired low surrogate in string"); + } else { + writeEscapedChar(ch); + } + } + writeByteRaw((byte) '"'); + } + + private void writeUnicodeEscape(char ch) { + if (coder == UTF16) { + ensure(12); + writeUtf16ByteNoEnsure((byte) '\\'); + writeUtf16ByteNoEnsure((byte) 'u'); + writeUtf16CharNoEnsure(hex((ch >>> 12) & 0xF)); + writeUtf16CharNoEnsure(hex((ch >>> 8) & 0xF)); + writeUtf16CharNoEnsure(hex((ch >>> 4) & 0xF)); + writeUtf16CharNoEnsure(hex(ch & 0xF)); + } else { + ensure(6); + buffer[position++] = '\\'; + buffer[position++] = 'u'; + buffer[position++] = (byte) hex((ch >>> 12) & 0xF); + buffer[position++] = (byte) hex((ch >>> 8) & 0xF); + buffer[position++] = (byte) hex((ch >>> 4) & 0xF); + buffer[position++] = (byte) hex(ch & 0xF); + } + } + + private void writeAscii(String value) { + int length = value.length(); + if (coder == LATIN1) { + ensure(length); + writeAsciiNoEnsure(value); + return; + } + writeAsciiUtf16(value, length); + } + + private void writeAsciiNoEnsure(String value) { + int length = value.length(); + if (coder == LATIN1) { + for (int i = 0; i < length; i++) { + buffer[position++] = (byte) value.charAt(i); + } + return; + } + writeAsciiUtf16NoEnsure(value, length); + } + + private void writeAsciiUtf16(String value, int length) { + ensure(length << 1); + writeAsciiUtf16NoEnsure(value, length); + } + + private void writeAsciiUtf16NoEnsure(String value, int length) { + byte[] bytes = buffer; + int pos = position; + for (int i = 0; i < length; i++) { + pos = putUtf16Char(bytes, pos, value.charAt(i)); + } + position = pos; + } + + private void writeAsciiLatin1NoEnsure(String value) { + int length = value.length(); + for (int i = 0; i < length; i++) { + buffer[position++] = (byte) value.charAt(i); + } + } + + private void writeRaw(byte[] bytes) { + if (coder == LATIN1) { + ensure(bytes.length); + writeRawNoEnsure(bytes); + return; + } + writeRawUtf16(bytes); + } + + private void writeRawNoEnsure(byte[] bytes) { + if (coder == LATIN1) { + System.arraycopy(bytes, 0, buffer, position, bytes.length); + position += bytes.length; + return; + } + writeRawUtf16NoEnsure(bytes); + } + + private void writeRawUtf16(byte[] bytes) { + ensure(bytes.length << 1); + writeRawUtf16NoEnsure(bytes); + } + + private void writeRawUtf16NoEnsure(byte[] bytes) { + byte[] target = buffer; + int pos = position; + for (byte value : bytes) { + pos = putUtf16Char(target, pos, (char) (value & 0xff)); + } + position = pos; + } + + private void writeRawLatin1NoEnsure(byte[] bytes) { + System.arraycopy(bytes, 0, buffer, position, bytes.length); + position += bytes.length; + } + + private void writeByteRaw(byte value) { + if (coder == LATIN1) { + ensure(1); + buffer[position++] = value; + return; + } + writeByteRawUtf16(value); + } + + private void writeByteRawUtf16(byte value) { + ensure(2); + writeUtf16ByteNoEnsure(value); + } + + private void writeCharRaw(char value) { + if (coder == LATIN1 && value <= 0xff) { + ensure(1); + buffer[position++] = (byte) value; + return; + } + writeCharRawUtf16(value); + } + + private void writeCharRawUtf16(char value) { + if (coder == LATIN1) { + upgradeToUtf16((position << 1) + 2); + } else { + ensure(2); + } + writeUtf16CharNoEnsure(value); + } + + private void reverse(int start, int end) { + while (start < end) { + byte tmp = buffer[start]; + buffer[start++] = buffer[end]; + buffer[end--] = tmp; + } + } + + private void reverseUtf16Chars(int start, int end) { + while (start < end) { + byte b0 = buffer[start]; + byte b1 = buffer[start + 1]; + buffer[start] = buffer[end]; + buffer[start + 1] = buffer[end + 1]; + buffer[end] = b0; + buffer[end + 1] = b1; + start += 2; + end -= 2; + } + } + + private void writeUtf16ByteNoEnsure(byte value) { + position = putUtf16Char(buffer, position, (char) (value & 0xff)); + } + + private void writeUtf16CharNoEnsure(char value) { + position = putUtf16Char(buffer, position, value); + } + + private static int putUtf16Char(byte[] bytes, int pos, char value) { + if (LITTLE_ENDIAN) { + bytes[pos] = (byte) value; + bytes[pos + 1] = (byte) (value >>> 8); + } else { + bytes[pos] = (byte) (value >>> 8); + bytes[pos + 1] = (byte) value; + } + return pos + 2; + } + + private void ensure(int additional) { + int minCapacity = position + additional; + if (minCapacity > buffer.length) { + grow(minCapacity); + } + } + + private void grow(int minCapacity) { + buffer = Arrays.copyOf(buffer, growCapacity(buffer.length, minCapacity)); + } + + private void upgradeToUtf16(int minCapacity) { + int oldPosition = position; + int newPosition = oldPosition << 1; + int required = Math.max(minCapacity, newPosition); + byte[] source = buffer; + byte[] target = scratch; + int minTargetCapacity = Math.max(source.length, required); + if (target.length < minTargetCapacity) { + target = growScratch(minTargetCapacity); + } + if (LITTLE_ENDIAN) { + widenLatin1ToUtf16LE(source, target, oldPosition); + } else { + widenLatin1ToUtf16BE(source, target, oldPosition); + } + scratch = source; + buffer = target; + coder = UTF16; + position = newPosition; + } + + private byte[] growScratch(int minCapacity) { + return new byte[growCapacity(buffer.length, minCapacity)]; + } + + private static int growCapacity(int capacity, int minCapacity) { + int expanded = capacity + Math.max(capacity, 1); + return expanded >= minCapacity && expanded > 0 ? expanded : minCapacity; + } + + private static void widenLatin1ToUtf16LE(byte[] source, byte[] target, int length) { + // JDK21 AArch64 C2 does not SuperWord-vectorize the plain byte-stride widening loop; hsdis + // shows scalar ldrsb/strb. Keep this explicit 8-byte widening path so the hot upgrade uses + // wide loads/stores without direct Unsafe in fory-json. + int i = 0; + int j = 0; + int bulkEnd = length & ~7; + for (; i < bulkEnd; i += 8, j += 16) { + long word = LittleEndian.getInt64(source, i); + LittleEndian.putInt64(target, j, spreadLatin1ToUtf16(word & 0xFFFFFFFFL)); + LittleEndian.putInt64(target, j + 8, spreadLatin1ToUtf16(word >>> 32)); + } + for (; i < length; i++, j += 2) { + target[j] = source[i]; + target[j + 1] = 0; + } + } + + private static void widenLatin1ToUtf16BE(byte[] source, byte[] target, int length) { + int i = 0; + int j = 0; + int bulkEnd = length & ~7; + for (; i < bulkEnd; i += 8, j += 16) { + long word = LittleEndian.getInt64(source, i); + LittleEndian.putInt64(target, j, spreadLatin1ToUtf16(word & 0xFFFFFFFFL) << 8); + LittleEndian.putInt64(target, j + 8, spreadLatin1ToUtf16(word >>> 32) << 8); + } + for (; i < length; i++, j += 2) { + target[j] = 0; + target[j + 1] = source[i]; + } + } + + private static long spreadLatin1ToUtf16(long value) { + value = (value | (value << 16)) & UTF16_PAIR_MASK; + return (value | (value << 8)) & UTF16_BYTE_MASK; + } + + private static boolean isJsonLatin1(char ch) { + return ch > 0x1F && ch <= 0xff && ch != '"' && ch != '\\'; + } + + private static boolean isJsonUtf16(char ch) { + return ch > 0x1F && ch != '"' && ch != '\\' && !Character.isSurrogate(ch); + } + + private static boolean isJsonLatin1Byte(byte value) { + int ch = value & 0xff; + return ch > 0x1F && ch != '"' && ch != '\\'; + } + + private static boolean isJsonAsciiWord(long word) { + return (((word + ASCII_CONTROL_OFFSET) & ~word) & HIGH_BITS) == HIGH_BITS + && (((word ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS + && (((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS; + } + + private static boolean isJsonAsciiInt(int word) { + return (((word + INT_ASCII_CONTROL_OFFSET) & ~word) & INT_HIGH_BITS) == INT_HIGH_BITS + && (((word ^ INT_QUOTE_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) == INT_HIGH_BITS + && (((word ^ INT_BACKSLASH_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) + == INT_HIGH_BITS; + } + + private void writeIntNoEnsure(int value) { + if (coder == LATIN1) { + writeIntLatin1NoEnsure(value); + return; + } + writeIntUtf16NoEnsure(value); + } + + private void writeIntLatin1NoEnsure(int value) { + if (value == Integer.MIN_VALUE) { + writeRawLatin1NoEnsure(MIN_INT_BYTES); + return; + } + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + writePositiveIntNoEnsure(value); + } + + private void writeLongNoEnsure(long value) { + if (coder == LATIN1) { + writeLongLatin1NoEnsure(value); + return; + } + writeLongUtf16NoEnsure(value); + } + + private void writeLongLatin1NoEnsure(long value) { + if (value == Long.MIN_VALUE) { + writeRawLatin1NoEnsure(MIN_LONG_BYTES); + return; + } + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + return; + } + int start = position; + do { + buffer[position++] = (byte) ('0' + value % 10); + value /= 10; + } while (value != 0); + reverse(start, position - 1); + } + + private void writeLongUtf16(long value) { + ensure(40); + writeLongUtf16NoEnsure(value); + } + + private void writeIntUtf16NoEnsure(int value) { + if (value == Integer.MIN_VALUE) { + writeRawUtf16NoEnsure(MIN_INT_BYTES); + return; + } + if (value < 0) { + writeUtf16ByteNoEnsure((byte) '-'); + value = -value; + } + writePositiveIntUtf16NoEnsure(value); + } + + private void writeLongUtf16NoEnsure(long value) { + if (value == Long.MIN_VALUE) { + writeRawUtf16NoEnsure(MIN_LONG_BYTES); + return; + } + if (value < 0) { + writeUtf16ByteNoEnsure((byte) '-'); + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntUtf16NoEnsure((int) value); + return; + } + int start = position; + do { + writeUtf16CharNoEnsure((char) ('0' + value % 10)); + value /= 10; + } while (value != 0); + reverseUtf16Chars(start, position - 2); + } + + private void writePositiveIntNoEnsure(int value) { + byte[] bytes = buffer; + int pos = position; + if (value < 10000) { + position = writeIntUpTo4(bytes, pos, value); + return; + } + int high = divide10000(value); + int low = value - high * 10000; + if (high < 10000) { + if (high >= 1000) { + position = writePadded8(bytes, pos, high, low); + return; + } + pos = writeIntUpTo4(bytes, pos, high); + position = writePadded4(bytes, pos, low); + return; + } + int top = divide10000(high); + int middle = high - top * 10000; + pos = writeIntUpTo4(bytes, pos, top); + position = writePadded8(bytes, pos, middle, low); + } + + private void writePositiveIntUtf16NoEnsure(int value) { + if (value < 10000) { + writeIntUpTo4Utf16(value); + return; + } + int high = divide10000(value); + int low = value - high * 10000; + if (high < 10000) { + writeIntUpTo4Utf16(high); + writePadded4Utf16(low); + return; + } + int top = divide10000(high); + int middle = high - top * 10000; + writeIntUpTo4Utf16(top); + writePadded4Utf16(middle); + writePadded4Utf16(low); + } + + private static int divide10000(int value) { + return (int) (((long) value * 1759218605L) >> 44); + } + + private static int writeIntUpTo4(byte[] bytes, int pos, int value) { + if (value < 1000) { + return writeIntUpTo3(bytes, pos, value); + } + return writePadded4(bytes, pos, value); + } + + private static int writeIntUpTo3(byte[] bytes, int pos, int value) { + int digits = DIGIT_TRIPLES[value]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + return pos + 3 - skip; + } + + private static int writePadded4(byte[] bytes, int pos, int value) { + LittleEndian.putInt32(bytes, pos, DIGIT_QUADS[value]); + return pos + 4; + } + + private static int writePadded8(byte[] bytes, int pos, int high, int low) { + long value = (DIGIT_QUADS[high] & 0xFFFFFFFFL) | ((DIGIT_QUADS[low] & 0xFFFFFFFFL) << 32); + LittleEndian.putInt64(bytes, pos, value); + return pos + 8; + } + + private void writeIntUpTo4Utf16(int value) { + if (value < 10) { + writeUtf16ByteNoEnsure(DIGIT_ONES[value]); + } else if (value < 100) { + writeUtf16ByteNoEnsure(DIGIT_TENS[value]); + writeUtf16ByteNoEnsure(DIGIT_ONES[value]); + } else if (value < 1000) { + writePadded3Utf16(value); + } else { + writePadded4Utf16(value); + } + } + + private void writePadded3Utf16(int value) { + writeUtf16ByteNoEnsure(DIGIT_HUNDREDS[value]); + writeUtf16ByteNoEnsure(DIGIT_TENS[value]); + writeUtf16ByteNoEnsure(DIGIT_ONES[value]); + } + + private void writePadded4Utf16(int value) { + int high = value / 100; + int low = value - high * 100; + writeUtf16ByteNoEnsure((byte) ('0' + high / 10)); + writeUtf16ByteNoEnsure((byte) ('0' + high % 10)); + writeUtf16ByteNoEnsure((byte) ('0' + low / 10)); + writeUtf16ByteNoEnsure((byte) ('0' + low % 10)); + } + + private static char hex(int value) { + return (char) (value < 10 ? '0' + value : 'a' + value - 10); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringObjectWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringObjectWriter.java new file mode 100644 index 0000000000..ccee0cf237 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringObjectWriter.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.json.writer; + +import org.apache.fory.json.resolver.JsonTypeResolver; + +public interface StringObjectWriter { + void writeString(StringJsonWriter writer, Object value, JsonTypeResolver typeResolver); +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java new file mode 100644 index 0000000000..5ba1ec5e08 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -0,0 +1,817 @@ +/* + * 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.writer; + +import java.util.Arrays; +import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.memory.LittleEndian; +import org.apache.fory.serializer.StringSerializer; + +public final class Utf8JsonWriter extends JsonWriter { + private static final int RETAINED_CAPACITY = 8192; + private static final byte[] MIN_INT_BYTES = + "-2147483648".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); + private static final byte[] MIN_LONG_BYTES = + "-9223372036854775808".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); + private static final long HIGH_BITS = 0x8080808080808080L; + private static final int INT_HIGH_BITS = 0x80808080; + private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; + private static final int INT_ASCII_CONTROL_OFFSET = 0x60606060; + private static final long ONE_BYTES = 0x0101010101010101L; + private static final int INT_ONE_BYTES = 0x01010101; + private static final long QUOTE_BYTES_COMPLEMENT = ~0x2222222222222222L; + private static final int INT_QUOTE_BYTES_COMPLEMENT = ~0x22222222; + private static final long BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C5C5C5C5CL; + private static final int INT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C; + private static final long UTF16_ASCII_MASK = 0xFF80FF80FF80FF80L; + private static final int[] DIGIT_TRIPLES = new int[1000]; + private static final int[] DIGIT_QUADS = new int[10000]; + private static final boolean STRING_BYTES_BACKED = StringSerializer.isBytesBackedString(); + + static { + for (int i = 0; i < 1000; i++) { + int c0 = '0' + i / 100; + int c1 = '0' + (i / 10) % 10; + int c2 = '0' + i % 10; + int skip = i < 10 ? 2 : i < 100 ? 1 : 0; + DIGIT_TRIPLES[i] = skip | (c0 << 8) | (c1 << 16) | (c2 << 24); + } + for (int i = 0; i < 10000; i++) { + int high = i / 100; + int low = i - high * 100; + int c0 = '0' + high / 10; + int c1 = '0' + high % 10; + int c2 = '0' + low / 10; + int c3 = '0' + low % 10; + DIGIT_QUADS[i] = c0 | (c1 << 8) | (c2 << 16) | (c3 << 24); + } + } + + private byte[] buffer; + private int position; + + public Utf8JsonWriter(boolean writeNullFields) { + this(writeNullFields, new byte[512]); + } + + public Utf8JsonWriter(boolean writeNullFields, byte[] buffer) { + super(writeNullFields); + this.buffer = buffer; + } + + public void reset() { + if (buffer.length > RETAINED_CAPACITY) { + buffer = new byte[RETAINED_CAPACITY]; + } + position = 0; + } + + public byte[] toJsonBytes() { + return Arrays.copyOf(buffer, position); + } + + @Override + public void writeNull() { + writeAscii("null"); + } + + @Override + public void writeBoolean(boolean value) { + writeAscii(value ? "true" : "false"); + } + + @Override + public void writeInt(int value) { + ensure(11); + writeIntNoEnsure(value); + } + + @Override + public void writeLong(long value) { + if (value == Long.MIN_VALUE) { + writeRaw(MIN_LONG_BYTES); + return; + } + ensure(20); + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + return; + } + int start = position; + do { + buffer[position++] = (byte) ('0' + value % 10); + value /= 10; + } while (value != 0); + reverse(start, position - 1); + } + + @Override + public void writeFloat(float value) { + if (!Float.isFinite(value)) { + throw new ForyJsonException("JSON does not support non-finite float " + value); + } + writeAscii(Float.toString(value)); + } + + @Override + public void writeDouble(double value) { + if (!Double.isFinite(value)) { + throw new ForyJsonException("JSON does not support non-finite double " + value); + } + writeAscii(Double.toString(value)); + } + + @Override + public void writeNumber(String value) { + writeAscii(value); + } + + @Override + public void writeChar(char value) { + if (Character.isSurrogate(value)) { + throw new ForyJsonException("JSON char cannot be a surrogate: " + Integer.toHexString(value)); + } + writeByteRaw((byte) '"'); + writeEscapedChar(value); + writeByteRaw((byte) '"'); + } + + @Override + public void writeString(String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + if (StringSerializer.isLatin1Coder(stringCoder)) { + if (writeLatin1String(bytes)) { + return; + } + } else if (StringSerializer.isUtf16Coder(stringCoder)) { + if (writeUtf16String(bytes)) { + return; + } + } + } + writeStringChars(value); + } + + private void writeStringChars(String value) { + int length = value.length(); + ensure(length + 2); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + int i = 0; + while (i + 4 <= length) { + char c0 = value.charAt(i); + char c1 = value.charAt(i + 1); + char c2 = value.charAt(i + 2); + char c3 = value.charAt(i + 3); + if (isJsonAscii(c0) && isJsonAscii(c1) && isJsonAscii(c2) && isJsonAscii(c3)) { + bytes[pos] = (byte) c0; + bytes[pos + 1] = (byte) c1; + bytes[pos + 2] = (byte) c2; + bytes[pos + 3] = (byte) c3; + pos += 4; + i += 4; + } else { + break; + } + } + while (i < length) { + char ch = value.charAt(i); + if (isJsonAscii(ch)) { + bytes[pos++] = (byte) ch; + i++; + } else { + position = pos; + writeStringSlow(value, i, length); + return; + } + } + bytes[pos++] = (byte) '"'; + position = pos; + } + + @Override + public void writeFieldName(String name) { + writeString(name); + writeByteRaw((byte) ':'); + } + + @Override + public void writeFieldName(JsonFieldInfo field) { + writeRaw(field.utf8NamePrefix()); + } + + public void writeFieldName(JsonFieldInfo field, int index) { + writeRaw(index == 0 ? field.utf8NamePrefix() : field.utf8CommaNamePrefix()); + } + + @Override + public void writeIntFieldName(int value) { + writeByteRaw((byte) '"'); + writeInt(value); + writeByteRaw((byte) '"'); + writeByteRaw((byte) ':'); + } + + @Override + public void writeLongFieldName(long value) { + writeByteRaw((byte) '"'); + writeLong(value); + writeByteRaw((byte) '"'); + writeByteRaw((byte) ':'); + } + + public void writeBooleanField( + byte[] namePrefix, byte[] commaNamePrefix, int index, boolean value) { + byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; + ensure(prefix.length + 5); + writeRawNoEnsure(prefix); + writeAsciiNoEnsure(value ? "true" : "false"); + } + + public void writeIntField(byte[] namePrefix, byte[] commaNamePrefix, int index, int value) { + byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; + writeIntField(prefix, value); + } + + public void writeIntField(byte[] prefix, int value) { + ensure(prefix.length + 11); + writeRawNoEnsure(prefix); + writeIntNoEnsure(value); + } + + public void writeObjectIntField(byte[] namePrefix, int value) { + ensure(namePrefix.length + 12); + buffer[position++] = (byte) '{'; + writeRawNoEnsure(namePrefix); + writeIntNoEnsure(value); + } + + public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, long value) { + byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; + writeLongField(prefix, value); + } + + public void writeLongField(byte[] prefix, long value) { + ensure(prefix.length + 20); + writeRawNoEnsure(prefix); + writeLongNoEnsure(value); + } + + public void writeObjectLongField(byte[] namePrefix, long value) { + ensure(namePrefix.length + 21); + buffer[position++] = (byte) '{'; + writeRawNoEnsure(namePrefix); + writeLongNoEnsure(value); + } + + public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { + byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; + writeStringField(prefix, value); + } + + public void writeStringField(byte[] prefix, String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + int start = position; + if (StringSerializer.isLatin1Coder(stringCoder)) { + ensure(prefix.length + bytes.length + 2); + writeRawNoEnsure(prefix); + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + position = start; + } else if (StringSerializer.isUtf16Coder(stringCoder)) { + ensure(prefix.length + (bytes.length >> 1) * 3 + 2); + writeRawNoEnsure(prefix); + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + position = start; + } + } + writeStringFieldChars(prefix, value); + } + + private void writeStringFieldChars(byte[] prefix, String value) { + ensure(prefix.length + value.length() * 3 + 2); + writeRawNoEnsure(prefix); + writeStringNoEnsure(value); + } + + public void writeStringElement(int index, String value) { + int comma = index == 0 ? 0 : 1; + if (value == null) { + writeNullStringElement(comma); + return; + } + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + int start = position; + if (StringSerializer.isLatin1Coder(stringCoder)) { + ensure(comma + bytes.length + 2); + if (comma != 0) { + buffer[position++] = ','; + } + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + position = start; + } else if (StringSerializer.isUtf16Coder(stringCoder)) { + ensure(comma + (bytes.length >> 1) * 3 + 2); + if (comma != 0) { + buffer[position++] = ','; + } + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + position = start; + } + } + writeStringElementChars(comma, value); + } + + private void writeNullStringElement(int comma) { + ensure(comma + 4); + if (comma != 0) { + buffer[position++] = ','; + } + writeAsciiNoEnsure("null"); + } + + private void writeStringElementChars(int comma, String value) { + ensure(comma + value.length() * 3 + 2); + if (comma != 0) { + buffer[position++] = ','; + } + writeStringNoEnsure(value); + } + + public void writeRawValue(byte[] value) { + writeRaw(value); + } + + @Override + public void writeObjectStart() { + writeByteRaw((byte) '{'); + } + + @Override + public void writeObjectEnd() { + writeByteRaw((byte) '}'); + } + + @Override + public void writeArrayStart() { + writeByteRaw((byte) '['); + } + + @Override + public void writeArrayEnd() { + writeByteRaw((byte) ']'); + } + + @Override + public void writeComma(int index) { + if (index != 0) { + writeByteRaw((byte) ','); + } + } + + private boolean writeLatin1String(byte[] value) { + int length = value.length; + ensure(length + 2); + return writeLatin1StringNoEnsure(value); + } + + private boolean writeLatin1StringNoEnsure(byte[] value) { + int length = value.length; + byte[] bytes = buffer; + int start = position; + int pos = start; + bytes[pos++] = (byte) '"'; + int i = 0; + int upperBound = length & ~15; + for (; i < upperBound; i += 16) { + long word0 = LittleEndian.getInt64(value, i); + long word1 = LittleEndian.getInt64(value, i + 8); + if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { + break; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + pos += 16; + } + upperBound = length & ~7; + for (; i < upperBound; i += 8) { + long word = LittleEndian.getInt64(value, i); + if (!isJsonAsciiWord(word)) { + break; + } + LittleEndian.putInt64(bytes, pos, word); + pos += 8; + } + if (i + 4 <= length) { + int word = LittleEndian.getInt32(value, i); + if (isJsonAsciiInt(word)) { + LittleEndian.putInt32(bytes, pos, word); + pos += 4; + i += 4; + } + } + for (; i < length; i++) { + byte ch = value[i]; + if (isJsonAsciiByte(ch)) { + bytes[pos++] = ch; + } else { + position = start; + return false; + } + } + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + + private boolean writeUtf16String(byte[] value) { + int length = value.length; + ensure((length >> 1) * 3 + 2); + return writeUtf16StringNoEnsure(value); + } + + private boolean writeUtf16StringNoEnsure(byte[] value) { + int length = value.length; + byte[] bytes = buffer; + int start = position; + int pos = start; + bytes[pos++] = (byte) '"'; + int i = 0; + int upperBound = length & ~7; + for (; i < upperBound; i += 8) { + long word = LittleEndian.getInt64(value, i); + if ((word & UTF16_ASCII_MASK) != 0) { + break; + } + int packed = packUtf16Ascii(word); + if (!isJsonAsciiInt(packed)) { + break; + } + LittleEndian.putInt32(bytes, pos, packed); + pos += 4; + } + for (; i < length; i += 2) { + char ch = StringSerializer.getBytesChar(value, i); + if (ch < 0x80) { + if (!isJsonAscii(ch)) { + position = start; + return false; + } + bytes[pos++] = (byte) ch; + } else if (ch < 0x800) { + bytes[pos++] = (byte) (0xC0 | (ch >>> 6)); + bytes[pos++] = (byte) (0x80 | (ch & 0x3F)); + } else if (Character.isSurrogate(ch)) { + position = start; + return false; + } else { + bytes[pos++] = (byte) (0xE0 | (ch >>> 12)); + bytes[pos++] = (byte) (0x80 | ((ch >>> 6) & 0x3F)); + bytes[pos++] = (byte) (0x80 | (ch & 0x3F)); + } + } + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + + private void writeEscapedChar(char ch) { + switch (ch) { + case '"': + writeAscii("\\\""); + return; + case '\\': + writeAscii("\\\\"); + return; + case '\b': + writeAscii("\\b"); + return; + case '\f': + writeAscii("\\f"); + return; + case '\n': + writeAscii("\\n"); + return; + case '\r': + writeAscii("\\r"); + return; + case '\t': + writeAscii("\\t"); + return; + default: + if (ch < 0x20) { + writeUnicodeEscape(ch); + } else if (ch < 0x80) { + writeByteRaw((byte) ch); + } else if (ch < 0x800) { + ensure(2); + buffer[position++] = (byte) (0xC0 | (ch >>> 6)); + buffer[position++] = (byte) (0x80 | (ch & 0x3F)); + } else { + ensure(3); + buffer[position++] = (byte) (0xE0 | (ch >>> 12)); + buffer[position++] = (byte) (0x80 | ((ch >>> 6) & 0x3F)); + buffer[position++] = (byte) (0x80 | (ch & 0x3F)); + } + } + } + + private void writeStringSlow(String value, int index, int length) { + for (int i = index; i < length; i++) { + char ch = value.charAt(i); + if (Character.isHighSurrogate(ch)) { + if (i + 1 >= length) { + throw new ForyJsonException("Unpaired high surrogate in string"); + } + char low = value.charAt(++i); + if (!Character.isLowSurrogate(low)) { + throw new ForyJsonException("Unpaired high surrogate in string"); + } + writeCodePoint(Character.toCodePoint(ch, low)); + } else if (Character.isLowSurrogate(ch)) { + throw new ForyJsonException("Unpaired low surrogate in string"); + } else { + writeEscapedChar(ch); + } + } + writeByteRaw((byte) '"'); + } + + private void writeStringNoEnsure(String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + if (StringSerializer.isLatin1Coder(stringCoder)) { + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + } else if (StringSerializer.isUtf16Coder(stringCoder)) { + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + } + } + writeStringCharsNoEnsure(value); + } + + private void writeStringCharsNoEnsure(String value) { + int length = value.length(); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + int i = 0; + while (i + 4 <= length) { + char c0 = value.charAt(i); + char c1 = value.charAt(i + 1); + char c2 = value.charAt(i + 2); + char c3 = value.charAt(i + 3); + if (isJsonAscii(c0) && isJsonAscii(c1) && isJsonAscii(c2) && isJsonAscii(c3)) { + bytes[pos] = (byte) c0; + bytes[pos + 1] = (byte) c1; + bytes[pos + 2] = (byte) c2; + bytes[pos + 3] = (byte) c3; + pos += 4; + i += 4; + } else { + break; + } + } + while (i < length) { + char ch = value.charAt(i); + if (isJsonAscii(ch)) { + bytes[pos++] = (byte) ch; + i++; + } else { + position = pos; + writeStringSlow(value, i, length); + return; + } + } + bytes[pos++] = (byte) '"'; + position = pos; + } + + private void writeCodePoint(int codePoint) { + ensure(4); + buffer[position++] = (byte) (0xF0 | (codePoint >>> 18)); + buffer[position++] = (byte) (0x80 | ((codePoint >>> 12) & 0x3F)); + buffer[position++] = (byte) (0x80 | ((codePoint >>> 6) & 0x3F)); + buffer[position++] = (byte) (0x80 | (codePoint & 0x3F)); + } + + private void writeUnicodeEscape(char ch) { + ensure(6); + buffer[position++] = '\\'; + buffer[position++] = 'u'; + buffer[position++] = '0'; + buffer[position++] = '0'; + buffer[position++] = (byte) hex((ch >>> 4) & 0xF); + buffer[position++] = (byte) hex(ch & 0xF); + } + + private void writeAscii(String value) { + int length = value.length(); + ensure(length); + writeAsciiNoEnsure(value); + } + + private void writeAsciiNoEnsure(String value) { + int length = value.length(); + for (int i = 0; i < length; i++) { + buffer[position++] = (byte) value.charAt(i); + } + } + + private void writeRaw(byte[] bytes) { + ensure(bytes.length); + writeRawNoEnsure(bytes); + } + + private void writeRawNoEnsure(byte[] bytes) { + System.arraycopy(bytes, 0, buffer, position, bytes.length); + position += bytes.length; + } + + private void writeByteRaw(byte value) { + ensure(1); + buffer[position++] = value; + } + + private void reverse(int start, int end) { + while (start < end) { + byte tmp = buffer[start]; + buffer[start++] = buffer[end]; + buffer[end--] = tmp; + } + } + + private void ensure(int additional) { + int minCapacity = position + additional; + if (minCapacity > buffer.length) { + grow(minCapacity); + } + } + + private void grow(int minCapacity) { + int newCapacity = buffer.length << 1; + while (newCapacity < minCapacity) { + newCapacity <<= 1; + } + buffer = Arrays.copyOf(buffer, newCapacity); + } + + private static char hex(int value) { + return (char) (value < 10 ? '0' + value : 'a' + value - 10); + } + + private static boolean isJsonAscii(char ch) { + return ch > 0x1F && ch < 0x80 && ch != '"' && ch != '\\'; + } + + private static boolean isJsonAsciiByte(byte value) { + int ch = value & 0xff; + return ch > 0x1F && ch < 0x80 && ch != '"' && ch != '\\'; + } + + private static boolean isJsonAsciiWord(long word) { + return (((word + ASCII_CONTROL_OFFSET) & ~word) & HIGH_BITS) == HIGH_BITS + && (((word ^ QUOTE_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS + && (((word ^ BACKSLASH_BYTES_COMPLEMENT) + ONE_BYTES) & HIGH_BITS) == HIGH_BITS; + } + + private static boolean isJsonAsciiInt(int word) { + return (((word + INT_ASCII_CONTROL_OFFSET) & ~word) & INT_HIGH_BITS) == INT_HIGH_BITS + && (((word ^ INT_QUOTE_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) == INT_HIGH_BITS + && (((word ^ INT_BACKSLASH_BYTES_COMPLEMENT) + INT_ONE_BYTES) & INT_HIGH_BITS) + == INT_HIGH_BITS; + } + + private static int packUtf16Ascii(long word) { + return (int) + ((word & 0xFFL) + | ((word >>> 8) & 0xFF00L) + | ((word >>> 16) & 0xFF0000L) + | ((word >>> 24) & 0xFF000000L)); + } + + private void writeIntNoEnsure(int value) { + if (value == Integer.MIN_VALUE) { + writeRawNoEnsure(MIN_INT_BYTES); + return; + } + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + writePositiveIntNoEnsure(value); + } + + private void writeLongNoEnsure(long value) { + if (value == Long.MIN_VALUE) { + writeRawNoEnsure(MIN_LONG_BYTES); + return; + } + if (value < 0) { + buffer[position++] = (byte) '-'; + value = -value; + } + if (value <= Integer.MAX_VALUE) { + writePositiveIntNoEnsure((int) value); + return; + } + int start = position; + do { + buffer[position++] = (byte) ('0' + value % 10); + value /= 10; + } while (value != 0); + reverse(start, position - 1); + } + + private void writePositiveIntNoEnsure(int value) { + byte[] bytes = buffer; + int pos = position; + if (value < 10000) { + position = writeIntUpTo4(bytes, pos, value); + return; + } + int high = divide10000(value); + int low = value - high * 10000; + if (high < 10000) { + if (high >= 1000) { + position = writePadded8(bytes, pos, high, low); + return; + } + pos = writeIntUpTo4(bytes, pos, high); + position = writePadded4(bytes, pos, low); + return; + } + int top = divide10000(high); + int middle = high - top * 10000; + pos = writeIntUpTo4(bytes, pos, top); + position = writePadded8(bytes, pos, middle, low); + } + + private static int divide10000(int value) { + return (int) (((long) value * 1759218605L) >> 44); + } + + private static int writeIntUpTo4(byte[] bytes, int pos, int value) { + if (value < 1000) { + return writeIntUpTo3(bytes, pos, value); + } + return writePadded4(bytes, pos, value); + } + + private static int writeIntUpTo3(byte[] bytes, int pos, int value) { + int digits = DIGIT_TRIPLES[value]; + int skip = digits & 0xFF; + LittleEndian.putInt32(bytes, pos, digits >>> ((skip + 1) << 3)); + return pos + 3 - skip; + } + + private static int writePadded4(byte[] bytes, int pos, int value) { + LittleEndian.putInt32(bytes, pos, DIGIT_QUADS[value]); + return pos + 4; + } + + private static int writePadded8(byte[] bytes, int pos, int high, int low) { + long value = (DIGIT_QUADS[high] & 0xFFFFFFFFL) | ((DIGIT_QUADS[low] & 0xFFFFFFFFL) << 32); + LittleEndian.putInt64(bytes, pos, value); + return pos + 8; + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8ObjectWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8ObjectWriter.java new file mode 100644 index 0000000000..a1005ed522 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8ObjectWriter.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.json.writer; + +import org.apache.fory.json.resolver.JsonTypeResolver; + +public interface Utf8ObjectWriter { + void writeUtf8(Utf8JsonWriter writer, Object value, JsonTypeResolver typeResolver); +} diff --git a/java/fory-json/src/main/java9/module-info.java b/java/fory-json/src/main/java9/module-info.java new file mode 100644 index 0000000000..438ecd0b2c --- /dev/null +++ b/java/fory-json/src/main/java9/module-info.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. + */ + +module org.apache.fory.json { + requires org.apache.fory.core; + requires static java.sql; + + exports org.apache.fory.json; + exports org.apache.fory.json.annotation; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java new file mode 100644 index 0000000000..cdbd4b4d19 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java @@ -0,0 +1,317 @@ +/* + * 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; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; +import org.apache.fory.json.data.FastContainers; +import org.apache.fory.json.data.GeneratedCollectionFields; +import org.apache.fory.json.data.JsonTestData; +import org.apache.fory.json.data.Kind; +import org.apache.fory.json.data.MethodsIgnored; +import org.apache.fory.json.data.NumericBoundaries; +import org.apache.fory.json.data.PrivateFields; +import org.apache.fory.json.data.PublicFields; +import org.apache.fory.json.data.TokenValues; +import org.apache.fory.json.data.UnicodeMatrix; +import org.apache.fory.platform.JdkVersion; +import org.apache.fory.reflect.FieldAccessor; +import org.testng.SkipException; + +public abstract class ForyJsonTestModels { + protected static final String TWO_BYTE_TEXT = JsonTestData.TWO_BYTE_TEXT; + protected static final String THREE_BYTE_TEXT = JsonTestData.THREE_BYTE_TEXT; + protected static final String SUPPLEMENTARY_TEXT = JsonTestData.SUPPLEMENTARY_TEXT; + protected static final String MIXED_SCRIPT_TEXT = JsonTestData.MIXED_SCRIPT_TEXT; + protected static final String COMBINING_TEXT = JsonTestData.COMBINING_TEXT; + protected static final String ZH_TEXT = JsonTestData.ZH_TEXT; + protected static final String EU_TEXT = JsonTestData.EU_TEXT; + + protected static TokenValues tokenValue(int count, String name, List tags, long total) { + TokenValues value = new TokenValues(); + value.count = count; + value.name = name; + value.tags = tags; + value.total = total; + return value; + } + + protected static String hiddenValue(MethodsIgnored value) { + return MethodsIgnored.hiddenValue(value); + } + + protected static int privateId(PrivateFields value) { + return PrivateFields.id(value); + } + + protected static String privateName(PrivateFields value) { + return PrivateFields.name(value); + } + + protected static String privateNullable(PrivateFields value) { + return PrivateFields.nullable(value); + } + + protected static String privateTransientValue(PrivateFields value) { + return PrivateFields.transientValue(value); + } + + protected static String privateStaticValue() { + return PrivateFields.staticValue(); + } + + protected static Map scores() { + return JsonTestData.scores(); + } + + protected static Map flags() { + return JsonTestData.flags(); + } + + protected static Map intNames() { + return JsonTestData.intNames(); + } + + protected static EnumMap enumScores() { + return JsonTestData.enumScores(); + } + + protected static Calendar calendar(long millis) { + return JsonTestData.calendar(millis); + } + + protected static byte[] byteBufferBytes(ByteBuffer buffer) { + ByteBuffer copy = buffer.duplicate(); + byte[] bytes = new byte[copy.remaining()]; + copy.get(bytes); + return bytes; + } + + protected static Map unicodeMap() { + return JsonTestData.unicodeMap(); + } + + protected static String unicodeMatrixJson() { + return "{\"boxedChar\":\"\u20ac\",\"charThreeByte\":\"\u4f60\"," + + "\"charTwoByte\":\"\u0100\",\"chars\":[\"\u0100\",\"\u07ff\",\"\u0800\"," + + "\"\u20ac\",\"\u4f60\"],\"combining\":\"" + + COMBINING_TEXT + + "\",\"eu\":\"" + + EU_TEXT + + "\",\"mixedScripts\":\"" + + MIXED_SCRIPT_TEXT + + "\",\"supplementary\":\"" + + SUPPLEMENTARY_TEXT + + "\",\"threeByte\":\"" + + THREE_BYTE_TEXT + + "\",\"twoByte\":\"" + + TWO_BYTE_TEXT + + "\",\"valueMap\":{\"" + + TWO_BYTE_TEXT + + "\":\"" + + THREE_BYTE_TEXT + + "\",\"" + + ZH_TEXT + + "\":\"" + + EU_TEXT + + "\",\"\u043a\u043b\u044e\u0447\":\"\uD83D\uDE00\"," + + "\"\u0645\u0631\u062d\u0628\u0627\":\"\u0928\u092e\u0938\u094d\u0924\u0947\"}," + + "\"values\":[\"" + + TWO_BYTE_TEXT + + "\",\"" + + THREE_BYTE_TEXT + + "\",\"" + + SUPPLEMENTARY_TEXT + + "\",\"" + + MIXED_SCRIPT_TEXT + + "\",\"" + + COMBINING_TEXT + + "\",\"" + + ZH_TEXT + + "\",\"" + + EU_TEXT + + "\"],\"zh\":\"" + + ZH_TEXT + + "\"}"; + } + + protected static void assertUnicodeMatrix(UnicodeMatrix value) { + assertEquals(value.boxedChar, Character.valueOf('€')); + assertEquals(value.charThreeByte, '你'); + assertEquals(value.charTwoByte, 'Ā'); + assertEquals(value.chars, new char[] {'Ā', '߿', 'ࠀ', '€', '你'}); + assertEquals(value.combining, COMBINING_TEXT); + assertEquals(value.eu, EU_TEXT); + assertEquals(value.mixedScripts, MIXED_SCRIPT_TEXT); + assertEquals(value.supplementary, SUPPLEMENTARY_TEXT); + assertEquals(value.threeByte, THREE_BYTE_TEXT); + assertEquals(value.twoByte, TWO_BYTE_TEXT); + assertEquals(value.valueMap, unicodeMap()); + assertEquals( + value.values, + Arrays.asList( + TWO_BYTE_TEXT, + THREE_BYTE_TEXT, + SUPPLEMENTARY_TEXT, + MIXED_SCRIPT_TEXT, + COMBINING_TEXT, + ZH_TEXT, + EU_TEXT)); + assertEquals(value.zh, ZH_TEXT); + } + + protected static void assertFastContainers(FastContainers value) { + assertEquals(value.booleans, Arrays.asList(Boolean.TRUE, Boolean.FALSE)); + assertEquals(value.flags, flags()); + assertEquals(value.intNames, intNames()); + assertEquals(value.ints, Arrays.asList(1, 2)); + assertEquals(value.names, Arrays.asList("alpha", ZH_TEXT)); + assertEquals(value.scores, scores()); + } + + protected static void assertGeneratedCollections(GeneratedCollectionFields value) { + assertTrue(value.kinds instanceof EnumSet); + assertEquals(value.kinds, EnumSet.of(Kind.FAST, Kind.SMALL)); + assertTrue(value.names instanceof LinkedHashSet); + assertEquals(value.names, new LinkedHashSet<>(Arrays.asList("alpha", ZH_TEXT))); + assertTrue(value.numbers instanceof ArrayDeque); + assertEquals(new ArrayList<>(value.numbers), Arrays.asList(1, 2)); + } + + protected static void assertNumericBoundaries(NumericBoundaries value, String text) { + assertEquals(value.intMax, Integer.MAX_VALUE); + assertEquals(value.intMin, Integer.MIN_VALUE); + assertEquals(value.longMax, Long.MAX_VALUE); + assertEquals(value.longMin, Long.MIN_VALUE); + assertEquals(value.small, -7); + assertEquals(value.text, text); + } + + protected static Class compileRecordClass(String simpleName, String source) throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertTrue(compiler != null); + Path output = + Paths.get( + ForyJsonTestModels.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + Path sourceDir = Files.createTempDirectory("fory-json-record"); + Files.createDirectories(sourceDir); + Path sourceFile = sourceDir.resolve(simpleName + ".java"); + Files.write(sourceFile, source.getBytes(StandardCharsets.UTF_8)); + int exit = + compiler.run( + null, null, null, "--release", "17", "-d", output.toString(), sourceFile.toString()); + assertEquals(exit, 0); + return Class.forName( + "org.apache.fory.json.records." + simpleName, + true, + ForyJsonTestModels.class.getClassLoader()); + } + + protected static void assertRecordValue(Object value, Class childType) throws Exception { + Class type = value.getClass(); + assertEquals(type.getMethod("id").invoke(value), Integer.valueOf(7)); + assertEquals(type.getMethod("name").invoke(value), ZH_TEXT); + assertEquals(type.getMethod("tags").invoke(value), Arrays.asList("a", "b")); + Object child = type.getMethod("child").invoke(value); + assertEquals(childType.getMethod("label").invoke(child), "kid"); + } + + protected static void assertTextRoundTrip(ForyJson json, String text) { + String rootJson = "\"" + text + "\""; + assertEquals(json.toJson(text), rootJson); + assertEquals(new String(json.toJsonBytes(text), StandardCharsets.UTF_8), rootJson); + assertEquals(json.fromJson(rootJson, String.class), text); + assertEquals(json.fromJson(rootJson.getBytes(StandardCharsets.UTF_8), String.class), text); + + PublicFields fields = new PublicFields(); + fields.name = text; + String objectJson = "{\"active\":true,\"id\":7,\"name\":\"" + text + "\"}"; + assertEquals(json.toJson(fields), objectJson); + assertEquals(new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), objectJson); + assertEquals(json.fromJson(objectJson, PublicFields.class).name, text); + assertEquals( + json.fromJson(objectJson.getBytes(StandardCharsets.UTF_8), PublicFields.class).name, text); + } + + protected static void assertGeneratedWhenSupported(ForyJson json, Class type) { + assertEquals(json.hasGeneratedWriter(type), JdkVersion.MAJOR_VERSION >= 15); + } + + protected static String repeat(char ch, int length) { + char[] chars = new char[length]; + Arrays.fill(chars, ch); + return new String(chars); + } + + protected static String nestedArray(int depth) { + StringBuilder builder = new StringBuilder(depth * 2 + 1); + for (int i = 0; i < depth; i++) { + builder.append('['); + } + builder.append('0'); + for (int i = 0; i < depth; i++) { + builder.append(']'); + } + return builder.toString(); + } + + protected static int arrayCapacity(ArrayList list) { + try { + Field field = ArrayList.class.getDeclaredField("elementData"); + Object[] array = (Object[]) FieldAccessor.createAccessor(field).getObject(list); + return array.length; + } catch (Throwable cause) { + throw new SkipException("Cannot inspect ArrayList capacity on this runtime", cause); + } + } + + protected static int mapCapacity(HashMap map) { + try { + Field field = HashMap.class.getDeclaredField("table"); + Object[] table = (Object[]) FieldAccessor.createAccessor(field).getObject(map); + return table == null ? 0 : table.length; + } catch (Throwable cause) { + throw new SkipException("Cannot inspect HashMap capacity on this runtime", cause); + } + } + + protected static Map values() { + return JsonTestData.values(); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java new file mode 100644 index 0000000000..9357fea10f --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -0,0 +1,238 @@ +/* + * 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; + +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.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.fory.json.data.FastContainers; +import org.apache.fory.json.data.MapKeyFields; +import org.apache.fory.json.data.Nested; +import org.apache.fory.json.data.TokenValues; +import org.apache.fory.reflect.TypeRef; +import org.testng.annotations.Test; + +public class JsonContainerTest extends ForyJsonTestModels { + @Test + public void writeNestedCollections() { + ForyJson json = ForyJson.builder().build(); + assertEquals( + json.toJson(new Nested()), + "{\"kind\":\"FAST\",\"names\":[\"a\",\"b\"],\"scores\":{\"one\":1,\"two\":2}}"); + } + + @Test + public void readTypeRefList() { + ForyJson json = ForyJson.builder().build(); + List values = + json.fromJson( + "[{\"count\":1,\"name\":\"alpha\",\"tags\":[\"x\",\"y\"],\"total\":2}," + + "{\"count\":3,\"name\":\"beta\",\"tags\":[\"z\"],\"total\":4}]", + new TypeRef>() {}); + assertEquals(values.size(), 2); + assertEquals(values.get(0).name, "alpha"); + assertEquals(values.get(0).tags, Arrays.asList("x", "y")); + assertEquals(values.get(1).count, 3); + assertEquals(values.get(1).total, 4); + } + + @Test + public void readTypeRefMapBytes() { + ForyJson json = ForyJson.builder().build(); + byte[] bytes = + "{\"first\":{\"count\":5,\"name\":\"gamma\",\"tags\":[\"u\"],\"total\":6}}" + .getBytes(StandardCharsets.UTF_8); + Map values = + json.fromJson(bytes, new TypeRef>() {}); + assertEquals(values.size(), 1); + assertEquals(values.get("first").name, "gamma"); + assertEquals(values.get("first").tags, Arrays.asList("u")); + assertEquals(values.get("first").total, 6); + } + + @Test + public void readJsonContainers() { + ForyJson json = ForyJson.builder().build(); + JSONObject object = + json.fromJson("{\"name\":\"fory\",\"items\":[1,\"你好,Fory\"]}", JSONObject.class); + assertEquals(object.get("name"), "fory"); + assertTrue(object.get("items") instanceof JSONArray); + JSONArray items = (JSONArray) object.get("items"); + assertEquals(items.get(0), Long.valueOf(1)); + assertEquals(items.get(1), ZH_TEXT); + + Object natural = json.fromJson("{\"items\":[true]}", Object.class); + assertTrue(natural instanceof JSONObject); + assertTrue(((JSONObject) natural).get("items") instanceof JSONArray); + } + + @Test + public void parsedContainersStartSmall() { + ForyJson json = ForyJson.builder().build(); + JSONArray array = json.fromJson("[1]", JSONArray.class); + assertEquals(arrayCapacity(array), 1); + assertEquals(arrayCapacity(json.fromJson("[]", JSONArray.class)), 0); + + List list = json.fromJson("[1]", new TypeRef>() {}); + assertTrue(list instanceof ArrayList); + assertEquals(arrayCapacity((ArrayList) list), 1); + + JSONObject object = json.fromJson("{\"x\":1}", JSONObject.class); + assertEquals(mapCapacity(object), 2); + assertEquals(mapCapacity(json.fromJson("{}", JSONObject.class)), 0); + + Map map = json.fromJson("{\"x\":1}", new TypeRef>() {}); + assertTrue(map instanceof LinkedHashMap); + assertEquals(mapCapacity((LinkedHashMap) map), 2); + } + + @Test + public void writeJsonContainers() { + ForyJson json = ForyJson.builder().build(); + JSONObject object = new JSONObject(); + JSONArray values = new JSONArray(); + values.add(Integer.valueOf(1)); + values.add(ZH_TEXT); + object.put("values", values); + object.put("name", "fory"); + String expected = "{\"values\":[1,\"你好,Fory\"],\"name\":\"fory\"}"; + assertEquals(json.toJson(object), expected); + assertEquals(new String(json.toJsonBytes(object), StandardCharsets.UTF_8), expected); + } + + @Test + public void writeReadMapKeyFields() { + ForyJson json = ForyJson.builder().build(); + String expected = + "{\"intNames\":{\"1\":\"one\",\"2\":\"two\"},\"scores\":{\"FAST\":1,\"SMALL\":2}}"; + assertEquals(json.toJson(new MapKeyFields()), expected); + MapKeyFields read = json.fromJson(expected, MapKeyFields.class); + assertEquals(read.intNames, intNames()); + assertEquals(read.scores, enumScores()); + } + + @Test + public void readTypeRefOptional() { + ForyJson json = ForyJson.builder().build(); + Optional value = + json.fromJson( + "{\"count\":9,\"name\":\"optional\",\"tags\":[\"a\"],\"total\":10}", + new TypeRef>() {}); + assertTrue(value.isPresent()); + assertEquals(value.get().name, "optional"); + assertEquals(value.get().tags, Arrays.asList("a")); + assertEquals(json.fromJson("null", new TypeRef>() {}), Optional.empty()); + } + + @Test + public void readTypeRefMapKeys() { + ForyJson json = ForyJson.builder().build(); + Map value = + json.fromJson("{\"1\":\"one\",\"2\":\"two\"}", new TypeRef>() {}); + assertEquals(value, intNames()); + + Map escaped = + json.fromJson("{\"\\u0031\":\"one\"}", new TypeRef>() {}); + assertEquals(escaped.get(1), "one"); + + Map longs = + json.fromJson( + "{\"-1\":\"negative\",\"9223372036854775807\":\"max\"}", + new TypeRef>() {}); + Map expected = new LinkedHashMap<>(); + expected.put(-1L, "negative"); + expected.put(Long.MAX_VALUE, "max"); + assertEquals(longs, expected); + assertEquals( + json.fromJson( + "{\"-1\":\"negative\",\"9223372036854775807\":\"max\"}" + .getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}), + expected); + } + + @Test + public void readFastContainerTypeRefs() { + ForyJson json = ForyJson.builder().build(); + assertEquals( + json.fromJson("[\"alpha\",\"你好,Fory\"]", new TypeRef>() {}), + Arrays.asList("alpha", ZH_TEXT)); + assertEquals(json.fromJson("[1,2]", new TypeRef>() {}), Arrays.asList(1, 2)); + assertEquals( + json.fromJson("[true,false]", new TypeRef>() {}), + Arrays.asList(Boolean.TRUE, Boolean.FALSE)); + assertEquals( + json.fromJson( + "{\"one\":1,\"two\":2}".getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}), + scores()); + assertEquals( + json.fromJson( + "{\"enabled\":true,\"disabled\":false}", new TypeRef>() {}), + flags()); + Map aliases = new LinkedHashMap<>(); + aliases.put("zh", ZH_TEXT); + aliases.put("eu", EU_TEXT); + assertEquals( + json.fromJson( + "{\"zh\":\"你好,Fory\",\"eu\":\"café crème Österreich € ČšŽ\"}", + new TypeRef>() {}), + aliases); + assertEquals( + json.fromJson("{\"1\":\"one\",\"2\":\"two\"}", new TypeRef>() {}), + intNames()); + } + + @Test + public void writeReadFastContainerFields() { + ForyJson json = ForyJson.builder().build(); + String input = + "{\"booleans\":[true,false],\"flags\":{\"enabled\":true,\"disabled\":false}," + + "\"intNames\":{\"1\":\"one\",\"2\":\"two\"},\"ints\":[1,2]," + + "\"names\":[\"alpha\",\"你好,Fory\"],\"scores\":{\"one\":1,\"two\":2}}"; + FastContainers read = json.fromJson(input, FastContainers.class); + assertFastContainers(read); + assertFastContainers( + json.fromJson(json.toJsonBytes(new FastContainers()), FastContainers.class)); + } + + @Test + public void readPrimitiveArrayRoots() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new int[] {1, 2}), "[1,2]"); + assertEquals(json.fromJson("[1,2]", int[].class), new int[] {1, 2}); + assertEquals( + json.fromJson("[1,2]".getBytes(StandardCharsets.UTF_8), int[].class), new int[] {1, 2}); + assertEquals( + json.fromJson("[true,false]".getBytes(StandardCharsets.UTF_8), boolean[].class), + new boolean[] {true, false}); + assertEquals(json.fromJson("[1,-2,3]", byte[].class), new byte[] {1, -2, 3}); + assertEquals(json.fromJson("[\"a\",\"你\"]", char[].class), new char[] {'a', '你'}); + assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", int[].class)); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonDepthTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonDepthTest.java new file mode 100644 index 0000000000..701c40d2f9 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonDepthTest.java @@ -0,0 +1,121 @@ +/* + * 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; + +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.Map; +import org.apache.fory.json.data.DepthNode; +import org.apache.fory.reflect.TypeRef; +import org.testng.annotations.Test; + +public class JsonDepthTest extends ForyJsonTestModels { + @Test + public void defaultMaxDepth() { + ForyJson json = ForyJson.builder().build(); + assertTrue( + json.fromJson(nestedArray(ForyJson.DEFAULT_MAX_DEPTH), Object.class) instanceof JSONArray); + assertThrows( + ForyJsonException.class, + () -> json.fromJson(nestedArray(ForyJson.DEFAULT_MAX_DEPTH + 1), Object.class)); + } + + @Test + public void readMaxDepth() { + ForyJson json = ForyJson.builder().maxDepth(2).build(); + assertEquals(json.fromJson("{\"child\":{\"value\":1}}", DepthNode.class).child.value, 1); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("{\"child\":{\"child\":{\"value\":1}}}", DepthNode.class)); + assertThrows( + ForyJsonException.class, + () -> + json.fromJson( + "{\"child\":{\"child\":{\"value\":1}}}".getBytes(StandardCharsets.UTF_8), + DepthNode.class)); + } + + @Test + public void readContainerMaxDepth() { + ForyJson json = ForyJson.builder().maxDepth(2).build(); + assertTrue(json.fromJson("[[1]]", Object.class) instanceof JSONArray); + assertThrows(ForyJsonException.class, () -> json.fromJson("[[[1]]]", Object.class)); + assertEquals( + json.fromJson("{\"a\":{\"b\":1}}", new TypeRef>() {}).size(), 1); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("{\"a\":{\"b\":{\"c\":1}}}", new TypeRef>() {})); + + ForyJson nestedJson = ForyJson.builder().maxDepth(3).build(); + assertEquals( + nestedJson + .fromJson("{\"children\":[{\"value\":2}]}", DepthNode.class) + .children + .get(0) + .value, + 2); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("{\"children\":[{\"value\":2}]}", DepthNode.class)); + assertEquals( + nestedJson + .fromJson("{\"nodes\":{\"a\":{\"value\":3}}}", DepthNode.class) + .nodes + .get("a") + .value, + 3); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("{\"nodes\":{\"a\":{\"value\":3}}}", DepthNode.class)); + } + + @Test + public void readJsonObjectMaxDepth() { + ForyJson json = ForyJson.builder().maxDepth(2).build(); + JSONObject object = json.fromJson("{\"items\":[1]}", JSONObject.class); + assertTrue(object.get("items") instanceof JSONArray); + assertThrows( + ForyJsonException.class, () -> json.fromJson("{\"items\":[{}]}", JSONObject.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("[[{}]]", JSONArray.class)); + } + + @Test + public void readDepthReset() { + ForyJson json = ForyJson.builder().maxDepth(1).build(); + assertThrows( + ForyJsonException.class, () -> json.fromJson("{\"child\":{\"value\":1}}", DepthNode.class)); + assertEquals(json.fromJson("{\"value\":2}", DepthNode.class).value, 2); + assertThrows( + ForyJsonException.class, + () -> + json.fromJson( + "{\"child\":{\"value\":1}}".getBytes(StandardCharsets.UTF_8), DepthNode.class)); + assertEquals( + json.fromJson("{\"value\":3}".getBytes(StandardCharsets.UTF_8), DepthNode.class).value, 3); + } + + @Test + public void rejectInvalidMaxDepth() { + assertThrows(IllegalArgumentException.class, () -> ForyJson.builder().maxDepth(0)); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java new file mode 100644 index 0000000000..cf6a10d7d1 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -0,0 +1,122 @@ +/* + * 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; + +import static org.testng.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.apache.fory.json.data.GeneratedCollectionFields; +import org.apache.fory.json.data.RecursiveChild; +import org.apache.fory.json.data.RecursiveParent; +import org.apache.fory.json.data.TokenGroup; +import org.apache.fory.json.data.TokenValues; +import org.testng.annotations.Test; + +public class JsonGeneratedCodecTest extends ForyJsonTestModels { + @Test + public void writeRecursiveGeneratedTypes() { + ForyJson json = ForyJson.builder().build(); + RecursiveParent value = new RecursiveParent(); + assertEquals(json.toJson(value), "{\"child\":{\"name\":\"child\"},\"name\":\"parent\"}"); + assertEquals( + new String(json.toJsonBytes(value), StandardCharsets.UTF_8), + "{\"child\":{\"name\":\"child\"},\"name\":\"parent\"}"); + assertGeneratedWhenSupported(json, RecursiveParent.class); + assertGeneratedWhenSupported(json, RecursiveChild.class); + } + + @Test + public void writeGeneratedTokenChanges() { + ForyJson json = ForyJson.builder().build(); + TokenValues value = new TokenValues(); + String first = "{\"count\":1,\"name\":\"alpha\",\"tags\":[\"x\",\"y\"],\"total\":2}"; + assertEquals(json.toJson(value), first); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), first); + assertEquals(json.toJson(value), first); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), first); + value.count = 7; + value.name = "beta"; + value.tags = Arrays.asList("z", "x"); + value.total = 9; + String second = "{\"count\":7,\"name\":\"beta\",\"tags\":[\"z\",\"x\"],\"total\":9}"; + assertEquals(json.toJson(value), second); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), second); + assertGeneratedWhenSupported(json, TokenValues.class); + } + + @Test + public void writeGeneratedTokenLanes() { + ForyJson json = ForyJson.builder().build(); + TokenGroup group = new TokenGroup(); + group.values = + Arrays.asList( + tokenValue(1, "alpha", Arrays.asList("x", "y"), 2), + tokenValue(3, "beta", Arrays.asList("z", "x"), 4), + tokenValue(5, "gamma", Arrays.asList("y", "z"), 6)); + String first = + "{\"values\":[{\"count\":1,\"name\":\"alpha\",\"tags\":[\"x\",\"y\"],\"total\":2}," + + "{\"count\":3,\"name\":\"beta\",\"tags\":[\"z\",\"x\"],\"total\":4}," + + "{\"count\":5,\"name\":\"gamma\",\"tags\":[\"y\",\"z\"],\"total\":6}]}"; + assertEquals(json.toJson(group), first); + assertEquals(new String(json.toJsonBytes(group), StandardCharsets.UTF_8), first); + assertEquals(json.toJson(group), first); + assertEquals(new String(json.toJsonBytes(group), StandardCharsets.UTF_8), first); + TokenValues middle = group.values.get(1); + middle.count = 7; + middle.name = "delta"; + middle.tags = Arrays.asList("q", "x"); + middle.total = 8; + String second = + "{\"values\":[{\"count\":1,\"name\":\"alpha\",\"tags\":[\"x\",\"y\"],\"total\":2}," + + "{\"count\":7,\"name\":\"delta\",\"tags\":[\"q\",\"x\"],\"total\":8}," + + "{\"count\":5,\"name\":\"gamma\",\"tags\":[\"y\",\"z\"],\"total\":6}]}"; + assertEquals(json.toJson(group), second); + assertEquals(new String(json.toJsonBytes(group), StandardCharsets.UTF_8), second); + assertGeneratedWhenSupported(json, TokenGroup.class); + assertGeneratedWhenSupported(json, TokenValues.class); + } + + @Test + public void readGeneratedObjectCollection() { + ForyJson json = ForyJson.builder().build(); + String input = "{\"values\":[{\"count\":1,\"name\":\"alpha\",\"tags\":[\"x\"],\"total\":2}]}"; + TokenGroup stringValue = json.fromJson(input, TokenGroup.class); + TokenGroup utf8Value = json.fromJson(input.getBytes(StandardCharsets.UTF_8), TokenGroup.class); + assertEquals(stringValue.values.size(), 1); + assertEquals(stringValue.values.get(0).name, "alpha"); + assertEquals(stringValue.values.get(0).tags, Arrays.asList("x")); + assertEquals(utf8Value.values.size(), 1); + assertEquals(utf8Value.values.get(0).total, 2); + assertGeneratedWhenSupported(json, TokenGroup.class); + assertGeneratedWhenSupported(json, TokenValues.class); + } + + @Test + public void readGeneratedCollectionFields() { + ForyJson json = ForyJson.builder().build(); + String input = + "{\"kinds\":[\"FAST\",\"SMALL\"],\"names\":[\"alpha\",\"你好,Fory\"]," + "\"numbers\":[1,2]}"; + assertGeneratedCollections(json.fromJson(input, GeneratedCollectionFields.class)); + assertGeneratedCollections( + json.fromJson(input.getBytes(StandardCharsets.UTF_8), GeneratedCollectionFields.class)); + assertGeneratedWhenSupported(json, GeneratedCollectionFields.class); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java new file mode 100644 index 0000000000..1cd303cb3d --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java @@ -0,0 +1,214 @@ +/* + * 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; + +import static org.testng.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.apache.fory.json.data.BoxedScalars; +import org.apache.fory.json.data.DeclaredParentField; +import org.apache.fory.json.data.DirectionalIgnore; +import org.apache.fory.json.data.FirstIntField; +import org.apache.fory.json.data.MethodsIgnored; +import org.apache.fory.json.data.ParentValue; +import org.apache.fory.json.data.PrivateFields; +import org.apache.fory.json.data.PublicFields; +import org.testng.annotations.Test; + +public class JsonObjectTest extends ForyJsonTestModels { + @Test + public void writePublicFields() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new PublicFields()), "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); + assertEquals( + new String(json.toJsonBytes(new PublicFields()), StandardCharsets.UTF_8), + "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); + } + + @Test + public void writeFirstIntGenerated() { + ForyJson json = ForyJson.builder().build(); + String expected = "{\"count\":2,\"name\":\"first\"}"; + assertEquals(json.toJson(new FirstIntField()), expected); + assertEquals( + new String(json.toJsonBytes(new FirstIntField()), StandardCharsets.UTF_8), expected); + assertGeneratedWhenSupported(json, FirstIntField.class); + } + + @Test + public void sharedFacadeThreads() throws Exception { + ForyJson json = ForyJson.builder().build(); + String expected = "{\"active\":true,\"id\":7,\"name\":\"fory\"}"; + int threads = 8; + int iterations = 200; + ExecutorService executor = Executors.newFixedThreadPool(threads); + CountDownLatch start = new CountDownLatch(1); + List> futures = new ArrayList<>(); + try { + for (int t = 0; t < threads; t++) { + futures.add( + executor.submit( + () -> { + start.await(); + for (int i = 0; i < iterations; i++) { + assertEquals(json.toJson(new PublicFields()), expected); + assertEquals( + new String(json.toJsonBytes(new PublicFields()), StandardCharsets.UTF_8), + expected); + PublicFields value = json.fromJson(expected, PublicFields.class); + assertEquals(value.name, "fory"); + assertEquals(value.id, 7); + assertEquals(value.active, true); + } + return null; + })); + } + start.countDown(); + for (Future future : futures) { + future.get(); + } + } finally { + executor.shutdownNow(); + } + } + + @Test + public void useGeneratedWriter() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new PublicFields()), "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); + assertGeneratedWhenSupported(json, PublicFields.class); + } + + @Test + public void disableGeneratedWriter() { + ForyJson json = ForyJson.builder().withCodegen(false).build(); + assertEquals(json.toJson(new PublicFields()), "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); + PublicFields fields = + json.fromJson("{\"active\":false,\"id\":8,\"name\":\"json\"}", PublicFields.class); + assertEquals(fields.active, false); + assertEquals(fields.id, 8); + assertEquals(fields.name, "json"); + BoxedScalars scalars = + json.fromJson( + "{\"bool\":true,\"byteValue\":2,\"charValue\":\"x\",\"doubleValue\":2.5," + + "\"floatValue\":1.5,\"intValue\":4,\"longValue\":5,\"shortValue\":3}", + BoxedScalars.class); + assertEquals(scalars.byteValue, Byte.valueOf((byte) 2)); + assertEquals(scalars.shortValue, Short.valueOf((short) 3)); + assertEquals(json.hasGeneratedWriter(PublicFields.class), false); + } + + @Test + public void writeNullFields() { + ForyJson json = ForyJson.builder().writeNullFields(true).build(); + assertEquals( + json.toJson(new PublicFields()), + "{\"active\":true,\"id\":7,\"missing\":null,\"name\":\"fory\"}"); + } + + @Test + public void ignoreMethods() { + ForyJson json = ForyJson.builder().build(); + assertEquals( + json.toJson(new MethodsIgnored()), + "{\"hidden\":\"hidden\",\"setterCalls\":0,\"value\":\"field\"}"); + MethodsIgnored value = + json.fromJson("{\"hidden\":\"json\",\"value\":\"json\"}", MethodsIgnored.class); + assertEquals(hiddenValue(value), "json"); + assertEquals(value.setterCalls, 0); + assertEquals(value.value, "json"); + } + + @Test + public void writeDeclaredFields() { + ForyJson json = ForyJson.builder().build(); + String expected = "{\"id\":11,\"name\":\"private\"}"; + assertEquals(json.toJson(new PrivateFields()), expected); + assertEquals( + new String(json.toJsonBytes(new PrivateFields()), StandardCharsets.UTF_8), expected); + assertGeneratedWhenSupported(json, PrivateFields.class); + PrivateFields value = + json.fromJson("{\"id\":12,\"name\":\"json\",\"nullable\":\"value\"}", PrivateFields.class); + assertEquals(privateId(value), 12); + assertEquals(privateName(value), "json"); + assertEquals(privateNullable(value), "value"); + assertEquals(privateTransientValue(value), "transient"); + assertEquals(privateStaticValue(), "static"); + } + + @Test + public void writeDirectionalIgnore() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new DirectionalIgnore()), "{\"writeOnly\":2}"); + } + + @Test + public void writeDeclaredObjectFieldType() { + ForyJson json = ForyJson.builder().build(); + String expected = "{\"value\":{\"parent\":1}}"; + assertEquals(json.toJson(new DeclaredParentField()), expected); + assertEquals( + new String(json.toJsonBytes(new DeclaredParentField()), StandardCharsets.UTF_8), expected); + DeclaredParentField read = + json.fromJson("{\"value\":{\"child\":9,\"parent\":3}}", DeclaredParentField.class); + assertEquals(read.value.getClass(), ParentValue.class); + assertEquals(read.value.parent, 3); + } + + @Test + public void readPublicFields() { + ForyJson json = ForyJson.builder().build(); + PublicFields fields = + json.fromJson( + "{\"unknown\":[1,true,{\"x\":\"y\"}],\"name\":\"fory\",\"id\":7,\"active\":true}", + PublicFields.class); + assertEquals(fields.name, "fory"); + assertEquals(fields.id, 7); + assertEquals(fields.active, true); + } + + @Test + public void readUtf8Bytes() { + ForyJson json = ForyJson.builder().build(); + byte[] bytes = + "{\"name\":\"\uD83D\uDE00\u1234\",\"id\":8,\"active\":false}" + .getBytes(StandardCharsets.UTF_8); + PublicFields fields = json.fromJson(bytes, PublicFields.class); + assertEquals(fields.name, "\uD83D\uDE00\u1234"); + assertEquals(fields.id, 8); + assertEquals(fields.active, false); + } + + @Test + public void readDirectionalIgnore() { + ForyJson json = ForyJson.builder().build(); + DirectionalIgnore value = + json.fromJson("{\"both\":7,\"writeOnly\":8,\"readOnly\":9}", DirectionalIgnore.class); + assertEquals(value.both, 1); + assertEquals(value.writeOnly, 2); + assertEquals(value.readOnly, 9); + } +} 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 new file mode 100644 index 0000000000..b65328b5bc --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java @@ -0,0 +1,66 @@ +/* + * 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; + +import static org.testng.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import org.apache.fory.platform.JdkVersion; +import org.testng.SkipException; +import org.testng.annotations.Test; + +public class JsonRecordTest extends ForyJsonTestModels { + @Test + public void writeReadRecordClass() throws Exception { + if (JdkVersion.MAJOR_VERSION < 17) { + throw new SkipException("Java record test requires JDK 17+"); + } + Class type = + compileRecordClass( + "JsonRecordValue", + "package org.apache.fory.json.records;\n" + + "import java.util.List;\n" + + "public record JsonRecordValue(int id, String name, List tags, " + + "Child child) {\n" + + " public record Child(String label) {}\n" + + "}\n"); + Class childType = Class.forName(type.getName() + "$Child", true, type.getClassLoader()); + Object child = childType.getConstructor(String.class).newInstance("kid"); + Object value = + type.getConstructor(int.class, String.class, List.class, childType) + .newInstance(7, ZH_TEXT, Arrays.asList("a", "b"), child); + ForyJson json = ForyJson.builder().build(); + String expected = + "{\"child\":{\"label\":\"kid\"},\"id\":7,\"name\":\"你好,Fory\"," + "\"tags\":[\"a\",\"b\"]}"; + assertEquals(json.toJson(value), expected); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + assertGeneratedWhenSupported(json, type); + assertRecordValue(json.fromJson(expected, type), childType); + assertRecordValue(json.fromJson(expected.getBytes(StandardCharsets.UTF_8), type), childType); + + Object missing = json.fromJson("{\"name\":\"missing\"}", type); + assertEquals(type.getMethod("id").invoke(missing), Integer.valueOf(0)); + assertEquals(type.getMethod("name").invoke(missing), "missing"); + assertEquals(type.getMethod("tags").invoke(missing), null); + assertEquals(type.getMethod("child").invoke(missing), null); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java new file mode 100644 index 0000000000..e79ee060fa --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -0,0 +1,168 @@ +/* + * 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; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import org.apache.fory.json.data.BoxedScalars; +import org.apache.fory.json.data.CoreScalarFields; +import org.apache.fory.json.data.NaturalObjectValue; +import org.apache.fory.json.data.NaturalValues; +import org.apache.fory.json.data.NumericBoundaries; +import org.apache.fory.json.data.PublicFields; +import org.testng.annotations.Test; + +public class JsonScalarTest extends ForyJsonTestModels { + @Test + public void writeBoxedScalars() { + ForyJson json = ForyJson.builder().build(); + String expected = + "{\"bool\":true,\"byteValue\":2,\"charValue\":\"x\",\"doubleValue\":2.5," + + "\"floatValue\":1.5,\"intValue\":4,\"longValue\":5,\"shortValue\":3}"; + assertEquals(json.toJson(new BoxedScalars()), expected); + assertEquals( + new String(json.toJsonBytes(new BoxedScalars()), StandardCharsets.UTF_8), expected); + } + + @Test + public void writeNaturalObjectValues() { + ForyJson json = ForyJson.builder().build(); + String expected = + "{\"bool\":true,\"list\":[\"a\",1,false],\"map\":{\"name\":\"fory\",\"score\":9}," + + "\"number\":7,\"text\":\"fory\"}"; + assertEquals(json.toJson(new NaturalValues()), expected); + assertEquals( + new String(json.toJsonBytes(new NaturalValues()), StandardCharsets.UTF_8), expected); + } + + @Test + public void writeNaturalEmptyObject() { + ForyJson json = ForyJson.builder().build(); + String expected = "{\"value\":{}}"; + assertEquals(json.toJson(new NaturalObjectValue()), expected); + assertEquals( + new String(json.toJsonBytes(new NaturalObjectValue()), StandardCharsets.UTF_8), expected); + } + + @Test + public void readBoxedScalars() { + ForyJson json = ForyJson.builder().build(); + BoxedScalars value = + json.fromJson( + "{\"bool\":false,\"byteValue\":6,\"charValue\":\"z\",\"doubleValue\":3.5," + + "\"floatValue\":2.5,\"intValue\":8,\"longValue\":9,\"shortValue\":7}", + BoxedScalars.class); + assertEquals(value.bool, Boolean.FALSE); + assertEquals(value.byteValue, Byte.valueOf((byte) 6)); + assertEquals(value.charValue, Character.valueOf('z')); + assertEquals(value.doubleValue, Double.valueOf(3.5)); + assertEquals(value.floatValue, Float.valueOf(2.5f)); + assertEquals(value.intValue, Integer.valueOf(8)); + assertEquals(value.longValue, Long.valueOf(9)); + assertEquals(value.shortValue, Short.valueOf((short) 7)); + } + + @Test + public void readNumericBoundaries() { + ForyJson json = ForyJson.builder().build(); + String latin1 = + "{\"intMax\":2147483647,\"intMin\":-2147483648," + + "\"longMax\":9223372036854775807,\"longMin\":-9223372036854775808," + + "\"small\":-7,\"text\":\"café\"}"; + String utf16 = latin1.replace("café", ZH_TEXT); + assertNumericBoundaries(json.fromJson(latin1, NumericBoundaries.class), "café"); + assertNumericBoundaries(json.fromJson(utf16, NumericBoundaries.class), ZH_TEXT); + assertNumericBoundaries( + json.fromJson(utf16.getBytes(StandardCharsets.UTF_8), NumericBoundaries.class), ZH_TEXT); + + assertThrows(ForyJsonException.class, () -> json.fromJson("2147483648", int.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("-2147483649", int.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("1.0", int.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("9223372036854775808", long.class)); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("-9223372036854775809".getBytes(StandardCharsets.UTF_8), long.class)); + assertThrows( + ForyJsonException.class, + () -> + json.fromJson( + "{\"intMax\":2147483648,\"text\":\"" + ZH_TEXT + "\"}", NumericBoundaries.class)); + } + + @Test + public void writeReadCoreScalarFields() { + ForyJson json = ForyJson.builder().build(); + CoreScalarFields value = new CoreScalarFields(); + String expected = + "{\"atomicInt\":7,\"bigDecimal\":12345.6789,\"bigInteger\":12345678901234567890," + + "\"builder\":\"build\",\"bytes\":[1,-2,3],\"calendar\":123456789," + + "\"charset\":\"UTF-8\",\"currency\":\"EUR\",\"date\":\"2026-06-21\"," + + "\"instant\":\"2026-06-21T01:02:03Z\",\"locale\":\"zh-Hans-CN\"," + + "\"maybe\":\"yes\",\"optionalInt\":4,\"timeZone\":\"UTC\",\"type\":\"" + + PublicFields.class.getName() + + "\",\"uri\":\"https://fory.apache.org/json\"," + + "\"url\":\"https://fory.apache.org/\"," + + "\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"}"; + assertEquals(json.toJson(value), expected); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + CoreScalarFields read = json.fromJson(expected, CoreScalarFields.class); + assertEquals(read.atomicInt.get(), 7); + assertEquals(read.bigDecimal, value.bigDecimal); + assertEquals(read.bigInteger, value.bigInteger); + assertEquals(read.builder.toString(), "build"); + assertEquals(byteBufferBytes(read.bytes), new byte[] {1, -2, 3}); + assertEquals(read.calendar.getTimeInMillis(), 123456789L); + assertEquals(read.charset, StandardCharsets.UTF_8); + assertEquals(read.currency, value.currency); + assertEquals(read.date, value.date); + assertEquals(read.instant, value.instant); + assertEquals(read.locale, value.locale); + assertEquals(read.maybe, Optional.of("yes")); + assertEquals(read.optionalInt.getAsInt(), 4); + assertEquals(read.timeZone.getID(), "UTC"); + assertEquals(read.type, PublicFields.class); + assertEquals(read.uri, value.uri); + assertEquals(read.url, value.url); + assertEquals(read.uuid, value.uuid); + } + + @Test + public void readScalarRoots() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.fromJson("7", int.class), Integer.valueOf(7)); + assertEquals(json.fromJson("true", boolean.class), Boolean.TRUE); + assertEquals(json.fromJson("\"fory\"".getBytes(StandardCharsets.UTF_8), String.class), "fory"); + assertEquals( + json.fromJson("\"\uD83D\uDE00\u1234\"".getBytes(StandardCharsets.UTF_8), String.class), + "\uD83D\uDE00\u1234"); + } + + @Test + public void rejectLeadingZero() { + ForyJson json = ForyJson.builder().build(); + assertThrows(ForyJsonException.class, () -> json.fromJson("01", int.class)); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("{\"id\":01}".getBytes(StandardCharsets.UTF_8), PublicFields.class)); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java new file mode 100644 index 0000000000..823873b160 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -0,0 +1,272 @@ +/* + * 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; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import org.apache.fory.json.data.CharValue; +import org.apache.fory.json.data.Kind; +import org.apache.fory.json.data.Nested; +import org.apache.fory.json.data.PublicFields; +import org.apache.fory.json.data.UnicodeEnumValue; +import org.apache.fory.json.data.UnicodeFieldNames; +import org.apache.fory.json.data.UnicodeKind; +import org.apache.fory.json.data.UnicodeMatrix; +import org.apache.fory.json.data.UnicodeValues; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.serializer.StringSerializer; +import org.testng.annotations.Test; + +public class JsonStringTest extends ForyJsonTestModels { + @Test + public void escapeStrings() { + ForyJson json = ForyJson.builder().build(); + PublicFields fields = new PublicFields(); + fields.name = "a\n\"b\"\\\u1234"; + String stringExpected = "{\"active\":true,\"id\":7,\"name\":\"a\\n\\\"b\\\"\\\\\u1234\"}"; + assertEquals(json.toJson(fields), stringExpected); + assertEquals(json.fromJson(stringExpected, PublicFields.class).name, fields.name); + } + + @Test + public void writeUtf16StringText() { + ForyJson json = ForyJson.builder().build(); + UnicodeValues values = new UnicodeValues(); + String expected = + "{\"first\":\"\u1234\",\"second\":\"music \uD834\uDD1E\"," + + "\"tags\":[\"latin\",\"\u1234\",\"\uD83D\uDE00\"]}"; + assertEquals(json.toJson(values), expected); + assertEquals(json.toJson(values), expected); + assertEquals(new String(json.toJsonBytes(values), StandardCharsets.UTF_8), expected); + assertEquals(json.fromJson(expected, UnicodeValues.class).second, values.second); + assertEquals(json.fromJson(json.toJsonBytes(values), UnicodeValues.class).tags, values.tags); + } + + @Test + public void writeUtf16Char() { + ForyJson json = ForyJson.builder().build(); + CharValue value = new CharValue(); + value.value = '\u1234'; + assertEquals(json.toJson(value), "{\"value\":\"\u1234\"}"); + assertEquals( + new String(json.toJsonBytes(value), StandardCharsets.UTF_8), "{\"value\":\"\u1234\"}"); + } + + @Test + public void writeNonLatin1Matrix() { + ForyJson json = ForyJson.builder().build(); + UnicodeMatrix value = new UnicodeMatrix(); + String expected = unicodeMatrixJson(); + assertEquals(json.toJson(value), expected); + assertEquals(json.toJson(value), expected); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + assertUnicodeMatrix(json.fromJson(expected, UnicodeMatrix.class)); + assertUnicodeMatrix( + json.fromJson(expected.getBytes(StandardCharsets.UTF_8), UnicodeMatrix.class)); + assertEquals(json.fromJson("\"" + MIXED_SCRIPT_TEXT + "\"", String.class), MIXED_SCRIPT_TEXT); + assertEquals( + json.fromJson( + ("\"" + SUPPLEMENTARY_TEXT + "\"").getBytes(StandardCharsets.UTF_8), String.class), + SUPPLEMENTARY_TEXT); + } + + @Test + public void writeReadZhEuStrings() { + ForyJson json = ForyJson.builder().build(); + assertTextRoundTrip(json, ZH_TEXT); + assertTextRoundTrip(json, EU_TEXT); + } + + @Test + public void stringWriterResetAfterMaterialize() { + StringJsonWriter writer = new StringJsonWriter(false, new byte[16]); + writer.writeString("你好,Fory"); + String utf16Json = writer.toJson(); + writer.reset(); + writer.writeString("ascii"); + assertEquals(utf16Json, "\"你好,Fory\""); + assertEquals(writer.toJson(), "\"ascii\""); + if (StringSerializer.isBytesBackedString()) { + assertTrue(StringSerializer.isUtf16Coder(StringSerializer.getStringCoder(utf16Json))); + } + } + + @Test + public void stringWriterShrinksOnReset() throws Exception { + StringJsonWriter writer = new StringJsonWriter(false, new byte[16]); + writer.writeString(repeat('a', 9000) + "你好,Fory"); + assertTrue(writerBufferLength(writer) > 8192); + writer.toJson(); + writer.reset(); + assertEquals(writerBufferLength(writer), 8192); + writer.writeString("café"); + assertEquals(writer.toJson(), "\"café\""); + } + + @Test + public void readStringInputLayouts() { + ForyJson json = ForyJson.builder().build(); + String latin1Json = "{\"active\":true,\"id\":7,\"name\":\"café\"}"; + String utf16Json = "{\"active\":true,\"id\":7,\"name\":\"你好,Fory\"}"; + if (StringSerializer.isBytesBackedString()) { + assertTrue(StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(latin1Json))); + assertTrue(StringSerializer.isUtf16Coder(StringSerializer.getStringCoder(utf16Json))); + } + assertEquals(json.fromJson(latin1Json, PublicFields.class).name, "café"); + assertEquals(json.fromJson(utf16Json, PublicFields.class).name, ZH_TEXT); + assertEquals(json.fromJson("\"café\"", String.class), "café"); + assertEquals(json.fromJson("\"你好,Fory\"", String.class), ZH_TEXT); + } + + @Test + public void readUnicodeFieldNames() { + ForyJson json = ForyJson.builder().build(); + String direct = "{\"café\":\"" + EU_TEXT + "\",\"你好\":\"" + ZH_TEXT + "\"}"; + String escaped = "{\"caf\\u00e9\":\"" + EU_TEXT + "\",\"\\u4f60\\u597d\":\"" + ZH_TEXT + "\"}"; + UnicodeFieldNames directValue = json.fromJson(direct, UnicodeFieldNames.class); + UnicodeFieldNames escapedValue = json.fromJson(escaped, UnicodeFieldNames.class); + UnicodeFieldNames bytesValue = + json.fromJson(direct.getBytes(StandardCharsets.UTF_8), UnicodeFieldNames.class); + assertEquals(directValue.café, EU_TEXT); + assertEquals(directValue.你好, ZH_TEXT); + assertEquals(escapedValue.café, EU_TEXT); + assertEquals(escapedValue.你好, ZH_TEXT); + assertEquals(bytesValue.café, EU_TEXT); + assertEquals(bytesValue.你好, ZH_TEXT); + } + + @Test + public void readUnicodeEnum() { + ForyJson json = ForyJson.builder().build(); + String direct = "{\"kind\":\"你好\"}"; + String escaped = "{\"kind\":\"\\u4f60\\u597d\"}"; + String asciiDirect = "{\"kind\":\"FAST\"}"; + String asciiEscaped = "{\"kind\":\"F\\u0041ST\"}"; + assertEquals( + new String(json.toJsonBytes(new UnicodeEnumValue()), StandardCharsets.UTF_8), direct); + assertEquals(json.fromJson(direct, UnicodeEnumValue.class).kind, UnicodeKind.你好); + assertEquals(json.fromJson(escaped, UnicodeEnumValue.class).kind, UnicodeKind.你好); + assertEquals( + json.fromJson(direct.getBytes(StandardCharsets.UTF_8), UnicodeEnumValue.class).kind, + UnicodeKind.你好); + assertEquals(json.fromJson(asciiDirect, Nested.class).kind, Kind.FAST); + assertEquals( + json.fromJson(asciiDirect.getBytes(StandardCharsets.UTF_8), Nested.class).kind, Kind.FAST); + assertEquals(json.fromJson(asciiEscaped, Nested.class).kind, Kind.FAST); + assertEquals( + json.fromJson(asciiEscaped.getBytes(StandardCharsets.UTF_8), Nested.class).kind, Kind.FAST); + } + + @Test + public void writeLatin1NonAsciiBytes() { + ForyJson json = ForyJson.builder().build(); + PublicFields fields = new PublicFields(); + fields.name = "caf\u00e9"; + assertEquals(json.toJson(fields), "{\"active\":true,\"id\":7,\"name\":\"caf\u00e9\"}"); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"active\":true,\"id\":7,\"name\":\"caf\u00e9\"}"); + fields.name = "\u0080"; + String expected = "{\"active\":true,\"id\":7,\"name\":\"\u0080\"}"; + assertEquals(json.toJson(fields), expected); + assertEquals(new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), expected); + assertEquals(json.fromJson(json.toJsonBytes(fields), PublicFields.class).name, fields.name); + } + + @Test + public void rejectSurrogateChar() { + ForyJson json = ForyJson.builder().build(); + CharValue value = new CharValue(); + value.value = '\uD800'; + assertThrows(ForyJsonException.class, () -> json.toJson(value)); + assertThrows(ForyJsonException.class, () -> json.toJsonBytes(value)); + } + + @Test + public void rejectSurrogateString() { + ForyJson json = ForyJson.builder().build(); + PublicFields fields = new PublicFields(); + fields.name = "\uD800"; + assertThrows(ForyJsonException.class, () -> json.toJson(fields)); + assertThrows(ForyJsonException.class, () -> json.toJsonBytes(fields)); + } + + @Test + public void writeSurrogatePair() { + ForyJson json = ForyJson.builder().build(); + PublicFields fields = new PublicFields(); + fields.name = "a\uD83D\uDE00"; + String stringExpected = "{\"active\":true,\"id\":7,\"name\":\"a\uD83D\uDE00\"}"; + String utf8Expected = "{\"active\":true,\"id\":7,\"name\":\"a\uD83D\uDE00\"}"; + assertEquals(json.toJson(fields), stringExpected); + assertEquals(new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), utf8Expected); + assertEquals(json.fromJson(stringExpected, PublicFields.class).name, fields.name); + } + + @Test + public void readStringScanBoundaries() { + ForyJson json = ForyJson.builder().build(); + for (int length : new int[] {7, 8, 15, 16, 23, 24}) { + String value = repeat('a', length); + String input = "\"" + value + "\""; + assertEquals(json.fromJson(input, String.class), value); + assertEquals(json.fromJson(input.getBytes(StandardCharsets.UTF_8), String.class), value); + } + String escapedValue = repeat('b', 16) + "\n"; + String escapedInput = "\"" + repeat('b', 16) + "\\n\""; + assertEquals(json.fromJson(escapedInput, String.class), escapedValue); + assertEquals( + json.fromJson(escapedInput.getBytes(StandardCharsets.UTF_8), String.class), escapedValue); + + String utf8Value = repeat('c', 16) + ZH_TEXT; + String utf8Input = "\"" + utf8Value + "\""; + assertEquals( + json.fromJson(utf8Input.getBytes(StandardCharsets.UTF_8), String.class), utf8Value); + + String rawControl = "\"" + repeat('d', 16) + "\u001f\""; + assertThrows(ForyJsonException.class, () -> json.fromJson(rawControl, String.class)); + assertThrows( + ForyJsonException.class, + () -> json.fromJson(rawControl.getBytes(StandardCharsets.UTF_8), String.class)); + byte[] invalidUtf8 = {'"', 'a', (byte) 0xC0, (byte) 0xAF, '"'}; + assertThrows(ForyJsonException.class, () -> json.fromJson(invalidUtf8, String.class)); + } + + @Test + public void rejectInvalidSurrogates() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.fromJson("\"\\uD834\\uDD1E\"", String.class), "\uD834\uDD1E"); + assertThrows(ForyJsonException.class, () -> json.fromJson("\"\\uD800\"", String.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("\"\\uDC00\"", String.class)); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("\"" + Character.toString('\uD800') + "\"", String.class)); + } + + private static int writerBufferLength(StringJsonWriter writer) throws Exception { + Field field = StringJsonWriter.class.getDeclaredField("buffer"); + field.setAccessible(true); + return ((byte[]) field.get(writer)).length; + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/BoxedScalars.java b/java/fory-json/src/test/java/org/apache/fory/json/data/BoxedScalars.java new file mode 100644 index 0000000000..603c304de3 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/BoxedScalars.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.json.data; + +public final class BoxedScalars { + public Boolean bool = true; + public Byte byteValue = Byte.valueOf((byte) 2); + public Character charValue = Character.valueOf('x'); + public Double doubleValue = Double.valueOf(2.5); + public Float floatValue = Float.valueOf(1.5f); + public Integer intValue = Integer.valueOf(4); + public Long longValue = Long.valueOf(5); + public Short shortValue = Short.valueOf((short) 3); +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/CharValue.java b/java/fory-json/src/test/java/org/apache/fory/json/data/CharValue.java new file mode 100644 index 0000000000..82632e5c3a --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/CharValue.java @@ -0,0 +1,24 @@ +/* + * 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.data; + +public final class CharValue { + public char value; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/ChildValue.java b/java/fory-json/src/test/java/org/apache/fory/json/data/ChildValue.java new file mode 100644 index 0000000000..b5962b7ddb --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/ChildValue.java @@ -0,0 +1,24 @@ +/* + * 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.data; + +public final class ChildValue extends ParentValue { + public int child = 2; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/CoreScalarFields.java b/java/fory-json/src/test/java/org/apache/fory/json/data/CoreScalarFields.java new file mode 100644 index 0000000000..7aee8e9ea7 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/CoreScalarFields.java @@ -0,0 +1,59 @@ +/* + * 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.data; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.util.Calendar; +import java.util.Currency; +import java.util.Locale; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public final class CoreScalarFields { + public AtomicInteger atomicInt = new AtomicInteger(7); + public BigDecimal bigDecimal = new BigDecimal("12345.6789"); + public BigInteger bigInteger = new BigInteger("12345678901234567890"); + public StringBuilder builder = new StringBuilder("build"); + public ByteBuffer bytes = ByteBuffer.wrap(new byte[] {1, -2, 3}); + public Calendar calendar = JsonTestData.calendar(123456789L); + public Charset charset = StandardCharsets.UTF_8; + public Currency currency = Currency.getInstance("EUR"); + public LocalDate date = LocalDate.of(2026, 6, 21); + public Instant instant = Instant.parse("2026-06-21T01:02:03Z"); + public Locale locale = Locale.forLanguageTag("zh-Hans-CN"); + public Optional maybe = Optional.of("yes"); + public OptionalInt optionalInt = OptionalInt.of(4); + public TimeZone timeZone = TimeZone.getTimeZone("UTC"); + public Class type = PublicFields.class; + public URI uri = URI.create("https://fory.apache.org/json"); + public URL url = JsonTestData.url("https://fory.apache.org/"); + public UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/DeclaredParentField.java b/java/fory-json/src/test/java/org/apache/fory/json/data/DeclaredParentField.java new file mode 100644 index 0000000000..6e3f938b41 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/DeclaredParentField.java @@ -0,0 +1,24 @@ +/* + * 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.data; + +public final class DeclaredParentField { + public ParentValue value = new ChildValue(); +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/DepthNode.java b/java/fory-json/src/test/java/org/apache/fory/json/data/DepthNode.java new file mode 100644 index 0000000000..064e97ba15 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/DepthNode.java @@ -0,0 +1,30 @@ +/* + * 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.data; + +import java.util.List; +import java.util.Map; + +public final class DepthNode { + public int value; + public DepthNode child; + public List children; + public Map nodes; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/DirectionalIgnore.java b/java/fory-json/src/test/java/org/apache/fory/json/data/DirectionalIgnore.java new file mode 100644 index 0000000000..43ce16a835 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/DirectionalIgnore.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.json.data; + +import org.apache.fory.json.annotation.JsonIgnore; + +public final class DirectionalIgnore { + @JsonIgnore public int both = 1; + + @JsonIgnore(ignoreRead = true, ignoreWrite = false) + public int writeOnly = 2; + + @JsonIgnore(ignoreRead = false, ignoreWrite = true) + public int readOnly = 3; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/FastContainers.java b/java/fory-json/src/test/java/org/apache/fory/json/data/FastContainers.java new file mode 100644 index 0000000000..af06c1433b --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/FastContainers.java @@ -0,0 +1,33 @@ +/* + * 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.data; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public final class FastContainers { + public List booleans = Arrays.asList(Boolean.TRUE, Boolean.FALSE); + public Map flags = JsonTestData.flags(); + public Map intNames = JsonTestData.intNames(); + public List ints = Arrays.asList(1, 2); + public List names = Arrays.asList("alpha", JsonTestData.ZH_TEXT); + public Map scores = JsonTestData.scores(); +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/FirstIntField.java b/java/fory-json/src/test/java/org/apache/fory/json/data/FirstIntField.java new file mode 100644 index 0000000000..82ff20fe2e --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/FirstIntField.java @@ -0,0 +1,25 @@ +/* + * 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.data; + +public final class FirstIntField { + public int count = 2; + public String name = "first"; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/GeneratedCollectionFields.java b/java/fory-json/src/test/java/org/apache/fory/json/data/GeneratedCollectionFields.java new file mode 100644 index 0000000000..d7171d3159 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/GeneratedCollectionFields.java @@ -0,0 +1,33 @@ +/* + * 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.data; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.Queue; +import java.util.Set; + +public final class GeneratedCollectionFields { + public EnumSet kinds = EnumSet.of(Kind.FAST, Kind.SMALL); + public Set names = new LinkedHashSet<>(Arrays.asList("alpha", JsonTestData.ZH_TEXT)); + public Queue numbers = new ArrayDeque<>(Arrays.asList(1, 2)); +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/JsonTestData.java b/java/fory-json/src/test/java/org/apache/fory/json/data/JsonTestData.java new file mode 100644 index 0000000000..ebf1826ac0 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/JsonTestData.java @@ -0,0 +1,100 @@ +/* + * 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.data; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Calendar; +import java.util.EnumMap; +import java.util.GregorianCalendar; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TimeZone; + +public final class JsonTestData { + public static final String TWO_BYTE_TEXT = "\u0100\u07ff\u03a9"; + public static final String THREE_BYTE_TEXT = "\u0800\u20ac\u4f60\ud7ff\ue000"; + public static final String SUPPLEMENTARY_TEXT = "\uD834\uDD1E\uD83D\uDE00\uD83C\uDF0D"; + public static final String MIXED_SCRIPT_TEXT = "\u0100\u03a9\u0416\u05d0\u0627\u0905\u0e01\u4f60"; + public static final String COMBINING_TEXT = "e\u0301\u200d\uD83D\uDCBB"; + public static final String ZH_TEXT = "你好,Fory"; + public static final String EU_TEXT = "café crème Österreich € ČšŽ"; + + private JsonTestData() {} + + public static Map scores() { + Map scores = new LinkedHashMap<>(); + scores.put("one", 1); + scores.put("two", 2); + return scores; + } + + public static Map flags() { + Map flags = new LinkedHashMap<>(); + flags.put("enabled", Boolean.TRUE); + flags.put("disabled", Boolean.FALSE); + return flags; + } + + public static Map intNames() { + Map values = new LinkedHashMap<>(); + values.put(1, "one"); + values.put(2, "two"); + return values; + } + + public static EnumMap enumScores() { + EnumMap values = new EnumMap<>(Kind.class); + values.put(Kind.FAST, 1); + values.put(Kind.SMALL, 2); + return values; + } + + public static Calendar calendar(long millis) { + Calendar calendar = new GregorianCalendar(); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + calendar.setTimeInMillis(millis); + return calendar; + } + + public static URL url(String value) { + try { + return new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + public static Map unicodeMap() { + Map values = new LinkedHashMap<>(); + values.put(TWO_BYTE_TEXT, THREE_BYTE_TEXT); + values.put(ZH_TEXT, EU_TEXT); + values.put("\u043a\u043b\u044e\u0447", "\uD83D\uDE00"); + values.put("\u0645\u0631\u062d\u0628\u0627", "\u0928\u092e\u0938\u094d\u0924\u0947"); + return values; + } + + public static Map values() { + Map values = new LinkedHashMap<>(); + values.put("name", "fory"); + values.put("score", 9); + return values; + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/Kind.java b/java/fory-json/src/test/java/org/apache/fory/json/data/Kind.java new file mode 100644 index 0000000000..b864b64525 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/Kind.java @@ -0,0 +1,25 @@ +/* + * 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.data; + +public enum Kind { + FAST, + SMALL +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/MapKeyFields.java b/java/fory-json/src/test/java/org/apache/fory/json/data/MapKeyFields.java new file mode 100644 index 0000000000..567caf36d7 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/MapKeyFields.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.data; + +import java.util.EnumMap; +import java.util.Map; + +public final class MapKeyFields { + public Map intNames = JsonTestData.intNames(); + public EnumMap scores = JsonTestData.enumScores(); +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/MethodsIgnored.java b/java/fory-json/src/test/java/org/apache/fory/json/data/MethodsIgnored.java new file mode 100644 index 0000000000..04a3589a18 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/MethodsIgnored.java @@ -0,0 +1,43 @@ +/* + * 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.data; + +public final class MethodsIgnored { + public int setterCalls; + public String value = "field"; + private String hidden = "hidden"; + + public String getHidden() { + return hidden; + } + + public String getValue() { + return "getter"; + } + + public void setValue(String value) { + setterCalls++; + this.value = "setter:" + value; + } + + public static String hiddenValue(MethodsIgnored value) { + return value.hidden; + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/NaturalObjectValue.java b/java/fory-json/src/test/java/org/apache/fory/json/data/NaturalObjectValue.java new file mode 100644 index 0000000000..ab19608e60 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/NaturalObjectValue.java @@ -0,0 +1,24 @@ +/* + * 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.data; + +public final class NaturalObjectValue { + public Object value = new Object(); +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/NaturalValues.java b/java/fory-json/src/test/java/org/apache/fory/json/data/NaturalValues.java new file mode 100644 index 0000000000..f40a276235 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/NaturalValues.java @@ -0,0 +1,30 @@ +/* + * 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.data; + +import java.util.Arrays; + +public final class NaturalValues { + public Object bool = Boolean.TRUE; + public Object list = Arrays.asList("a", Integer.valueOf(1), Boolean.FALSE); + public Object map = JsonTestData.values(); + public Object number = Integer.valueOf(7); + public Object text = "fory"; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/Nested.java b/java/fory-json/src/test/java/org/apache/fory/json/data/Nested.java new file mode 100644 index 0000000000..3e768c9f0b --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/Nested.java @@ -0,0 +1,30 @@ +/* + * 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.data; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public final class Nested { + public Kind kind = Kind.FAST; + public List names = Arrays.asList("a", "b"); + public Map scores = JsonTestData.scores(); +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/NumericBoundaries.java b/java/fory-json/src/test/java/org/apache/fory/json/data/NumericBoundaries.java new file mode 100644 index 0000000000..ca2c30f93b --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/NumericBoundaries.java @@ -0,0 +1,29 @@ +/* + * 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.data; + +public final class NumericBoundaries { + public int intMax; + public int intMin; + public long longMax; + public long longMin; + public int small; + public String text; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/ParentValue.java b/java/fory-json/src/test/java/org/apache/fory/json/data/ParentValue.java new file mode 100644 index 0000000000..fa8e544e2e --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/ParentValue.java @@ -0,0 +1,24 @@ +/* + * 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.data; + +public class ParentValue { + public int parent = 1; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/PrivateFields.java b/java/fory-json/src/test/java/org/apache/fory/json/data/PrivateFields.java new file mode 100644 index 0000000000..6540ea816b --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/PrivateFields.java @@ -0,0 +1,48 @@ +/* + * 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.data; + +public final class PrivateFields { + private static String staticValue = "static"; + private transient String transientValue = "transient"; + private int id = 11; + private String name = "private"; + private String nullable; + + public static int id(PrivateFields value) { + return value.id; + } + + public static String name(PrivateFields value) { + return value.name; + } + + public static String nullable(PrivateFields value) { + return value.nullable; + } + + public static String transientValue(PrivateFields value) { + return value.transientValue; + } + + public static String staticValue() { + return staticValue; + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/PublicFields.java b/java/fory-json/src/test/java/org/apache/fory/json/data/PublicFields.java new file mode 100644 index 0000000000..3e0bbd9644 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/PublicFields.java @@ -0,0 +1,27 @@ +/* + * 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.data; + +public final class PublicFields { + public boolean active = true; + public int id = 7; + public String name = "fory"; + public String missing; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/RecursiveChild.java b/java/fory-json/src/test/java/org/apache/fory/json/data/RecursiveChild.java new file mode 100644 index 0000000000..75799f54bd --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/RecursiveChild.java @@ -0,0 +1,25 @@ +/* + * 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.data; + +public final class RecursiveChild { + public String name = "child"; + public RecursiveParent parent; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/RecursiveParent.java b/java/fory-json/src/test/java/org/apache/fory/json/data/RecursiveParent.java new file mode 100644 index 0000000000..aebf7202e1 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/RecursiveParent.java @@ -0,0 +1,25 @@ +/* + * 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.data; + +public final class RecursiveParent { + public RecursiveChild child = new RecursiveChild(); + public String name = "parent"; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/TokenGroup.java b/java/fory-json/src/test/java/org/apache/fory/json/data/TokenGroup.java new file mode 100644 index 0000000000..e50760d264 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/TokenGroup.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.json.data; + +import java.util.List; + +public final class TokenGroup { + public List values; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/TokenValues.java b/java/fory-json/src/test/java/org/apache/fory/json/data/TokenValues.java new file mode 100644 index 0000000000..d0eb59f69c --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/TokenValues.java @@ -0,0 +1,30 @@ +/* + * 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.data; + +import java.util.Arrays; +import java.util.List; + +public final class TokenValues { + public int count = 1; + public String name = "alpha"; + public List tags = Arrays.asList("x", "y"); + public long total = 2; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeEnumValue.java b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeEnumValue.java new file mode 100644 index 0000000000..2b7589a9ca --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeEnumValue.java @@ -0,0 +1,24 @@ +/* + * 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.data; + +public final class UnicodeEnumValue { + public UnicodeKind kind = UnicodeKind.你好; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeFieldNames.java b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeFieldNames.java new file mode 100644 index 0000000000..357af45f40 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeFieldNames.java @@ -0,0 +1,25 @@ +/* + * 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.data; + +public final class UnicodeFieldNames { + public String café = JsonTestData.EU_TEXT; + public String 你好 = JsonTestData.ZH_TEXT; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeKind.java b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeKind.java new file mode 100644 index 0000000000..cdfb4342cc --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeKind.java @@ -0,0 +1,24 @@ +/* + * 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.data; + +public enum UnicodeKind { + 你好 +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeMatrix.java b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeMatrix.java new file mode 100644 index 0000000000..881c626d1c --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeMatrix.java @@ -0,0 +1,48 @@ +/* + * 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.data; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public final class UnicodeMatrix { + public Character boxedChar = Character.valueOf('\u20ac'); + public char charThreeByte = '\u4f60'; + public char charTwoByte = '\u0100'; + public char[] chars = {'\u0100', '\u07ff', '\u0800', '\u20ac', '\u4f60'}; + public String combining = JsonTestData.COMBINING_TEXT; + public String eu = JsonTestData.EU_TEXT; + public String mixedScripts = JsonTestData.MIXED_SCRIPT_TEXT; + public String supplementary = JsonTestData.SUPPLEMENTARY_TEXT; + public String threeByte = JsonTestData.THREE_BYTE_TEXT; + public String twoByte = JsonTestData.TWO_BYTE_TEXT; + public Map valueMap = JsonTestData.unicodeMap(); + public List values = + Arrays.asList( + JsonTestData.TWO_BYTE_TEXT, + JsonTestData.THREE_BYTE_TEXT, + JsonTestData.SUPPLEMENTARY_TEXT, + JsonTestData.MIXED_SCRIPT_TEXT, + JsonTestData.COMBINING_TEXT, + JsonTestData.ZH_TEXT, + JsonTestData.EU_TEXT); + public String zh = JsonTestData.ZH_TEXT; +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeValues.java b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeValues.java new file mode 100644 index 0000000000..df60d634be --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/UnicodeValues.java @@ -0,0 +1,29 @@ +/* + * 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.data; + +import java.util.Arrays; +import java.util.List; + +public final class UnicodeValues { + public String first = "\u1234"; + public String second = "music \uD834\uDD1E"; + public List tags = Arrays.asList("latin", "\u1234", "\uD83D\uDE00"); +} diff --git a/java/pom.xml b/java/pom.xml index 0feb8c8363..1be1d25b69 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -60,6 +60,7 @@ fory-core + fory-json fory-annotation-processor fory-extensions fory-test-core @@ -101,9 +102,9 @@ - [25,26) + [25,) - Apache Fory JVM releases must run with JDK 25 so JDK25 multi-release classes are packaged. + Apache Fory JVM releases must run with JDK 25+ so JDK25 multi-release classes are packaged.