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 index 8fc4662da3..a5a0b43bad 100644 --- 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 @@ -19,7 +19,9 @@ package org.apache.fory.json; +import java.io.OutputStream; import java.lang.reflect.Type; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; import org.apache.fory.json.codec.GeneratedObjectCodec; @@ -55,10 +57,16 @@ public final class ForyJson { private final AtomicReferenceArray slots; ForyJson( - boolean writeNullFields, boolean codegenEnabled, int maxDepth, CodecRegistry codecRegistry) { + boolean writeNullFields, + boolean codegenEnabled, + boolean propertyDiscoveryEnabled, + int maxDepth, + CodecRegistry codecRegistry) { this.writeNullFields = writeNullFields; this.maxDepth = maxDepth; - sharedRegistry = new JsonSharedRegistry(codegenEnabled, writeNullFields, codecRegistry); + sharedRegistry = + new JsonSharedRegistry( + codegenEnabled, writeNullFields, propertyDiscoveryEnabled, codecRegistry); poolSize = DEFAULT_POOL_SIZE; primarySlot = new AtomicReference<>( @@ -111,6 +119,27 @@ public byte[] toJsonBytes(Object value) { } } + /** Serializes {@code value} as UTF-8 JSON to {@code output}. */ + public void writeJsonTo(Object value, OutputStream output) { + Objects.requireNonNull(output, "output"); + 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); + } + writer.writeTo(output); + } finally { + writer.reset(); + release(entry); + } + } + public T fromJson(String json, Class type) { PooledState entry = acquire(); JsonState state = entry.state; 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 index 17a5cb6c53..b0ed1f58b5 100644 --- 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 @@ -26,6 +26,7 @@ public final class ForyJsonBuilder { private boolean writeNullFields; private boolean codegenEnabled = true; + private boolean propertyDiscoveryEnabled = true; private int maxDepth = ForyJson.DEFAULT_MAX_DEPTH; private final CodecRegistry codecRegistry = new CodecRegistry(); @@ -43,6 +44,12 @@ public ForyJsonBuilder withCodegen(boolean codegenEnabled) { return this; } + /** Enables JavaBean getter/setter JSON property discovery. */ + public ForyJsonBuilder withPropertyDiscovery(boolean propertyDiscoveryEnabled) { + this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; + return this; + } + /** Sets the maximum nested JSON object/array depth allowed while parsing. */ public ForyJsonBuilder maxDepth(int maxDepth) { if (maxDepth < 1) { @@ -59,6 +66,7 @@ public ForyJsonBuilder registerCodec(Class type, JsonCodec codec) { } public ForyJson build() { - return new ForyJson(writeNullFields, codegenEnabled, maxDepth, codecRegistry); + return new ForyJson( + writeNullFields, codegenEnabled, propertyDiscoveryEnabled, maxDepth, codecRegistry); } } 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 index 9724abbd92..aff3817589 100644 --- 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 @@ -20,9 +20,7 @@ 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; @@ -58,6 +56,24 @@ public static ArrayCodec create(Class componentType, JsonTypeResolver resolve return FloatArrayCodec.INSTANCE; } else if (componentType == double.class) { return DoubleArrayCodec.INSTANCE; + } else if (componentType == Integer.class) { + return BoxedIntArrayCodec.INSTANCE; + } else if (componentType == Long.class) { + return BoxedLongArrayCodec.INSTANCE; + } else if (componentType == Boolean.class) { + return BoxedBooleanArrayCodec.INSTANCE; + } else if (componentType == Short.class) { + return BoxedShortArrayCodec.INSTANCE; + } else if (componentType == Byte.class) { + return BoxedByteArrayCodec.INSTANCE; + } else if (componentType == Character.class) { + return BoxedCharArrayCodec.INSTANCE; + } else if (componentType == Float.class) { + return BoxedFloatArrayCodec.INSTANCE; + } else if (componentType == Double.class) { + return BoxedDoubleArrayCodec.INSTANCE; + } else if (componentType == String.class) { + return StringArrayCodec.INSTANCE; } return new ObjectArrayCodec(componentType, resolver.getTypeInfo(componentType, componentType)); } @@ -295,8 +311,91 @@ public Object readUtf8( reader.exitDepth(); return new long[0]; } - long[] values = new long[8]; - int size = 0; + rejectNull(reader); + long v0 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0}; + } + rejectNull(reader); + long v1 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1}; + } + rejectNull(reader); + long v2 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2}; + } + rejectNull(reader); + long v3 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3}; + } + return readUtf8Tail(reader, v0, v1, v2, v3); + } + + private long[] readUtf8Tail(Utf8JsonReader reader, long v0, long v1, long v2, long v3) { + rejectNull(reader); + long v4 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4}; + } + rejectNull(reader); + long v5 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5}; + } + rejectNull(reader); + long v6 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6}; + } + rejectNull(reader); + long v7 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + return readUtf8LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7); + } + + // Keep dynamic growth out of the exact small-array path so C2 can inline the common cases + // without pulling the uncommon grow/copy loop into the caller's inline budget. + private long[] readUtf8LongTail( + Utf8JsonReader reader, + long v0, + long v1, + long v2, + long v3, + long v4, + long v5, + long v6, + long v7) { + long[] values = new long[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + int size = 8; do { rejectNull(reader); if (size == values.length) { @@ -634,9 +733,258 @@ Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver re } } + public static final class StringArrayCodec extends ArrayCodec { + private static final StringArrayCodec INSTANCE = new StringArrayCodec(); + + private StringArrayCodec() { + super(String.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + String[] array = (String[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + if (array[i] == null) { + writer.writeNull(); + } else { + writer.writeString(array[i]); + } + } + writer.writeArrayEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + String[] array = (String[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeStringElement(i, array[i]); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + String[] array = (String[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeStringElement(i, array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNullableString(); + } 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 String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNextNullableString(); + } 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 String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNextNullableString(); + } 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 String[0]; + } + String v0 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0}; + } + String v1 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1}; + } + String v2 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2}; + } + String v3 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3}; + } + return readUtf8Tail(reader, v0, v1, v2, v3); + } + + private String[] readUtf8Tail( + Utf8JsonReader reader, String v0, String v1, String v2, String v3) { + String v4 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return stringArray5(v0, v1, v2, v3, v4); + } + String v5 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return stringArray6(v0, v1, v2, v3, v4, v5); + } + String v6 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return stringArray7(v0, v1, v2, v3, v4, v5, v6); + } + String v7 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return stringArray8(v0, v1, v2, v3, v4, v5, v6, v7); + } + String v8 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7, v8}; + } + return readUtf8LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7, v8); + } + + // Keep dynamic growth out of the exact small-array path so C2 can inline the common cases + // without pulling the uncommon grow/copy loop into the caller's inline budget. + private String[] readUtf8LongTail( + Utf8JsonReader reader, + String v0, + String v1, + String v2, + String v3, + String v4, + String v5, + String v6, + String v7, + String v8) { + String[] values = new String[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + values[8] = v8; + int size = 9; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNextNullableString(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + private static String[] stringArray5(String v0, String v1, String v2, String v3, String v4) { + return new String[] {v0, v1, v2, v3, v4}; + } + + private static String[] stringArray6( + String v0, String v1, String v2, String v3, String v4, String v5) { + return new String[] {v0, v1, v2, v3, v4, v5}; + } + + private static String[] stringArray7( + String v0, String v1, String v2, String v3, String v4, String v5, String v6) { + return new String[] {v0, v1, v2, v3, v4, v5, v6}; + } + + private static String[] stringArray8( + String v0, String v1, String v2, String v3, String v4, String v5, String v6, String v7) { + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + } + public static final class ObjectArrayCodec extends ArrayCodec { + private static final int VALUES_CACHE_DEPTH = 8; + private static final int INITIAL_VALUES_SIZE = 8; + private static final int MAX_CACHED_VALUES_SIZE = 1024; + private final JsonTypeInfo elementTypeInfo; private final JsonCodec elementCodec; + // Recursive object-array reads borrow one scratch slot per active depth. + private final Object[][] valuesCache = new Object[VALUES_CACHE_DEPTH][]; + private int valuesDepth; private ObjectArrayCodec(Class componentType, JsonTypeInfo elementTypeInfo) { super(componentType); @@ -680,24 +1028,410 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso @Override Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { reader.enterDepth(); - List values = new ArrayList<>(0); + int depth = valuesDepth; + boolean useCache = depth < VALUES_CACHE_DEPTH; + Object[] values = null; + if (useCache) { + values = valuesCache[depth]; + valuesCache[depth] = null; + } + if (values == null) { + values = new Object[INITIAL_VALUES_SIZE]; + } + int size = 0; + boolean success = false; + valuesDepth = depth + 1; + try { + reader.expect('['); + if (!reader.consume(']')) { + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = elementCodec.read(reader, elementTypeInfo, resolver); + } while (reader.consume(',')); + reader.expect(']'); + } + Object[] array = (Object[]) Array.newInstance(componentType, size); + System.arraycopy(values, 0, array, 0, size); + success = true; + return array; + } finally { + reader.exitDepth(); + // Failed reads drop the scratch array, because it may contain partially parsed user values. + if (success && useCache) { + if (values.length <= MAX_CACHED_VALUES_SIZE) { + Arrays.fill(values, 0, size, null); + valuesCache[depth] = values; + } else { + // Keep the depth slot usable without retaining a grown array from one large value. + valuesCache[depth] = new Object[INITIAL_VALUES_SIZE]; + } + } + // Restore the codec recursion depth after the matching cache slot has been handled. + valuesDepth = depth; + } + } + } + + public static final class BoxedIntArrayCodec extends ArrayCodec { + private static final BoxedIntArrayCodec INSTANCE = new BoxedIntArrayCodec(); + + private BoxedIntArrayCodec() { + super(Integer.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Integer[] array = (Integer[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Integer element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeInt(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); reader.expect('['); - if (!reader.consume(']')) { - do { - values.add(elementCodec.read(reader, elementTypeInfo, resolver)); - } while (reader.consume(',')); - reader.expect(']'); + if (reader.consume(']')) { + reader.exitDepth(); + return new Integer[0]; } + Integer[] values = new Integer[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : reader.readInt(); + } while (reader.consume(',')); + reader.expect(']'); reader.exitDepth(); - return toArray(values); + return Arrays.copyOf(values, size); } + } + + public static final class BoxedLongArrayCodec extends ArrayCodec { + private static final BoxedLongArrayCodec INSTANCE = new BoxedLongArrayCodec(); - 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)); + private BoxedLongArrayCodec() { + 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); + Long element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeLong(element); + } } - return array; + 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 { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : reader.readLong(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedBooleanArrayCodec extends ArrayCodec { + private static final BoxedBooleanArrayCodec INSTANCE = new BoxedBooleanArrayCodec(); + + private BoxedBooleanArrayCodec() { + 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); + Boolean element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeBoolean(element); + } + } + 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 { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : reader.readBoolean(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedShortArrayCodec extends ArrayCodec { + private static final BoxedShortArrayCodec INSTANCE = new BoxedShortArrayCodec(); + + private BoxedShortArrayCodec() { + 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); + Short element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeInt(element); + } + } + 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 { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : readShort(reader.readInt()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedByteArrayCodec extends ArrayCodec { + private static final BoxedByteArrayCodec INSTANCE = new BoxedByteArrayCodec(); + + private BoxedByteArrayCodec() { + 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); + Byte element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeInt(element); + } + } + 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 { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : readByte(reader.readInt()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedCharArrayCodec extends ArrayCodec { + private static final BoxedCharArrayCodec INSTANCE = new BoxedCharArrayCodec(); + + private BoxedCharArrayCodec() { + super(Character.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Character[] array = (Character[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Character element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeChar(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Character[0]; + } + Character[] values = new Character[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + String value = reader.readNullableString(); + values[size++] = value == null ? null : readChar(value); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedFloatArrayCodec extends ArrayCodec { + private static final BoxedFloatArrayCodec INSTANCE = new BoxedFloatArrayCodec(); + + private BoxedFloatArrayCodec() { + 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); + Float element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeFloat(element); + } + } + 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 { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : Float.parseFloat(reader.readNumber()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedDoubleArrayCodec extends ArrayCodec { + private static final BoxedDoubleArrayCodec INSTANCE = new BoxedDoubleArrayCodec(); + + private BoxedDoubleArrayCodec() { + 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); + Double element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeDouble(element); + } + } + 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 { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : Double.parseDouble(reader.readNumber()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); } } @@ -740,7 +1474,10 @@ private static byte readByte(int value) { } private static char readChar(JsonReader reader) { - String value = reader.readString(); + return readChar(reader.readString()); + } + + private static char readChar(String value) { if (value.length() != 1) { throw new ForyJsonException("Expected one-character JSON string for char"); } 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 index 3566caa8bc..631141b2d1 100644 --- 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 @@ -20,12 +20,14 @@ package org.apache.fory.json.codec; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; 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; @@ -81,7 +83,7 @@ record = RecordUtils.isRecord(type); } } - public static ObjectCodec build(Class type) { + public static ObjectCodec build(Class type, boolean propertyDiscoveryEnabled) { if (type.isInterface() || Modifier.isAbstract(type.getModifiers()) || type.isPrimitive() @@ -92,40 +94,22 @@ public static ObjectCodec build(Class 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()); - } - } + LinkedHashMap builders = new LinkedHashMap<>(); + addFields(type, record, writeExpose, readExpose, propertyDiscoveryEnabled, builders); + if (propertyDiscoveryEnabled && !record) { + addAccessors(type, writeExpose, readExpose, builders); } List writes = new ArrayList<>(); List reads = new ArrayList<>(); for (FieldBuilder builder : builders.values()) { + if (!builder.hasWriteSource() && !builder.hasReadSink()) { + continue; + } JsonFieldInfo field = builder.build(record); - if (builder.writeAccessor != null) { + if (builder.hasWriteSource()) { writes.add(field); } - if (builder.readField != null) { + if (builder.hasReadSink()) { reads.add(field); } } @@ -138,6 +122,76 @@ boolean record = RecordUtils.isRecord(type); type, writeArray, readArray, ObjectInstantiators.createObjectInstantiator(type)); } + private static void addFields( + Class type, + boolean record, + boolean writeExpose, + boolean readExpose, + boolean propertyDiscoveryEnabled, + LinkedHashMap builders) { + List> hierarchy = new ArrayList<>(); + for (Class current = type; + current != null && current != Object.class; + current = current.getSuperclass()) { + hierarchy.add(current); + } + for (int i = hierarchy.size() - 1; i >= 0; i--) { + Class current = hierarchy.get(i); + for (Field field : current.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (!isEligibleField(field)) { + continue; + } + boolean write = includeWrite(field, writeExpose); + boolean readAllowed = includeRead(field, readExpose); + boolean read = (record || !Modifier.isFinal(modifiers)) && readAllowed; + if (!propertyDiscoveryEnabled && !write && !read) { + continue; + } + FieldBuilder builder = + builders.computeIfAbsent(field.getName(), name -> new FieldBuilder(name)); + builder.setField(field, write, read, write, readAllowed); + } + } + } + + private static void addAccessors( + Class type, + boolean writeExpose, + boolean readExpose, + LinkedHashMap builders) { + for (Method method : type.getMethods()) { + if (!isEligibleAccessor(method)) { + continue; + } + String propertyName = getterPropertyName(method); + if (propertyName != null) { + FieldBuilder builder = builders.get(propertyName); + if (builder == null) { + if (writeExpose) { + continue; + } + builder = new FieldBuilder(propertyName); + builders.put(propertyName, builder); + } + builder.setWriteGetter(method, writeExpose); + continue; + } + propertyName = setterPropertyName(method); + if (propertyName != null) { + FieldBuilder builder = builders.get(propertyName); + if (builder == null) { + if (readExpose) { + continue; + } + builder = new FieldBuilder(propertyName); + builders.put(propertyName, builder); + } + builder.setReadSetter(method, readExpose); + } + } + } + public final Class type() { return type; } @@ -384,6 +438,53 @@ private static boolean isEligibleField(Field field) { && !field.isSynthetic(); } + private static boolean isEligibleAccessor(Method method) { + int modifiers = method.getModifiers(); + return Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && !method.isSynthetic() + && !method.isBridge(); + } + + private static String getterPropertyName(Method method) { + if (method.getParameterCount() != 0 || method.getReturnType() == void.class) { + return null; + } + String name = method.getName(); + if (name.equals("getClass")) { + return null; + } + if (name.length() > 3 && name.startsWith("get")) { + return decapitalize(name.substring(3)); + } + if (name.length() > 2 + && name.startsWith("is") + && (method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class)) { + return decapitalize(name.substring(2)); + } + return null; + } + + private static String setterPropertyName(Method method) { + if (method.getParameterCount() != 1 || method.getReturnType() != void.class) { + return null; + } + String name = method.getName(); + if (name.length() > 3 && name.startsWith("set")) { + return decapitalize(name.substring(3)); + } + return null; + } + + private static String decapitalize(String name) { + if (name.length() > 1 + && Character.isUpperCase(name.charAt(0)) + && Character.isUpperCase(name.charAt(1))) { + return name; + } + return Character.toLowerCase(name.charAt(0)) + name.substring(1); + } + private static boolean includeWrite(Field field, boolean exposeMode) { return include(field, exposeMode, true); } @@ -419,8 +520,13 @@ private static Object[] recordFieldDefaults( private static final class FieldBuilder { private final String name; + private Field field; + private boolean fieldWriteAllowed; + private boolean fieldReadAllowed; private Field writeField; private Field readField; + private Method writeGetter; + private Method readSetter; private JsonFieldAccessor writeAccessor; private JsonFieldAccessor readAccessor; @@ -428,24 +534,88 @@ private FieldBuilder(String name) { this.name = name; } - private void setWriteField(Field field) { - if (writeField != null) { - throw new ForyJsonException("Duplicate public JSON field " + name); + private void setField( + Field field, + boolean writeSource, + boolean readSink, + boolean writeAllowed, + boolean readAllowed) { + if (this.field != null) { + throw new ForyJsonException("Duplicate JSON field " + name); + } + this.field = field; + fieldWriteAllowed = writeAllowed; + fieldReadAllowed = readAllowed; + if (writeSource) { + writeField = field; + } + if (readSink) { + readField = field; } - writeField = field; } - private void setReadField(Field field) { - if (readField != null) { - throw new ForyJsonException("Duplicate public JSON field " + name); + private void setWriteGetter(Method getter, boolean exposeMode) { + if (!methodAllowed(exposeMode, fieldWriteAllowed)) { + return; + } + if (writeGetter != null) { + throw new ForyJsonException("Duplicate JSON getter for property " + name); } - readField = field; + writeGetter = getter; + writeField = null; + } + + private void setReadSetter(Method setter, boolean exposeMode) { + if (!methodAllowed(exposeMode, fieldReadAllowed)) { + return; + } + if (readSetter != null) { + throw new ForyJsonException("Duplicate JSON setter for property " + name); + } + readSetter = setter; + readField = null; + } + + private boolean hasWriteSource() { + return writeGetter != null || writeField != null; + } + + private boolean hasReadSink() { + return readSetter != null || readField != null; } 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); + validateTypes(); + writeAccessor = + writeGetter != null + ? JsonFieldAccessor.forGetter(writeGetter) + : (writeField == null ? null : JsonFieldAccessor.forField(writeField)); + readAccessor = + readSetter != null + ? JsonFieldAccessor.forSetter(readSetter) + : (readField == null || record ? null : JsonFieldAccessor.forField(readField)); + return new JsonFieldInfo( + name, writeField, writeGetter, readField, readSetter, writeAccessor, readAccessor); + } + + private boolean methodAllowed(boolean exposeMode, boolean fieldAllowed) { + // A same-named field owns @Expose/@JsonIgnore direction decisions for the JSON property. + return field == null ? !exposeMode : fieldAllowed; + } + + private void validateTypes() { + Type writeType = + writeGetter == null ? fieldType(writeField) : writeGetter.getGenericReturnType(); + Type readType = + readSetter == null ? fieldType(readField) : readSetter.getGenericParameterTypes()[0]; + if (writeType != null && readType != null && !writeType.equals(readType)) { + throw new ForyJsonException( + "Conflicting JSON property types for " + name + ": " + writeType + " and " + readType); + } + } + + private static Type fieldType(Field field) { + return field == null ? null : field.getGenericType(); } } } 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 index dfd70c8346..a0af5b054a 100644 --- 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 @@ -50,10 +50,12 @@ public abstract class CollectionCodec extends AbstractJsonCodec { private final TypeRef typeRef; private final CollectionFactory factory; + private final boolean createsArrayList; CollectionCodec(TypeRef typeRef, CollectionFactory factory) { this.typeRef = typeRef; this.factory = factory; + this.createsArrayList = factory.createsArrayList(); } public static CollectionCodec create( @@ -118,6 +120,10 @@ final Collection newCollection() { return factory.newCollection(); } + final boolean createsArrayList() { + return createsArrayList; + } + public abstract Object readLatin1NonNull( Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver); @@ -166,7 +172,7 @@ private static CollectionFactory collectionFactory(Class rawType, Class el if (Queue.class.isAssignableFrom(rawType)) { return ArrayDeque::new; } - return () -> new ArrayList<>(0); + return CollectionFactory.ARRAY_LIST; } return () -> { try { @@ -178,7 +184,24 @@ private static CollectionFactory collectionFactory(Class rawType, Class el } private interface CollectionFactory { + CollectionFactory ARRAY_LIST = + new CollectionFactory() { + @Override + public Collection newCollection() { + return new ArrayList<>(0); + } + + @Override + public boolean createsArrayList() { + return true; + } + }; + Collection newCollection(); + + default boolean createsArrayList() { + return false; + } } public abstract static class DirectCollectionCodec extends CollectionCodec { @@ -260,6 +283,9 @@ public final Object readUtf8( @Override public final Object readUtf8NonNull( Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readUtf8ArrayListNonNull(reader); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -272,6 +298,123 @@ public final Object readUtf8NonNull( return collection; } + private ArrayList readUtf8ArrayListNonNull(Utf8JsonReader reader) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + return readUtf8ArrayListTail(reader, e0, e1, e2, e3); + } + + private ArrayList readUtf8ArrayListTail( + Utf8JsonReader reader, Object e0, Object e1, Object e2, Object e3) { + Object e4 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + return readUtf8ArrayListLongTail(reader, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readUtf8ArrayListLongTail( + Utf8JsonReader reader, Object e0, Object e1, Object e2, Object e3, Object e4, Object e5) { + Object e6 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableUtf8Element(reader)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + abstract Object readElement(JsonReader reader); Object readNullableElement(JsonReader reader) { @@ -447,13 +590,26 @@ private ObjectCollectionCodec( @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); + if (value.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) value; + for (int index = 0, size = list.size(); index < size; index++) { + Object element = list.get(index); + writer.writeComma(index); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeNonNull(writer, element, resolver); + } + } + } else { + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeNonNull(writer, element, resolver); + } } } writer.writeArrayEnd(); @@ -462,13 +618,26 @@ void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { @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); + if (value.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) value; + for (int index = 0, size = list.size(); index < size; index++) { + Object element = list.get(index); + writer.writeComma(index); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeStringNonNull(writer, element, resolver); + } + } + } else { + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeStringNonNull(writer, element, resolver); + } } } writer.writeArrayEnd(); @@ -477,13 +646,26 @@ void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver @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); + if (value.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) value; + for (int index = 0, size = list.size(); index < size; index++) { + Object element = list.get(index); + writer.writeComma(index); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeUtf8NonNull(writer, element, resolver); + } + } + } else { + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeUtf8NonNull(writer, element, resolver); + } } } writer.writeArrayEnd(); @@ -572,6 +754,9 @@ public Object readUtf8( @Override public Object readUtf8NonNull( Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readUtf8ArrayListNonNull(reader, resolver); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -586,6 +771,142 @@ public Object readUtf8NonNull( reader.exitDepth(); return collection; } + + private ArrayList readUtf8ArrayListNonNull( + Utf8JsonReader reader, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + return readUtf8ArrayListTail(reader, resolver, e0, e1, e2, e3); + } + + private ArrayList readUtf8ArrayListTail( + Utf8JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3) { + Object e4 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + return readUtf8ArrayListLongTail(reader, resolver, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readUtf8ArrayListLongTail( + Utf8JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3, + Object e4, + Object e5) { + Object e6 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableUtf8Element(reader, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + + private Object readNullableUtf8Element(Utf8JsonReader reader, JsonTypeResolver resolver) { + return reader.tryReadNextNullToken() + ? null + : elementCodec.readUtf8NonNull(reader, elementTypeInfo, resolver); + } } public static final class StringCollectionCodec extends DirectCollectionCodec { 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 index 2b58fc72c2..7902bf7134 100644 --- 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 @@ -56,8 +56,11 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Pattern; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.meta.JsonAsciiToken; @@ -438,7 +441,7 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso @Override Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - return Double.parseDouble(reader.readNumber()); + return reader.readDouble(); } @Override @@ -451,9 +454,9 @@ public void readField( if (reader.peekNull()) { readFieldDefault(reader, object, accessor, typeInfo, resolver); } else if (typeInfo.primitive()) { - accessor.putDouble(object, Double.parseDouble(reader.readNumber())); + accessor.putDouble(object, reader.readDouble()); } else { - accessor.putObject(object, Double.parseDouble(reader.readNumber())); + accessor.putObject(object, reader.readDouble()); } } } @@ -509,13 +512,16 @@ final void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolv } @Override - final void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { writer.writeString(toJsonString(value)); } @Override final Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - String value = reader.readString(); + return readStringValue(reader.readString(), typeInfo); + } + + final Object readStringValue(String value, JsonTypeInfo typeInfo) { try { return fromJsonString(value); } catch (ForyJsonException e) { @@ -578,6 +584,15 @@ String toJsonNumber(Object value) { Object fromJsonNumber(String value) { return new BigDecimal(value); } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return reader.readBigDecimal(); + } } public static final class Float16Codec extends AbstractJsonCodec { @@ -742,10 +757,30 @@ String toJsonString(Object value) { return value.toString(); } + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeUuid((UUID) value); + } + @Override Object fromJsonString(String value) { return UUID.fromString(value); } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readUuid(); + } catch (ForyJsonException e) { + throw e; + } catch (RuntimeException e) { + throw new ForyJsonException("Invalid " + typeInfo.rawType().getName() + " JSON string", e); + } + } } public static final class LocaleCodec extends StringValueCodec { @@ -895,12 +930,27 @@ String toJsonString(Object value) { return value.toString(); } + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLocalDate((LocalDate) value); + } + @Override Object fromJsonString(String value) { - if (value.length() > 10 && value.charAt(10) == 'T') { - return LocalDate.parse(value.substring(0, 10)); + return parseIsoLocalDate(value); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoLocalDate(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); } - return LocalDate.parse(value); } } @@ -1080,12 +1130,153 @@ String toJsonString(Object value) { return value.toString(); } + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeOffsetDateTime((OffsetDateTime) value); + } + @Override Object fromJsonString(String value) { + return parseIsoOffsetDateTime(value); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoOffsetDateTime(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); + } + } + } + + private static LocalDate parseIsoLocalDate(String value) { + int length = value.length(); + if (length >= 10 + && (length == 10 || value.charAt(10) == 'T') + && value.charAt(4) == '-' + && value.charAt(7) == '-') { + try { + return LocalDate.of(parse4(value, 0), parse2(value, 5), parse2(value, 8)); + } catch (RuntimeException e) { + if (length > 10 && value.charAt(10) == 'T') { + return LocalDate.parse(value.substring(0, 10)); + } + return LocalDate.parse(value); + } + } + return LocalDate.parse(value); + } + + private static OffsetDateTime parseIsoOffsetDateTime(String value) { + try { + // java.time emits these ISO forms, and parsing them directly avoids DateTimeFormatter's + // parsed-field maps on JSON scalar hot paths. Uncommon forms still use the JDK parser. + return parseIsoOffsetDateTimeFast(value); + } catch (RuntimeException e) { return OffsetDateTime.parse(value); } } + private static OffsetDateTime parseIsoOffsetDateTimeFast(String value) { + int length = value.length(); + if (length < 17 + || value.charAt(4) != '-' + || value.charAt(7) != '-' + || value.charAt(10) != 'T' + || value.charAt(13) != ':') { + throw new IllegalArgumentException(); + } + int year = parse4(value, 0); + int month = parse2(value, 5); + int day = parse2(value, 8); + int hour = parse2(value, 11); + int minute = parse2(value, 14); + int second = 0; + int nano = 0; + int index = 16; + if (index < length && value.charAt(index) == ':') { + second = parse2(value, index + 1); + index += 3; + if (index < length && value.charAt(index) == '.') { + int fractionStart = index + 1; + int fractionEnd = fractionStart; + while (fractionEnd < length && isDigit(value.charAt(fractionEnd))) { + fractionEnd++; + } + if (fractionEnd == fractionStart || fractionEnd - fractionStart > 9) { + throw new IllegalArgumentException(); + } + nano = parseNano(value, fractionStart, fractionEnd); + index = fractionEnd; + } + } + int offsetSeconds = parseOffsetSeconds(value, index); + return OffsetDateTime.of( + year, month, day, hour, minute, second, nano, ZoneOffset.ofTotalSeconds(offsetSeconds)); + } + + private static int parseOffsetSeconds(String value, int index) { + int length = value.length(); + char offset = value.charAt(index); + if (offset == 'Z') { + if (index + 1 != length) { + throw new IllegalArgumentException(); + } + return 0; + } + if (offset != '+' && offset != '-') { + throw new IllegalArgumentException(); + } + if (index + 6 > length || value.charAt(index + 3) != ':') { + throw new IllegalArgumentException(); + } + int hour = parse2(value, index + 1); + int minute = parse2(value, index + 4); + int second = 0; + int end = index + 6; + if (end < length) { + if (end + 3 != length || value.charAt(end) != ':') { + throw new IllegalArgumentException(); + } + second = parse2(value, end + 1); + } + int total = hour * 3600 + minute * 60 + second; + return offset == '-' ? -total : total; + } + + private static int parseNano(String value, int start, int end) { + int nano = 0; + for (int i = start; i < end; i++) { + nano = nano * 10 + value.charAt(i) - '0'; + } + for (int i = end - start; i < 9; i++) { + nano *= 10; + } + return nano; + } + + private static int parse4(String value, int index) { + return parse2(value, index) * 100 + parse2(value, index + 2); + } + + private static int parse2(String value, int index) { + int high = value.charAt(index) - '0'; + int low = value.charAt(index + 1) - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + throw new IllegalArgumentException(); + } + return high * 10 + low; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + public static final class AtomicBooleanCodec extends AbstractJsonCodec { public static final AtomicBooleanCodec INSTANCE = new AtomicBooleanCodec(); @@ -1178,6 +1369,160 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso } } + public static final class AtomicIntegerArrayCodec extends AbstractJsonCodec { + public static final AtomicIntegerArrayCodec INSTANCE = new AtomicIntegerArrayCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicIntegerArray array = (AtomicIntegerArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeInt(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicIntegerArray array = (AtomicIntegerArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeInt(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new AtomicIntegerArray(0); + } + int[] values = new int[8]; + int size = 0; + do { + if (reader.tryReadNull()) { + throw new ForyJsonException("Cannot read null into AtomicIntegerArray element"); + } + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readInt(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return new AtomicIntegerArray(Arrays.copyOf(values, size)); + } + } + + public static final class AtomicLongArrayCodec extends AbstractJsonCodec { + public static final AtomicLongArrayCodec INSTANCE = new AtomicLongArrayCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicLongArray array = (AtomicLongArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeLong(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicLongArray array = (AtomicLongArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeLong(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new AtomicLongArray(0); + } + long[] values = new long[8]; + int size = 0; + do { + if (reader.tryReadNull()) { + throw new ForyJsonException("Cannot read null into AtomicLongArray element"); + } + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readLong(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return new AtomicLongArray(Arrays.copyOf(values, size)); + } + } + + public static final class AtomicReferenceArrayCodec extends AbstractJsonCodec { + private final JsonTypeInfo valueTypeInfo; + private final JsonCodec valueCodec; + + public AtomicReferenceArrayCodec(java.lang.reflect.Type valueType, JsonTypeResolver resolver) { + Class valueRawType = CodecUtils.rawType(valueType, Object.class); + valueTypeInfo = resolver.getTypeInfo(valueType, valueRawType); + valueCodec = valueTypeInfo.codec(); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicReferenceArray array = (AtomicReferenceArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + valueCodec.write(writer, array.get(i), resolver); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicReferenceArray array = (AtomicReferenceArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + valueCodec.writeUtf8(writer, array.get(i), resolver); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new AtomicReferenceArray<>(0); + } + Object[] values = new Object[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = valueCodec.read(reader, valueTypeInfo, resolver); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return new AtomicReferenceArray<>(Arrays.copyOf(values, size)); + } + } + public static final class OptionalCodec extends AbstractJsonCodec { private final JsonTypeInfo valueTypeInfo; private final JsonCodec valueCodec; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index df786e028e..ed79b7ce60 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -21,6 +21,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.Map; @@ -203,10 +204,10 @@ static Class readNestedType(JsonFieldInfo property) { private boolean canCompileWrite(JsonFieldInfo property) { Field field = property.writeField(); - if (field == null) { + if (field == null && property.writeGetter() == null) { return false; } - if (!isRecordField(property) && property.writeField() == null) { + if (property.writeGetter() != null && !canCall(property.writeGetter())) { return false; } Class rawType = property.writeRawType(); @@ -221,7 +222,12 @@ private boolean canCompileRead(JsonFieldInfo property, boolean record) { if (!record && property.readAccessor() == null) { return false; } - if (!record && property.readAccessor().coreAccessor() == null) { + if (!record && property.readSetter() != null && !canCall(property.readSetter())) { + return false; + } + if (!record + && property.readSetter() == null + && property.readAccessor().coreAccessor() == null) { return false; } Class rawType = property.readRawType(); @@ -236,6 +242,11 @@ private boolean canCompile(Class type) { return CodeGenerator.sourcePublicAccessible(type) && isVisible(type); } + private boolean canCall(Method method) { + return Modifier.isPublic(method.getModifiers()) + && CodeGenerator.sourcePublicAccessible(method.getDeclaringClass()); + } + private boolean isVisible(Class type) { if (type.isPrimitive()) { return true; @@ -294,6 +305,8 @@ static boolean usesReadCodec(JsonFieldInfo property) { case COLLECTION: case MAP: return true; + case OBJECT: + return !usesReadObjectCodec(property); default: return false; } @@ -306,7 +319,7 @@ static boolean usesReadTypeField(JsonFieldInfo property) { case MAP: return true; case OBJECT: - return usesReadObjectCodec(property); + return true; default: return false; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java index e5e0877993..33ee0e568a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java @@ -119,6 +119,18 @@ private Method recordReadMethod(Field field) { } Expression fieldValue(JsonFieldInfo property, Expression object) { + Method getter = property.writeGetter(); + if (getter != null) { + // JSON writers check the returned member value directly. Requesting expression-level null + // state here only emits an unused boolean for each nullable getter and bloats generated + // object writers enough to hurt C2 inlining. + return new Expression.Invoke( + object, + getter.getName(), + property.name(), + TypeRef.of(getter.getGenericReturnType()), + false); + } return getFieldValue(object, writeDescriptor(property)); } @@ -127,6 +139,15 @@ Expression newObject() { } Expression setField(JsonFieldInfo property, Expression object, Expression value) { + Method setter = property.readSetter(); + if (setter != null) { + Class rawType = setter.getParameterTypes()[0]; + TypeRef typeRef = TypeRef.of(setter.getGenericParameterTypes()[0]); + if (!rawType.isAssignableFrom(value.type().getRawType())) { + value = tryInlineCast(value, typeRef); + } + return new Expression.Invoke(object, setter.getName(), value); + } return setFieldValue( object, readDescriptor(property), diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 1736168059..d1cae5ad1d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -29,6 +29,7 @@ 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.meta.JsonFieldTable; import org.apache.fory.json.reader.JsonReader; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Latin1ObjectReader; @@ -46,6 +47,9 @@ final class JsonReaderCodegen { 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 static final int MIN_SPLIT_READ_FIELDS = 12; + private static final int READ_FIELD_GROUP_SIZE = 2; + private static final int READ_FIELD_SWITCH_SIZE = 8; private final JsonCodegen codegen; @@ -73,7 +77,8 @@ String genReaderCode( JsonReader.class, Latin1JsonReader.class, Utf16JsonReader.class, - Utf8JsonReader.class); + Utf8JsonReader.class, + JsonFieldTable.class); ctx.implementsInterfaces( ctx.type(ObjectReader.class), ctx.type(Latin1ObjectReader.class), @@ -101,14 +106,6 @@ String genReaderCode( "codecs", BaseObjectCodec[].class, "objectCodecs"); - addGeneratedMethod( - ctx, - "final", - "fieldIndex", - fieldIndexExpression(properties), - int.class, - long.class, - "fieldHash"); addGeneratedMethod( ctx, "public", @@ -121,11 +118,14 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addReadFieldMethods( + ctx, builder, "read", JsonReader.class, type, properties, GENERIC_READER, record); addGeneratedMethod( ctx, "public", "readLatin1", - fastReadExpression(builder, "readLatin1Slow", type, properties, LATIN1_READER, record), + fastReadExpression( + builder, "readLatin1", "readLatin1Slow", type, properties, LATIN1_READER, record), Object.class, Latin1JsonReader.class, "reader", @@ -133,6 +133,25 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addFastReadGroupMethods( + ctx, + builder, + "readLatin1", + "readLatin1Slow", + Latin1JsonReader.class, + type, + properties, + LATIN1_READER, + record); + addReadFieldMethods( + ctx, + builder, + "readLatin1", + Latin1JsonReader.class, + type, + properties, + LATIN1_READER, + record); addSlowReadMethods( ctx, builder, @@ -146,7 +165,8 @@ String genReaderCode( ctx, "public", "readUtf16", - fastReadExpression(builder, "readUtf16Slow", type, properties, UTF16_READER, record), + fastReadExpression( + builder, "readUtf16", "readUtf16Slow", type, properties, UTF16_READER, record), Object.class, Utf16JsonReader.class, "reader", @@ -154,6 +174,18 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addFastReadGroupMethods( + ctx, + builder, + "readUtf16", + "readUtf16Slow", + Utf16JsonReader.class, + type, + properties, + UTF16_READER, + record); + addReadFieldMethods( + ctx, builder, "readUtf16", Utf16JsonReader.class, type, properties, UTF16_READER, record); addSlowReadMethods( ctx, builder, @@ -167,7 +199,8 @@ String genReaderCode( ctx, "public", "readUtf8", - fastReadExpression(builder, "readUtf8Slow", type, properties, UTF8_READER, record), + fastReadExpression( + builder, "readUtf8", "readUtf8Slow", type, properties, UTF8_READER, record), Object.class, Utf8JsonReader.class, "reader", @@ -175,11 +208,103 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addFastReadGroupMethods( + ctx, + builder, + "readUtf8", + "readUtf8Slow", + Utf8JsonReader.class, + type, + properties, + UTF8_READER, + record); + addReadFieldMethods( + ctx, builder, "readUtf8", Utf8JsonReader.class, type, properties, UTF8_READER, record); addSlowReadMethods( ctx, builder, "readUtf8Slow", Utf8JsonReader.class, type, properties, UTF8_READER, record); return ctx.genCode(); } + private void addFastReadGroupMethods( + CodegenContext ctx, + JsonGeneratedCodecBuilder builder, + String readMethod, + String slowMethod, + Class readerType, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + if (!shouldSplitFastRead(properties)) { + return; + } + Class objectType = record ? Object[].class : type; + for (int start = 0; start < properties.length; ) { + int end = readGroupEnd(properties, start); + addGeneratedMethod( + ctx, + "final", + readGroupMethod(readMethod, start), + fastReadGroupExpression( + builder, slowMethod, type, properties, start, end, readerMode, record), + boolean.class, + readerType, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver", + objectType, + "object", + long[].class, + "fieldHashes"); + start = end; + } + } + + private void addReadFieldMethods( + CodegenContext ctx, + JsonGeneratedCodecBuilder builder, + String readMethod, + Class readerType, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + if (!shouldSplitFieldSwitch(properties)) { + return; + } + Class objectType = record ? Object[].class : type; + for (int start = 0; start < properties.length; start += READ_FIELD_SWITCH_SIZE) { + int end = Math.min(start + READ_FIELD_SWITCH_SIZE, properties.length); + addGeneratedMethod( + ctx, + "final", + readFieldMethod(readMethod, start), + fieldSwitchRange( + builder, + type, + properties, + start, + end, + readerMode, + objectParam(type, record), + new Reference("fieldIndex", TypeRef.of(int.class)), + record), + void.class, + readerType, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver", + objectType, + "object", + int.class, + "fieldIndex"); + } + } + private void addSlowReadMethods( CodegenContext ctx, JsonGeneratedCodecBuilder builder, @@ -295,19 +420,6 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr 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, @@ -338,11 +450,16 @@ private Expression readExpression( private Expression fastReadExpression( JsonGeneratedCodecBuilder builder, + String readMethod, String slowMethod, Class type, JsonFieldInfo[] properties, int readerMode, boolean record) { + if (shouldSplitFastRead(properties)) { + return splitFastReadExpression( + builder, readMethod, slowMethod, type, properties, readerMode, record); + } Expression object = objectExpression(builder, record); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(object); @@ -371,6 +488,86 @@ private Expression fastReadExpression( return expressions; } + private Expression splitFastReadExpression( + JsonGeneratedCodecBuilder builder, + String readMethod, + 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))); + Expression hashes = + new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); + expressions.add(hashes); + for (int start = 0; start < properties.length; ) { + int end = readGroupEnd(properties, start); + Expression groupCall = + new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + readGroupMethod(readMethod, start), + "", + TypeRef.of(boolean.class), + false, + false, + readerRef(readerMode), + ownerRef(), + typeResolverRef(), + object, + hashes); + expressions.add(new Expression.If(not(groupCall), returnObject(object, record))); + start = end; + } + expressions.add(returnObject(object, record)); + return expressions; + } + + private Expression fastReadGroupExpression( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int start, + int end, + int readerMode, + boolean record) { + Expression object = objectParam(type, record); + Expression hashes = new Reference("fieldHashes", TypeRef.of(long[].class)); + Expression[] skips = new Expression[properties.length]; + Expression.ListExpression expressions = new Expression.ListExpression(); + for (int i = start + 1; i < end; i++) { + skips[i] = new Expression.Variable("skip" + i, Expression.Literal.False); + expressions.add(skips[i]); + } + for (int i = start; i < end; i++) { + Expression read = + fastReadField( + builder, + slowMethod, + type, + properties, + i, + end, + true, + readerMode, + object, + hashes, + skips, + record); + expressions.add(i == start ? read : new Expression.If(not(skips[i]), read)); + } + if (end < properties.length) { + expressions.add(returnTrue()); + } else { + expressions.add(returnFalse()); + } + return expressions; + } + private Expression fastReadField( JsonGeneratedCodecBuilder builder, String slowMethod, @@ -382,8 +579,36 @@ private Expression fastReadField( Expression hashes, Expression[] skips, boolean record) { - if (isPackedName(properties[index].name())) { - return new Expression.If( + return fastReadField( + builder, + slowMethod, + type, + properties, + index, + properties.length, + false, + readerMode, + object, + hashes, + skips, + record); + } + + private Expression fastReadField( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int index, + int groupEnd, + boolean groupHelper, + int readerMode, + Expression object, + Expression hashes, + Expression[] skips, + boolean record) { + if (isDirectName(readerMode, properties[index].name())) { + return statementIf( tryReadNextFieldNameColon(readerMode, properties[index]), new Expression.ListExpression( readField( @@ -395,21 +620,43 @@ private Expression fastReadField( object, record, usesTokenValueRead(readerMode)), - fieldEnd(slowMethod, properties.length, index, readerMode, object, record)), + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + index, + readerMode, + object, + record)), nextDirectFallback( builder, slowMethod, type, properties, index, + groupEnd, + groupHelper, readerMode, object, hashes, skips, - record)); + record), + groupHelper); } return nextDirectFallback( - builder, slowMethod, type, properties, index, readerMode, object, hashes, skips, record); + builder, + slowMethod, + type, + properties, + index, + groupEnd, + groupHelper, + readerMode, + object, + hashes, + skips, + record); } private Expression nextDirectFallback( @@ -418,14 +665,16 @@ private Expression nextDirectFallback( Class type, JsonFieldInfo[] properties, int index, + int groupEnd, + boolean groupHelper, 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( + if (nextIndex < groupEnd && isDirectName(readerMode, properties[nextIndex].name())) { + return statementIf( tryReadNextFieldNameColon(readerMode, properties[nextIndex]), new Expression.ListExpression( readField( @@ -437,22 +686,44 @@ private Expression nextDirectFallback( object, record, usesTokenValueRead(readerMode)), - fieldEnd(slowMethod, properties.length, nextIndex, readerMode, object, record), - new Expression.Assign(skips[nextIndex], Expression.Literal.True)), + new Expression.Assign(skips[nextIndex], Expression.Literal.True), + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + nextIndex, + readerMode, + object, + record)), hashFallback( builder, slowMethod, type, properties, index, + groupEnd, + groupHelper, readerMode, object, hashes, skips, - record)); + record), + groupHelper); } return hashFallback( - builder, slowMethod, type, properties, index, readerMode, object, hashes, skips, record); + builder, + slowMethod, + type, + properties, + index, + groupEnd, + groupHelper, + readerMode, + object, + hashes, + skips, + record); } private Expression hashFallback( @@ -461,6 +732,8 @@ private Expression hashFallback( Class type, JsonFieldInfo[] properties, int index, + int groupEnd, + boolean groupHelper, int readerMode, Expression object, Expression hashes, @@ -475,6 +748,8 @@ private Expression hashFallback( type, properties, index, + groupEnd, + groupHelper, readerMode, object, hashes, @@ -489,6 +764,8 @@ private Expression fastReadFieldFromHash( Class type, JsonFieldInfo[] properties, int index, + int groupEnd, + boolean groupHelper, int readerMode, Expression object, Expression hashes, @@ -496,9 +773,9 @@ private Expression fastReadFieldFromHash( Expression fieldHash, boolean record) { Expression fallback; - if (index + 1 < properties.length) { + if (index + 1 < groupEnd) { fallback = - new Expression.If( + statementIf( eq(fieldHash, arrayValue(hashes, index + 1)), new Expression.ListExpression( expectExpr(readerMode, ':'), @@ -511,19 +788,40 @@ private Expression fastReadFieldFromHash( 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)); + new Expression.Assign(skips[index + 1], Expression.Literal.True), + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + index + 1, + readerMode, + object, + record)), + slowConsumedReturn( + slowMethod, index, fieldIndexInvoke(fieldHash), object, record, groupHelper), + groupHelper); } else { - fallback = slowConsumedReturn(slowMethod, index, fieldIndexInvoke(fieldHash), object, record); + fallback = + slowConsumedReturn( + slowMethod, index, fieldIndexInvoke(fieldHash), object, record, groupHelper); } - return new Expression.If( + return statementIf( 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))); + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + index, + readerMode, + object, + record)), + groupHelper); } private static boolean isPackedName(String name) { @@ -531,6 +829,18 @@ private static boolean isPackedName(String name) { if (length == 0 || length > Long.BYTES) { return false; } + return isAsciiName(name); + } + + private static boolean isDirectName(int readerMode, String name) { + if (readerMode == LATIN1_READER || readerMode == UTF8_READER) { + return JsonAsciiToken.isLongPackable(fieldNameToken(name)); + } + return isPackedName(name); + } + + private static boolean isAsciiName(String name) { + int length = name.length(); for (int i = 0; i < length; i++) { char ch = name.charAt(i); if (ch == 0 || ch > 0xFF) { @@ -544,6 +854,67 @@ private static long packedNameMask(int length) { return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L; } + private static boolean shouldSplitFastRead(JsonFieldInfo[] properties) { + return properties.length >= MIN_SPLIT_READ_FIELDS; + } + + private static String readGroupMethod(String readMethod, int start) { + return readMethod + "Group" + start; + } + + private static String readFieldMethod(String readMethod, int start) { + return readMethod + "Field" + start; + } + + private static int readGroupEnd(JsonFieldInfo[] properties, int start) { + int weight = 0; + int index = start; + while (index < properties.length) { + int fieldWeight = readFieldWeight(properties[index]); + if (index > start && weight + fieldWeight > READ_FIELD_GROUP_SIZE) { + break; + } + weight += fieldWeight; + index++; + if (fieldWeight >= READ_FIELD_GROUP_SIZE) { + break; + } + } + return index; + } + + private static int readFieldWeight(JsonFieldInfo property) { + switch (property.readKind()) { + case ARRAY: + case COLLECTION: + case MAP: + case OBJECT: + case ENUM: + return READ_FIELD_GROUP_SIZE; + default: + return 1; + } + } + + private static String readMethod(int readerMode) { + switch (readerMode) { + case GENERIC_READER: + return "read"; + case LATIN1_READER: + return "readLatin1"; + case UTF16_READER: + return "readUtf16"; + case UTF8_READER: + return "readUtf8"; + default: + throw new IllegalArgumentException(String.valueOf(readerMode)); + } + } + + private static boolean shouldSplitFieldSwitch(JsonFieldInfo[] properties) { + return properties.length > READ_FIELD_SWITCH_SIZE; + } + private Expression objectExpression(JsonGeneratedCodecBuilder builder, boolean record) { if (record) { return new Expression.Variable( @@ -562,6 +933,22 @@ private Expression returnObject(Expression object, boolean record) { return new Expression.Return(object); } + private static Expression returnTrue() { + return new Expression.Return(Expression.Literal.True); + } + + private static Expression returnFalse() { + return new Expression.Return(Expression.Literal.False); + } + + private static Expression statementIf( + Expression predicate, Expression trueExpr, Expression falseExpr, boolean statementOnly) { + if (statementOnly) { + return new Expression.If(predicate, trueExpr, falseExpr, false, TypeRef.of(void.class)); + } + return new Expression.If(predicate, trueExpr, falseExpr); + } + private Expression slowConsumedReturn( String slowMethod, int index, Expression firstFieldIndex, Expression object, boolean record) { return new Expression.ListExpression( @@ -569,6 +956,36 @@ private Expression slowConsumedReturn( returnObject(object, record)); } + private Expression slowConsumedReturn( + String slowMethod, + int index, + Expression firstFieldIndex, + Expression object, + boolean record, + boolean groupHelper) { + if (!groupHelper) { + return slowConsumedReturn(slowMethod, index, firstFieldIndex, object, record); + } + return new Expression.ListExpression( + slowCall(slowMethod, object, Expression.Literal.ofInt(index), firstFieldIndex), + returnFalse()); + } + + private Expression fieldEnd( + String slowMethod, + int propertyCount, + int groupEnd, + boolean groupHelper, + int index, + int readerMode, + Expression object, + boolean record) { + if (!groupHelper) { + return fieldEnd(slowMethod, propertyCount, index, readerMode, object, record); + } + return fastReadGroupEnd(slowMethod, propertyCount, index, readerMode, object); + } + private Expression fieldEnd( String slowMethod, int propertyCount, @@ -585,6 +1002,17 @@ private Expression fieldEnd( slowCall(slowMethod, object, Expression.Literal.ofInt(propertyCount))); } + private Expression fastReadGroupEnd( + String slowMethod, int propertyCount, int index, int readerMode, Expression object) { + if (index + 1 < propertyCount) { + return new Expression.If(not(consumeCommaOrEndObjectExpr(readerMode)), returnFalse()); + } + return new Expression.ListExpression( + new Expression.If( + consumeCommaOrEndObjectExpr(readerMode), + slowCall(slowMethod, object, Expression.Literal.ofInt(propertyCount)))); + } + private Expression slowReadExpression( JsonGeneratedCodecBuilder builder, Class type, @@ -664,9 +1092,51 @@ private Expression fieldSwitch( 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] = + if (shouldSplitFieldSwitch(properties)) { + int chunks = (properties.length + READ_FIELD_SWITCH_SIZE - 1) / READ_FIELD_SWITCH_SIZE; + Expression.Switch.Case[] cases = new Expression.Switch.Case[chunks]; + for (int i = 0, start = 0; i < chunks; i++, start += READ_FIELD_SWITCH_SIZE) { + cases[i] = + new Expression.Switch.Case( + i, + new Expression.ListExpression( + new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + readFieldMethod(readMethod(readerMode), start), + "", + TypeRef.of(void.class), + false, + false, + readerRef(readerMode), + ownerRef(), + typeResolverRef(), + object, + fieldIndex), + new Expression.Break())); + } + Expression chunkIndex = + new Expression.Arithmetic( + true, "/", fieldIndex, Expression.Literal.ofInt(READ_FIELD_SWITCH_SIZE)); + return new Expression.Switch( + chunkIndex, cases, new Expression.Invoke(readerRef(readerMode), "skipValue")); + } + return fieldSwitchRange( + builder, type, properties, 0, properties.length, readerMode, object, fieldIndex, record); + } + + private Expression fieldSwitchRange( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + int start, + int end, + int readerMode, + Expression object, + Expression fieldIndex, + boolean record) { + Expression.Switch.Case[] cases = new Expression.Switch.Case[end - start]; + for (int i = start; i < end; i++) { + cases[i - start] = new Expression.Switch.Case( i, new Expression.ListExpression( @@ -801,6 +1271,14 @@ private static Expression readLongExpr(int readerMode, boolean tokenValueRead) { .inline(); } + private static Expression readDoubleExpr(int readerMode, boolean tokenValueRead) { + return new Expression.Invoke( + readerRef(readerMode), + readDoubleMethod(readerMode, tokenValueRead), + TypeRef.of(double.class)) + .inline(); + } + private static Expression readStringExpr(int readerMode) { return readStringExpr(readerMode, false); } @@ -835,6 +1313,13 @@ private static String readLongMethod(int readerMode, boolean tokenValueRead) { return tokenValueRead ? "readLongTokenValue" : "readNextLongValue"; } + private static String readDoubleMethod(int readerMode, boolean tokenValueRead) { + if (readerMode == UTF8_READER) { + return tokenValueRead ? "readDoubleTokenValue" : "readNextDoubleValue"; + } + return "readDouble"; + } + private static String readStringMethod(int readerMode, boolean tokenValueRead) { if (readerMode == GENERIC_READER) { return "readNullableString"; @@ -853,12 +1338,10 @@ private static Expression readFieldNameHash(int readerMode, String namePrefix) { private static Expression fieldIndexInvoke(Expression fieldHash) { return new Expression.Invoke( - new Reference("this", TypeRef.of(Object.class)), - "fieldIndex", - "", + new Expression.Invoke(ownerRef(), "readTable", TypeRef.of(JsonFieldTable.class)), + "index", TypeRef.of(int.class), - false, - false, + true, fieldHash) .inline(); } @@ -881,6 +1364,17 @@ private static Expression tryReadNextFieldNameColon(int readerMode, JsonFieldInf Expression.Literal.ofInt(tokenLength)) .inline(); } + if (suffixLength > 3) { + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextFieldNameToken8", + TypeRef.of(boolean.class), + Expression.Literal.ofLong(JsonAsciiToken.prefix(token)), + Expression.Literal.ofLong(JsonAsciiToken.suffixLong(token)), + Expression.Literal.ofLong(JsonAsciiToken.suffixMask(tokenLength)), + Expression.Literal.ofInt(tokenLength)) + .inline(); + } return new Expression.Invoke( readerRef(readerMode), "tryReadNextFieldNameToken" + suffixLength, @@ -1017,6 +1511,8 @@ private static boolean usesReadCodec(JsonFieldInfo property) { case COLLECTION: case MAP: return true; + case OBJECT: + return !usesReadObjectCodec(property); default: return false; } @@ -1029,7 +1525,7 @@ private static boolean usesReadTypeField(JsonFieldInfo property) { case MAP: return true; case OBJECT: - return usesReadObjectCodec(property); + return true; default: return false; } @@ -1066,6 +1562,8 @@ private Expression readField( return readInt(builder, property, rawType, readerMode, object, tokenValueRead); case LONG: return readLong(builder, property, rawType, readerMode, object, tokenValueRead); + case DOUBLE: + return readDouble(builder, property, rawType, readerMode, object, tokenValueRead); case STRING: return builder.setField(property, object, readStringExpr(readerMode, tokenValueRead)); case ENUM: @@ -1102,6 +1600,8 @@ private Expression readRecordField( return readRecordInt(rawType, id, readerMode, object, tokenValueRead); case LONG: return readRecordLong(rawType, id, readerMode, object, tokenValueRead); + case DOUBLE: + return readRecordDouble(rawType, id, readerMode, object, tokenValueRead); case STRING: return assignRecord(object, id, readStringExpr(readerMode, tokenValueRead)); case ENUM: @@ -1163,6 +1663,18 @@ private static Expression readRecordLong( assignRecord(object, id, value)); } + private static Expression readRecordDouble( + Class rawType, int id, int readerMode, Expression object, boolean tokenValueRead) { + Expression value = box(Double.class, readDoubleExpr(readerMode, tokenValueRead)); + if (rawType.isPrimitive()) { + return assignRecord(object, id, value); + } + return new Expression.If( + tryReadNullExpr(readerMode), + assignRecord(object, id, new Expression.Null(TypeRef.of(Double.class), false)), + assignRecord(object, id, value)); + } + private static Expression readRecordEnum( int id, int readerMode, Expression object, boolean tokenValueRead) { return new Expression.If( @@ -1175,16 +1687,7 @@ 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 assignRecord(object, id, readResolvedValue(property, id, readerMode)); } return new Expression.If( tryReadNullExpr(readerMode), @@ -1254,6 +1757,23 @@ private static Expression readLong( property, object, box(Long.class, readLongExpr(readerMode, tokenValueRead)))); } + private static Expression readDouble( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + Class rawType, + int readerMode, + Expression object, + boolean tokenValueRead) { + if (rawType.isPrimitive()) { + return builder.setField(property, object, readDoubleExpr(readerMode, tokenValueRead)); + } + return new Expression.If( + tryReadNullExpr(readerMode), + builder.setNull(property, object), + builder.setField( + property, object, box(Double.class, readDoubleExpr(readerMode, tokenValueRead)))); + } + private static Expression readEnum( JsonGeneratedCodecBuilder builder, JsonFieldInfo property, @@ -1331,12 +1851,7 @@ private static Expression readObject( 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 readResolvedField(builder, property, id, readerMode, object); } return new Expression.If( tryReadNullExpr(readerMode), @@ -1366,19 +1881,22 @@ private static Expression readEnumValue( readEnumMethod(readerMode, tokenValueRead, hashFallback), "", TypeRef.of(Object.class), - true, + false, false, readerRef(readerMode)), TypeRef.of(enumType)); } private static Expression readResolvedValue(JsonFieldInfo property, int id, int readerMode) { + // Generated readers either branch on JSON null before calling a non-null codec path, or assign + // nullable reference results directly. Requesting expression null-state here only emits dead + // boolean locals around codec calls and bloats hot generated reader methods. return new Expression.Cast( new Expression.Invoke( fieldRef("r" + id, JsonCodec.class), readObjectMethod(readerMode), TypeRef.of(Object.class), - true, + false, readerRef(readerMode), fieldRef("t" + id, JsonTypeInfo.class), typeResolverRef()), @@ -1391,7 +1909,7 @@ private static Expression readCollectionValue(JsonFieldInfo property, int id, in fieldRef("r" + id, CollectionCodec.class), readObjectNonNullMethod(readerMode), TypeRef.of(Object.class), - true, + false, readerRef(readerMode), fieldRef("t" + id, JsonTypeInfo.class), typeResolverRef()), @@ -1407,7 +1925,7 @@ private static Expression readObjectValue( codec, readObjectNonNullMethod(readerMode), TypeRef.of(Object.class), - true, + false, readerRef(readerMode), fieldRef("t" + id, JsonTypeInfo.class), typeResolverRef()), 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 index 689dfa2951..a0ce024439 100644 --- 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 @@ -19,10 +19,18 @@ package org.apache.fory.json.codegen; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.UUID; 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.codegen.ExpressionOptimizer; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.JsonCodec; import org.apache.fory.json.meta.JsonFieldInfo; @@ -36,6 +44,11 @@ import org.apache.fory.reflect.TypeRef; final class JsonWriterCodegen { + private static final int MIN_SPLIT_MEMBERS = 12; + // Keep generated member helpers below C2's big-method range without fragmenting them into + // tiny calls. This mirrors Fory core's object-codec split strategy for large generated codecs. + private static final int MEMBER_GROUP_SIZE = 10; + private final JsonCodegen codegen; private final boolean writeNullFields; @@ -209,31 +222,81 @@ private Expression writeExpression( "object", new Expression.Cast( new Reference("value", TypeRef.of(Object.class)), TypeRef.of(type))); + Reference writer = writerRef(utf8); + Reference typeResolver = typeResolverRef(); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(object); Expression index = null; if (!objectStartFused) { - expressions.add(new Expression.Invoke(writerRef(utf8), "writeObjectStart")); + expressions.add(new Expression.Invoke(writer, "writeObjectStart")); index = new Expression.Variable("index", Expression.Literal.ofInt(0)); expressions.add(index); } boolean commaKnown = objectStartFused; + boolean splitMembers = properties.length >= MIN_SPLIT_MEMBERS; + List memberGroup = splitMembers ? new ArrayList<>(MEMBER_GROUP_SIZE) : null; for (int i = 0; i < properties.length; i++) { + Expression member; if (objectStartFused && i == 0) { - expressions.add( + member = writeObjectStartPrimitive( - properties[i], builder.fieldValue(properties[i], object), utf8)); + properties[i], builder.fieldValue(properties[i], object), utf8, writer); + } else { + member = + writeProp( + builder, properties[i], i, utf8, commaKnown, index, object, writer, typeResolver); + } + if (splitMembers && commaKnown) { + memberGroup.add(member); + if (memberGroup.size() == MEMBER_GROUP_SIZE) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + } } else { - expressions.add(writeProp(builder, properties[i], i, utf8, commaKnown, index, object)); + if (splitMembers) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + } + expressions.add(member); } if (writeNullFields || properties[i].writeRawType().isPrimitive()) { commaKnown = true; } } - expressions.add(new Expression.Invoke(writerRef(utf8), "writeObjectEnd")); + if (splitMembers) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + } + expressions.add(new Expression.Invoke(writer, "writeObjectEnd")); return expressions; } + private static void addMemberGroup( + JsonGeneratedCodecBuilder builder, + Expression.ListExpression expressions, + List memberGroup, + Expression object, + Reference writer, + Reference typeResolver) { + if (memberGroup.isEmpty()) { + return; + } + if (memberGroup.size() == 1) { + expressions.add(memberGroup.get(0)); + memberGroup.clear(); + return; + } + LinkedHashSet cutPoints = new LinkedHashSet<>(); + cutPoints.add(object); + cutPoints.add(writer); + cutPoints.add(typeResolver); + expressions.add( + ExpressionOptimizer.invokeGenerated( + builder.context(), + cutPoints, + new Expression.ListExpression(new ArrayList<>(memberGroup)), + "writeMembers", + false)); + memberGroup.clear(); + } + private static boolean canFuseObjectStart(JsonFieldInfo[] properties) { if (properties.length == 0 || !properties[0].writeRawType().isPrimitive()) { return false; @@ -250,16 +313,24 @@ private static boolean canFuseObjectStart(JsonFieldInfo[] properties) { } private static Expression writeObjectStartPrimitive( - JsonFieldInfo property, Expression value, boolean utf8) { + JsonFieldInfo property, Expression value, boolean utf8, Expression writer) { switch (property.writeKind()) { case BYTE: case SHORT: case INT: + if (utf8 && canPackPrefix(property, false)) { + return new Expression.Invoke( + writer, "writeObjectIntField", packedPrefixArgs(property, false, value)); + } return new Expression.Invoke( - writerRef(utf8), "writeObjectIntField", prefixRef(utf8, false, 0), value); + writer, "writeObjectIntField", prefixRef(utf8, false, 0), value); case LONG: + if (utf8 && canPackPrefix(property, false)) { + return new Expression.Invoke( + writer, "writeObjectLongField", packedPrefixArgs(property, false, value)); + } return new Expression.Invoke( - writerRef(utf8), "writeObjectLongField", prefixRef(utf8, false, 0), value); + writer, "writeObjectLongField", prefixRef(utf8, false, 0), value); default: throw new ForyJsonException( "Unsupported generated object-start kind " + property.writeKind()); @@ -273,11 +344,13 @@ private Expression writeProp( boolean utf8, boolean commaKnown, Expression index, - Expression object) { + Expression object, + Expression writer, + Expression typeResolver) { Class rawType = property.writeRawType(); if (rawType.isPrimitive()) { return writePrimitive( - property, id, builder.fieldValue(property, object), utf8, commaKnown, index); + property, id, builder.fieldValue(property, object), utf8, commaKnown, index, writer); } Expression value = new Expression.Variable( @@ -291,24 +364,24 @@ private Expression writeProp( 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))); + writeFieldName(property, id, utf8, commaKnown, index, writer), + new Expression.Invoke(writer, "writeNull")), + writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver))); } return new Expression.ListExpression( value, - writeFieldName(id, utf8, commaKnown, index), + writeFieldName(property, id, utf8, commaKnown, index, writer), new Expression.If( eq(value, nullValue), - new Expression.Invoke(writerRef(utf8), "writeNull"), - writeValue(property, id, value, utf8, true, index))); + new Expression.Invoke(writer, "writeNull"), + writeValue(property, id, value, utf8, true, index, writer, typeResolver))); } Expression write = isPrefixValue(property.writeKind()) - ? writeValue(property, id, value, utf8, commaKnown, index) + ? writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver) : new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index), - writeValue(property, id, value, utf8, true, index)); + writeFieldName(property, id, utf8, commaKnown, index, writer), + writeValue(property, id, value, utf8, true, index, writer, typeResolver)); return new Expression.ListExpression(value, new Expression.If(ne(value, nullValue), write)); } @@ -318,39 +391,45 @@ private Expression writePrimitive( Expression value, boolean utf8, boolean commaKnown, - Expression index) { + Expression index, + Expression writer) { switch (property.writeKind()) { case BOOLEAN: return writeRawFieldValue( - utf8, commaKnown, index, booleanFieldValue(id, value, utf8, commaKnown, index)); + commaKnown, index, booleanFieldValue(id, value, utf8, commaKnown, index), writer); case BYTE: case SHORT: case INT: - return writeNumberField(id, value, false, utf8, commaKnown, index); + return writeNumberField(property, id, value, false, utf8, commaKnown, index, writer); case LONG: - return writeNumberField(id, value, true, utf8, commaKnown, index); + return writeNumberField(property, id, value, true, utf8, commaKnown, index, writer); default: return new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index), - writePrimitiveScalar(property.writeKind(), value, utf8)); + writeFieldName(property, id, utf8, commaKnown, index, writer), + writePrimitiveScalar(property.writeKind(), value, writer)); } } private static Expression writeNumberField( + JsonFieldInfo property, int id, Expression value, boolean longValue, boolean utf8, boolean commaKnown, - Expression index) { + Expression index, + Expression writer) { String writerMethod = longValue ? "writeLongField" : "writeIntField"; if (commaKnown) { - return new Expression.Invoke(writerRef(utf8), writerMethod, prefixRef(utf8, true, id), value); + if (utf8 && canPackPrefix(property, true)) { + return new Expression.Invoke(writer, writerMethod, packedPrefixArgs(property, true, value)); + } + return new Expression.Invoke(writer, writerMethod, prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = new Expression.ListExpression( new Expression.Invoke( - writerRef(utf8), + writer, writerMethod, prefixRef(utf8, false, id), prefixRef(utf8, true, id), @@ -361,15 +440,24 @@ private static Expression writeNumberField( } private static Expression writeStringField( - int id, Expression value, boolean utf8, boolean commaKnown, Expression index) { + JsonFieldInfo property, + int id, + Expression value, + boolean utf8, + boolean commaKnown, + Expression index, + Expression writer) { if (commaKnown) { - return new Expression.Invoke( - writerRef(utf8), "writeStringField", prefixRef(utf8, true, id), value); + if (utf8 && canPackPrefix(property, true)) { + return new Expression.Invoke( + writer, "writeStringField", packedPrefixArgs(property, true, value)); + } + return new Expression.Invoke(writer, "writeStringField", prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = new Expression.ListExpression( new Expression.Invoke( - writerRef(utf8), + writer, "writeStringField", prefixRef(utf8, false, id), prefixRef(utf8, true, id), @@ -380,7 +468,15 @@ private static Expression writeStringField( } private static Expression writeFieldName( - int id, boolean utf8, boolean commaKnown, Expression index) { + JsonFieldInfo property, + int id, + boolean utf8, + boolean commaKnown, + Expression index, + Expression writer) { + if (commaKnown && utf8 && canPackPrefix(property, true)) { + return new Expression.Invoke(writer, "writeRawValue", packedPrefixArgs(property, true)); + } Expression prefix = commaKnown ? prefixRef(utf8, true, id) @@ -391,8 +487,7 @@ private static Expression writeFieldName( true, TypeRef.of(byte[].class)); Expression.ListExpression expressions = - new Expression.ListExpression( - new Expression.Invoke(writerRef(utf8), "writeRawValue", prefix)); + new Expression.ListExpression(new Expression.Invoke(writer, "writeRawValue", prefix)); if (!commaKnown) { expressions.add(increment(index)); } @@ -405,12 +500,13 @@ private Expression writeValue( Expression value, boolean utf8, boolean commaKnown, - Expression index) { + Expression index, + Expression writer, + Expression typeResolver) { JsonFieldKind kind = property.writeKind(); switch (kind) { case BOOLEAN: return writeRawFieldValue( - utf8, commaKnown, index, booleanFieldValue( @@ -418,58 +514,92 @@ private Expression writeValue( new Expression.Invoke(value, "booleanValue", TypeRef.of(boolean.class)).inline(), utf8, commaKnown, - index)); + index), + writer); case BYTE: case SHORT: case INT: return writeNumberField( + property, id, new Expression.Invoke(value, "intValue", TypeRef.of(int.class)).inline(), false, utf8, commaKnown, - index); + index, + writer); case LONG: return writeNumberField( + property, id, new Expression.Invoke(value, "longValue", TypeRef.of(long.class)).inline(), true, utf8, commaKnown, - index); + index, + writer); case STRING: - return writeStringField(id, value, utf8, commaKnown, index); + return writeStringField(property, id, value, utf8, commaKnown, index, writer); case ENUM: return writeRawFieldValue( - utf8, commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index)); + commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index), writer); case FLOAT: case DOUBLE: case CHAR: - return writeScalar(kind, value, utf8); + return writeScalar(kind, value, writer); case ARRAY: + Expression array = writeExactUtf8Array(property.writeRawType(), value, utf8, writer); + return array == null ? writeCodec(id, value, utf8, writer, typeResolver) : array; case MAP: - return writeCodec(id, value, utf8); + return writeCodec(id, value, utf8, writer, typeResolver); case COLLECTION: if (property.writeElementRawType() == String.class) { - return writeStringCollection(value, utf8); + return writeStringCollection(value, utf8, writer); } - return writeCodec(id, value, utf8); + return writeCodec(id, value, utf8, writer, typeResolver); + case OBJECT: + Expression scalar = writeExactUtf8Scalar(property.writeRawType(), value, utf8, writer); + return scalar == null ? writeCodec(id, value, utf8, writer, typeResolver) : scalar; default: - return writeCodec(id, value, utf8); + return writeCodec(id, value, utf8, writer, typeResolver); } } private static Expression writeRawFieldValue( - boolean utf8, boolean commaKnown, Expression index, Expression value) { + boolean commaKnown, Expression index, Expression value, Expression writer) { Expression.ListExpression expressions = - new Expression.ListExpression( - new Expression.Invoke(writerRef(utf8), "writeRawValue", value)); + new Expression.ListExpression(new Expression.Invoke(writer, "writeRawValue", value)); if (!commaKnown) { expressions.add(increment(index)); } return expressions; } + private static Expression[] packedPrefixArgs( + JsonFieldInfo property, boolean comma, Expression... extraArgs) { + byte[] prefix = comma ? property.utf8CommaNamePrefix() : property.utf8NamePrefix(); + Expression[] args = new Expression[3 + extraArgs.length]; + args[0] = Expression.Literal.ofLong(packedPrefixWord(prefix, 0)); + args[1] = Expression.Literal.ofLong(packedPrefixWord(prefix, Long.BYTES)); + args[2] = Expression.Literal.ofInt(prefix.length); + System.arraycopy(extraArgs, 0, args, 3, extraArgs.length); + return args; + } + + private static boolean canPackPrefix(JsonFieldInfo property, boolean comma) { + int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length; + return length <= Long.BYTES * 2; + } + + private static long packedPrefixWord(byte[] prefix, int offset) { + long word = 0; + int end = Math.min(prefix.length, offset + Long.BYTES); + for (int i = offset; i < end; i++) { + word |= (prefix[i] & 0xffL) << ((i - offset) << 3); + } + return word; + } + private static Expression booleanFieldValue( int id, Expression value, boolean utf8, boolean commaKnown, Expression index) { return new Expression.Invoke( @@ -492,42 +622,84 @@ private static Expression enumFieldValue( .inline(); } - private static Expression writeCodec(int id, Expression value, boolean utf8) { + private static Expression writeCodec( + int id, Expression value, boolean utf8, Expression writer, Expression typeResolver) { return new Expression.Invoke( fieldRef("c" + id, JsonCodec.class), utf8 ? "writeUtf8" : "writeString", - writerRef(utf8), + writer, value, - typeResolverRef()); + typeResolver); + } + + private static Expression writeExactUtf8Scalar( + Class rawType, Expression value, boolean utf8, Expression writer) { + if (!utf8) { + return null; + } + String writerMethod; + if (rawType == UUID.class) { + writerMethod = "writeUuid"; + } else if (rawType == LocalDate.class) { + writerMethod = "writeLocalDate"; + } else if (rawType == OffsetDateTime.class) { + writerMethod = "writeOffsetDateTime"; + } else if (rawType == BigDecimal.class) { + return new Expression.Invoke( + writer, + "writeNumber", + new Expression.Invoke(value, "toString", TypeRef.of(String.class)).inline()); + } else { + return null; + } + return new Expression.Invoke(writer, writerMethod, value); } - private static Expression writeStringCollection(Expression value, boolean utf8) { + private static Expression writeExactUtf8Array( + Class rawType, Expression value, boolean utf8, Expression writer) { + if (!utf8) { + return null; + } + if (rawType == String[].class) { + return new Expression.Invoke(writer, "writeStringArray", value); + } + if (rawType == long[].class) { + return new Expression.Invoke(writer, "writeLongArray", value); + } + return null; + } + + private static Expression writeStringCollection( + Expression value, boolean utf8, Expression writer) { + if (utf8) { + return new Expression.Invoke(writer, "writeStringCollection", value); + } return new Expression.ListExpression( - new Expression.Invoke(writerRef(utf8), "writeArrayStart"), + new Expression.Invoke(writer, "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")); + new Expression.Invoke(writer, "writeStringElement", index, element)), + new Expression.Invoke(writer, "writeArrayEnd")); } - private Expression writeScalar(JsonFieldKind kind, Expression value, boolean utf8) { + private Expression writeScalar(JsonFieldKind kind, Expression value, Expression writer) { switch (kind) { case FLOAT: return new Expression.Invoke( - writerRef(utf8), + writer, "writeFloat", new Expression.Invoke(value, "floatValue", TypeRef.of(float.class)).inline()); case DOUBLE: return new Expression.Invoke( - writerRef(utf8), + writer, "writeDouble", new Expression.Invoke(value, "doubleValue", TypeRef.of(double.class)).inline()); case CHAR: return new Expression.Invoke( - writerRef(utf8), + writer, "writeChar", new Expression.Invoke(value, "charValue", TypeRef.of(char.class)).inline()); default: @@ -535,14 +707,14 @@ private Expression writeScalar(JsonFieldKind kind, Expression value, boolean utf } } - private Expression writePrimitiveScalar(JsonFieldKind kind, Expression value, boolean utf8) { + private Expression writePrimitiveScalar(JsonFieldKind kind, Expression value, Expression writer) { switch (kind) { case FLOAT: - return new Expression.Invoke(writerRef(utf8), "writeFloat", value); + return new Expression.Invoke(writer, "writeFloat", value); case DOUBLE: - return new Expression.Invoke(writerRef(utf8), "writeDouble", value); + return new Expression.Invoke(writer, "writeDouble", value); case CHAR: - return new Expression.Invoke(writerRef(utf8), "writeChar", value); + return new Expression.Invoke(writer, "writeChar", value); default: throw new ForyJsonException("Unsupported generated primitive kind " + kind); } 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 index 44b504e865..16285ff41e 100644 --- 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 @@ -21,6 +21,7 @@ public final class JsonAsciiToken { private static final int MAX_SUFFIX_LENGTH = 3; + private static final int MAX_LONG_SUFFIX_LENGTH = Long.BYTES; private JsonAsciiToken() {} @@ -29,6 +30,19 @@ public static boolean isPackable(String token) { if (length == 0 || suffixLength(length) > MAX_SUFFIX_LENGTH) { return false; } + return isLatin1Token(token); + } + + public static boolean isLongPackable(String token) { + int length = token.length(); + if (length == 0 || suffixLength(length) > MAX_LONG_SUFFIX_LENGTH) { + return false; + } + return isLatin1Token(token); + } + + private static boolean isLatin1Token(String token) { + int length = token.length(); for (int i = 0; i < length; i++) { char ch = token.charAt(i); if (ch == 0 || ch > 0xFF) { @@ -61,6 +75,20 @@ public static int suffix(String token) { return value; } + public static long suffixLong(String token) { + int suffixLength = suffixLength(token.length()); + long value = 0; + for (int i = 0; i < suffixLength; i++) { + value |= (long) (token.charAt(i + Long.BYTES) & 0xFF) << (i << 3); + } + return value; + } + + public static long suffixMask(int tokenLength) { + int suffixLength = suffixLength(tokenLength); + return suffixLength == Long.BYTES ? -1L : (1L << (suffixLength << 3)) - 1; + } + 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 index 5bed687f76..76503a7759 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java @@ -20,6 +20,9 @@ package org.apache.fory.json.meta; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.apache.fory.json.ForyJsonException; import org.apache.fory.reflect.FieldAccessor; public abstract class JsonFieldAccessor { @@ -27,9 +30,21 @@ public Object getObject(Object target) { throw new UnsupportedOperationException(); } - public abstract Field field(); + public Field field() { + return null; + } - public abstract FieldAccessor coreAccessor(); + public Method getter() { + return null; + } + + public Method setter() { + return null; + } + + public FieldAccessor coreAccessor() { + return null; + } public boolean getBoolean(Object target) { return (Boolean) getObject(target); @@ -103,6 +118,14 @@ public static JsonFieldAccessor forField(Field field) { return new FieldJsonAccessor(FieldAccessor.createAccessor(field)); } + public static JsonFieldAccessor forGetter(Method getter) { + return new GetterJsonAccessor(getter); + } + + public static JsonFieldAccessor forSetter(Method setter) { + return new SetterJsonAccessor(setter); + } + private static final class FieldJsonAccessor extends JsonFieldAccessor { private final FieldAccessor accessor; @@ -210,4 +233,56 @@ public void putChar(Object target, char value) { accessor.putChar(target, value); } } + + private static final class GetterJsonAccessor extends JsonFieldAccessor { + private final Method getter; + + private GetterJsonAccessor(Method getter) { + this.getter = getter; + getter.setAccessible(true); + } + + @Override + public Method getter() { + return getter; + } + + @Override + public Object getObject(Object target) { + try { + return getter.invoke(target); + } catch (IllegalAccessException | InvocationTargetException e) { + throw accessException(getter, e); + } + } + } + + private static final class SetterJsonAccessor extends JsonFieldAccessor { + private final Method setter; + + private SetterJsonAccessor(Method setter) { + this.setter = setter; + setter.setAccessible(true); + } + + @Override + public Method setter() { + return setter; + } + + @Override + public void putObject(Object target, Object value) { + try { + setter.invoke(target, value); + } catch (IllegalAccessException | InvocationTargetException e) { + throw accessException(setter, e); + } + } + } + + private static ForyJsonException accessException(Method method, ReflectiveOperationException e) { + Throwable cause = + e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e; + return new ForyJsonException("Cannot access JSON property method " + method, cause); + } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index 01a31a374a..d2e2afe3d1 100644 --- 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 @@ -20,6 +20,7 @@ package org.apache.fory.json.meta; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.Collection; @@ -54,6 +55,9 @@ public final class JsonFieldInfo { private final String name; private final Field writeField; + private final Method writeGetter; + private final Field readField; + private final Method readSetter; private final Type writeType; private final Class writeRawType; private final Type readType; @@ -98,16 +102,21 @@ public final class JsonFieldInfo { public JsonFieldInfo( String name, Field writeField, + Method writeGetter, Field readField, + Method readSetter, 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.writeGetter = writeGetter; + this.readField = readField; + this.readSetter = readSetter; + this.writeType = resolveWriteType(writeField, writeGetter); + this.writeRawType = resolveWriteRawType(writeField, writeGetter); + this.readType = resolveReadType(readField, readSetter); + this.readRawType = resolveReadRawType(readField, readSetter); this.writeAccessor = writeAccessor; this.readAccessor = readAccessor; writeKind = writeRawType == null ? null : kind(writeRawType); @@ -181,6 +190,10 @@ public Field writeField() { return writeField; } + public Method writeGetter() { + return writeGetter; + } + public Type writeType() { return writeType; } @@ -226,7 +239,11 @@ public Type readType() { } public Field readField() { - return readAccessor == null ? null : readAccessor.field(); + return readField; + } + + public Method readSetter() { + return readSetter; } public Class readRawType() { @@ -249,6 +266,22 @@ private static Class fieldRawType(Field field) { return field == null ? null : field.getType(); } + private static Type resolveWriteType(Field field, Method getter) { + return getter == null ? fieldType(field) : getter.getGenericReturnType(); + } + + private static Class resolveWriteRawType(Field field, Method getter) { + return getter == null ? fieldRawType(field) : getter.getReturnType(); + } + + private static Type resolveReadType(Field field, Method setter) { + return setter == null ? fieldType(field) : setter.getGenericParameterTypes()[0]; + } + + private static Class resolveReadRawType(Field field, Method setter) { + return setter == null ? fieldRawType(field) : setter.getParameterTypes()[0]; + } + public void resolveTypes(JsonTypeResolver typeResolver) { if (writeRawType != null) { writeTypeInfo = typeResolver.getTypeInfo(writeType, writeRawType); 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 index 1cb3552200..b76d6f8e2c 100644 --- 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 @@ -281,6 +281,10 @@ public final long readLong() { return negative ? result : -result; } + public double readDouble() { + return Double.parseDouble(readNumber()); + } + public int readFieldNameInt() { try { return Integer.parseInt(readString()); 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 index 8a9ae15696..729d7159f1 100644 --- 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 @@ -854,6 +854,37 @@ private boolean tryReadNextRawToken3(long prefix, long prefixMask, int suffix, i return false; } + public boolean tryReadNextFieldNameToken8( + long prefix, long suffix, long suffixMask, int tokenLength) { + return tryReadNextRawToken8(prefix, suffix, suffixMask, tokenLength); + } + + private boolean tryReadNextRawToken8(long prefix, long suffix, long suffixMask, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && LittleEndian.getInt64(bytes, mark) == prefix + && readTokenSuffix(bytes, suffixOffset, tokenLength, suffixMask) == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + private static long readTokenSuffix( + byte[] bytes, int suffixOffset, int tokenLength, long suffixMask) { + if (suffixOffset + Long.BYTES <= bytes.length) { + return LittleEndian.getInt64(bytes, suffixOffset) & suffixMask; + } + int suffixLength = tokenLength - Long.BYTES; + long suffix = 0; + for (int i = 0; i < suffixLength; i++) { + suffix |= (long) (bytes[suffixOffset + i] & 0xFF) << (i << 3); + } + return suffix; + } + private boolean tryReadFieldNameColonAt( int mark, long expectedHash, long expectedMask, int expectedLength) { byte[] bytes = input; 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 index df345945ea..5df2e24528 100644 --- 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 @@ -19,6 +19,11 @@ package org.apache.fory.json.reader; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; @@ -37,6 +42,14 @@ public final class Utf8JsonReader extends JsonReader { private static final int INT_CONTROL_LIMIT_BYTES = 0x20202020; private static final long QUOTE_BYTES = 0x2222222222222222L; private static final int INT_QUOTE_BYTES = 0x22222222; + private static final long LONG_MAX_DIV_10 = Long.MAX_VALUE / 10; + private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); + private static final long LONG_MIN_DIV_10 = Long.MIN_VALUE / 10; + private static final int LONG_MIN_LAST_DIGIT = (int) -(Long.MIN_VALUE % 10); + private static final long EIGHT_DIGITS = 100_000_000L; + private static final long ASCII_ZEROES = 0x3030_3030_3030_3030L; + private static final long ASCII_NINES = 0x3939_3939_3939_3939L; + private static final long ASCII_HIGH_BITS = 0x8080_8080_8080_8080L; // 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. @@ -351,6 +364,39 @@ public long readLongTokenValue() { return readLongToken(); } + public BigDecimal readBigDecimal() { + skipWhitespaceFast(); + return readBigDecimalToken(); + } + + public UUID readUuid() { + skipWhitespaceFast(); + int mark = position; + try { + return readUuidToken(); + } catch (RuntimeException e) { + position = mark; + return UUID.fromString(readStringToken()); + } + } + + @Override + public double readDouble() { + skipWhitespaceFast(); + return readDoubleToken(); + } + + public double readNextDoubleValue() { + if (position < input.length && !isWhitespace(input[position])) { + return readDoubleToken(); + } + return readDouble(); + } + + public double readDoubleTokenValue() { + return readDoubleToken(); + } + private long readLongToken() { byte[] bytes = input; int offset = position; @@ -377,6 +423,16 @@ private long readLongToken() { if (safeEnd > inputLength) { safeEnd = inputLength; } + int block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS + block; + offset += 8; + block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS + block; + offset += 8; + } + } while (offset < safeEnd) { ch = bytes[offset]; if (ch < '0' || ch > '9') { @@ -403,7 +459,7 @@ private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, lon break; } int digit = ch - '0'; - if (result > (Long.MAX_VALUE - digit) / 10) { + if (result > LONG_MAX_DIV_10 || (result == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { position = offset; throw error("Long overflow"); } @@ -416,15 +472,15 @@ private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, lon } private long readNegativeLongToken(int start) { - position = start + 1; - long result = 0; - long limit = Long.MIN_VALUE; - if (position >= input.length) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { throw error("Expected digit"); } - int ch = input[position]; + int ch = bytes[offset]; if (ch == '0') { - position++; + position = offset + 1; rejectLeadingDigitFast(); rejectFractionOrExponentFast(); return 0; @@ -432,27 +488,401 @@ private long readNegativeLongToken(int start) { if (ch < '1' || ch > '9') { throw error("Expected digit"); } - long multmin = limit / 10; - while (position < input.length) { - ch = input[position]; + long result = '0' - ch; + offset++; + int safeEnd = offset + 17; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + int block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS - block; + offset += 8; + block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS - block; + offset += 8; + } + } + while (offset < safeEnd) { + ch = bytes[offset]; if (ch < '0' || ch > '9') { break; } - int digit = ch - '0'; - if (result < multmin) { - throw error("Long overflow"); + result = result * 10 - (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readNegativeLongTail(bytes, offset, inputLength, result); } - result *= 10; - if (result < Long.MIN_VALUE + digit) { + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readNegativeLongTail(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_MIN_DIV_10 || (result == LONG_MIN_DIV_10 && digit > LONG_MIN_LAST_DIGIT)) { + position = offset; throw error("Long overflow"); } - result -= digit; - position++; + result = result * 10 - digit; + offset++; } + position = offset; rejectFractionOrExponentFast(); return result; } + private static int parseEightDigits(byte[] bytes, int offset, int safeEnd) { + if (offset + 8 > safeEnd) { + return -1; + } + // Keep this as one unaligned little-endian load. Eight separate byte loads made the helper too + // large for C2 to place well under generated readers, while the byte-lane math stays compact. + long chunk = LittleEndian.getInt64(bytes, offset); + long digits = chunk - ASCII_ZEROES; + if (((digits | (ASCII_NINES - chunk)) & ASCII_HIGH_BITS) != 0) { + return -1; + } + long pairs = (digits * 10 + (digits >>> 8)) & 0x00FF_00FF_00FF_00FFL; + long quads = (pairs * 100 + (pairs >>> 16)) & 0x0000_FFFF_0000_FFFFL; + return (int) ((quads & 0xFFFF) * 10_000 + (quads >>> 32)); + } + + private BigDecimal readBigDecimalToken() { + byte[] bytes = input; + int offset = position; + int start = offset; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + int ch = bytes[offset]; + if (ch == '-') { + return readSignedBigDecimalToken(start); + } + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + position = offset; + rejectLeadingDigitFast(); + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readBigDecimalFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { + return readBigDecimalFallback(start); + } + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readBigDecimalFallback(start); + } + } + position = offset; + return BigDecimal.valueOf(unscaled, scale); + } + + private BigDecimal readSignedBigDecimalToken(int start) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + int ch = bytes[offset]; + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + position = offset; + rejectLeadingDigitFast(); + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readBigDecimalFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { + return readBigDecimalFallback(start); + } + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readBigDecimalFallback(start); + } + } + position = offset; + return BigDecimal.valueOf(-unscaled, scale); + } + + private BigDecimal readBigDecimalFallback(int start) { + // Keep overflow and exponent forms on the existing string constructor path so the fast path + // only owns decimals that fit exactly as long + scale. + position = start; + return new BigDecimal(readNumber()); + } + + private UUID readUuidToken() { + byte[] bytes = input; + int offset = position; + int start = offset + 1; + if (offset + 38 > bytes.length || bytes[offset] != '"') { + throw new IllegalArgumentException(); + } + if (bytes[start + 8] != '-' + || bytes[start + 13] != '-' + || bytes[start + 18] != '-' + || bytes[start + 23] != '-' + || bytes[start + 36] != '"') { + throw new IllegalArgumentException(); + } + long msb = parseHex(bytes, start, 8); + msb = (msb << 16) | parseHex(bytes, start + 9, 4); + msb = (msb << 16) | parseHex(bytes, start + 14, 4); + long lsb = parseHex(bytes, start + 19, 4); + lsb = (lsb << 48) | parseHex(bytes, start + 24, 12); + position = start + 37; + return new UUID(msb, lsb); + } + + private static long parseHex(byte[] bytes, int offset, int length) { + long value = 0; + for (int i = 0; i < length; i++) { + value = (value << 4) | hexValue(bytes[offset + i]); + } + return value; + } + + private static int hexValue(int ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + int lower = ch | 0x20; + if (lower >= 'a' && lower <= 'f') { + return lower - 'a' + 10; + } + throw new IllegalArgumentException(); + } + + private double readDoubleToken() { + // Keep the fast path exact: compact plain decimals convert through BigDecimal's long+scale + // path, while exponents, overflow, and longer precision stay on Java's full parser. + byte[] bytes = input; + int offset = position; + int start = offset; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + int ch = bytes[offset]; + if (ch == '-') { + return readSignedDoubleToken(start); + } + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readDoubleFallback(start); + } + } + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readDoubleFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { + return readDoubleFallback(start); + } + } + return finishDoubleToken(bytes, offset, inputLength, start, unscaled, scale); + } + + private double readSignedDoubleToken(int start) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + int ch = bytes[offset]; + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readDoubleFallback(start); + } + } + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readDoubleFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { + return readDoubleFallback(start); + } + } + return finishSignedDoubleToken(bytes, offset, inputLength, start, unscaled, scale); + } + + private double finishDoubleToken( + byte[] bytes, int offset, int inputLength, int start, long unscaled, int scale) { + if (offset < inputLength) { + int ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readDoubleFallback(start); + } + } + position = offset; + return BigDecimal.valueOf(unscaled, scale).doubleValue(); + } + + private double finishSignedDoubleToken( + byte[] bytes, int offset, int inputLength, int start, long unscaled, int scale) { + if (offset < inputLength) { + int ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readDoubleFallback(start); + } + } + position = offset; + if (unscaled == 0) { + return -0.0d; + } + return BigDecimal.valueOf(-unscaled, scale).doubleValue(); + } + + private double readDoubleFallback(int start) { + position = start; + return Double.parseDouble(readNumber()); + } + @Override public int readFieldNameInt() { skipWhitespaceFast(); @@ -623,6 +1053,28 @@ public String readNullableStringToken() { return readStringToken(); } + public LocalDate readIsoLocalDate() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoLocalDateToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + + public OffsetDateTime readIsoOffsetDateTime() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoOffsetDateTimeToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + private String readStringToken() { byte[] bytes = input; int inputLength = bytes.length; @@ -680,6 +1132,187 @@ private String readStringToken() { throw error("Unterminated string"); } + private LocalDate readIsoLocalDateToken() { + byte[] bytes = input; + int offset = position; + int length = bytes.length; + if (offset + 12 > length || bytes[offset++] != '"') { + throw error("Expected string"); + } + int dateStart = offset; + if (bytes[dateStart + 4] != '-' || bytes[dateStart + 7] != '-') { + throw new IllegalArgumentException(); + } + int year = parse4(bytes, dateStart); + int month = parse2(bytes, dateStart + 5); + int day = parse2(bytes, dateStart + 8); + int end = dateStart + 10; + int ch = bytes[end]; + if (ch == '"') { + position = end + 1; + return LocalDate.of(year, month, day); + } + if (ch == 'T') { + position = scanSimpleStringTail(bytes, end + 1); + return LocalDate.of(year, month, day); + } + throw new IllegalArgumentException(); + } + + private OffsetDateTime readIsoOffsetDateTimeToken() { + byte[] bytes = input; + int offset = position; + int length = bytes.length; + if (offset + 19 > length || bytes[offset++] != '"') { + throw error("Expected string"); + } + int start = offset; + if (bytes[start + 4] != '-' + || bytes[start + 7] != '-' + || bytes[start + 10] != 'T' + || bytes[start + 13] != ':') { + throw new IllegalArgumentException(); + } + int year = parse4(bytes, start); + int month = parse2(bytes, start + 5); + int day = parse2(bytes, start + 8); + int hour = parse2(bytes, start + 11); + int minute = parse2(bytes, start + 14); + return readIsoOffsetDateTimeTail(bytes, start + 16, length, year, month, day, hour, minute); + } + + private OffsetDateTime readIsoOffsetDateTimeTail( + byte[] bytes, int index, int length, int year, int month, int day, int hour, int minute) { + int second = 0; + int nano = 0; + if (index < length && bytes[index] == ':') { + second = parse2(bytes, index + 1); + index += 3; + if (index < length && bytes[index] == '.') { + int fractionStart = index + 1; + int fractionEnd = fractionStart; + while (fractionEnd < length && isDigit(bytes[fractionEnd])) { + fractionEnd++; + } + if (fractionEnd == fractionStart || fractionEnd - fractionStart > 9) { + throw new IllegalArgumentException(); + } + nano = parseNano(bytes, fractionStart, fractionEnd); + index = fractionEnd; + } + } + if (index < length && bytes[index] == 'Z') { + if (index + 1 >= length || bytes[index + 1] != '"') { + throw new IllegalArgumentException(); + } + position = index + 2; + return OffsetDateTime.of(year, month, day, hour, minute, second, nano, ZoneOffset.UTC); + } + return readIsoOffsetDateTimeOffsetTail( + bytes, index, length, year, month, day, hour, minute, second, nano); + } + + private OffsetDateTime readIsoOffsetDateTimeOffsetTail( + byte[] bytes, + int index, + int length, + int year, + int month, + int day, + int hour, + int minute, + int second, + int nano) { + long offsetAndEnd = parseOffsetAndEnd(bytes, index, length); + position = (int) offsetAndEnd; + return OffsetDateTime.of( + year, + month, + day, + hour, + minute, + second, + nano, + ZoneOffset.ofTotalSeconds((int) (offsetAndEnd >> 32))); + } + + private int scanSimpleStringTail(byte[] bytes, int offset) { + int length = bytes.length; + while (offset < length) { + int b = bytes[offset++]; + if (b == '"') { + return offset; + } + if (b == '\\' || b < 0x20 || b < 0) { + throw new IllegalArgumentException(); + } + } + throw error("Unterminated string"); + } + + private static long parseOffsetAndEnd(byte[] bytes, int index, int length) { + int offset = bytes[index]; + if (offset == 'Z') { + if (index + 1 >= length || bytes[index + 1] != '"') { + throw new IllegalArgumentException(); + } + return ((long) (index + 2)) & 0xFFFF_FFFFL; + } + if (offset != '+' && offset != '-') { + throw new IllegalArgumentException(); + } + if (index + 6 >= length || bytes[index + 3] != ':') { + throw new IllegalArgumentException(); + } + int hour = parse2(bytes, index + 1); + int minute = parse2(bytes, index + 4); + int second = 0; + int end = index + 6; + if (bytes[end] == ':') { + if (end + 3 >= length) { + throw new IllegalArgumentException(); + } + second = parse2(bytes, end + 1); + end += 3; + } + if (bytes[end] != '"') { + throw new IllegalArgumentException(); + } + int total = hour * 3600 + minute * 60 + second; + if (offset == '-') { + total = -total; + } + return ((long) total << 32) | ((long) (end + 1) & 0xFFFF_FFFFL); + } + + private static int parseNano(byte[] bytes, int start, int end) { + int nano = 0; + for (int i = start; i < end; i++) { + nano = nano * 10 + bytes[i] - '0'; + } + for (int i = end - start; i < 9; i++) { + nano *= 10; + } + return nano; + } + + private static int parse4(byte[] bytes, int index) { + return parse2(bytes, index) * 100 + parse2(bytes, index + 2); + } + + private static int parse2(byte[] bytes, int index) { + int high = bytes[index] - '0'; + int low = bytes[index + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + throw new IllegalArgumentException(); + } + return high * 10 + low; + } + + private static boolean isDigit(byte b) { + return b >= '0' && b <= '9'; + } + private String readStringStop(int start, int stop, int b) { position = stop + 1; if (b >= 0 && b < 0x20) { @@ -704,27 +1337,18 @@ private StringBuilder newStringBuilder(int start, int stop) { } 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); + // UTF-8 mode stops on every high-bit byte, and readStringToken only uses the first stop bit. + // Subtraction borrow may only create later high bits after an earlier real stop, so the + // compact syntax/range expression preserves the first-stop position. Latin1JsonReader cannot + // use this shortcut because high-bit Latin-1 bytes are valid string payload. + long syntaxStop = ((word ^ QUOTE_BYTES) - BYTE_ONES) | ((word ^ BACKSLASH_BYTES) - BYTE_ONES); + return (syntaxStop | word | (word - CONTROL_LIMIT_BYTES)) & 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; + int syntaxStop = + ((word ^ INT_QUOTE_BYTES) - INT_BYTE_ONES) | ((word ^ INT_BACKSLASH_BYTES) - INT_BYTE_ONES); + return (syntaxStop | word | (word - INT_CONTROL_LIMIT_BYTES)) & INT_BYTE_HIGH_BITS; } @Override @@ -860,6 +1484,37 @@ private boolean tryReadNextRawToken3(long prefix, long prefixMask, int suffix, i return false; } + public boolean tryReadNextFieldNameToken8( + long prefix, long suffix, long suffixMask, int tokenLength) { + return tryReadNextRawToken8(prefix, suffix, suffixMask, tokenLength); + } + + private boolean tryReadNextRawToken8(long prefix, long suffix, long suffixMask, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && LittleEndian.getInt64(bytes, mark) == prefix + && readTokenSuffix(bytes, suffixOffset, tokenLength, suffixMask) == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + private static long readTokenSuffix( + byte[] bytes, int suffixOffset, int tokenLength, long suffixMask) { + if (suffixOffset + Long.BYTES <= bytes.length) { + return LittleEndian.getInt64(bytes, suffixOffset) & suffixMask; + } + int suffixLength = tokenLength - Long.BYTES; + long suffix = 0; + for (int i = 0; i < suffixLength; i++) { + suffix |= (long) (bytes[suffixOffset + i] & 0xFF) << (i << 3); + } + return suffix; + } + private boolean tryReadFieldNameColonAt( int mark, long expectedHash, long expectedMask, int expectedLength) { byte[] bytes = input; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java index 4fe557959e..8bdb3c3e26 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java @@ -56,8 +56,11 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Pattern; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.ArrayCodec; @@ -79,10 +82,15 @@ public final class JsonSharedRegistry { private final CodecRegistry customCodecs; private final IdentityHashMap, JsonCodec> exactCodecs; private final JsonCodegen codegen; + private final boolean propertyDiscoveryEnabled; public JsonSharedRegistry( - boolean codegenEnabled, boolean writeNullFields, CodecRegistry customCodecs) { + boolean codegenEnabled, + boolean writeNullFields, + boolean propertyDiscoveryEnabled, + CodecRegistry customCodecs) { this.customCodecs = customCodecs.copy(); + this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; exactCodecs = new IdentityHashMap<>(); codegen = codegenEnabled ? new JsonCodegen(writeNullFields) : null; registerExactCodecs(); @@ -116,6 +124,10 @@ public JsonCodec createCodec( return new ScalarCodecs.AtomicReferenceCodec( CodecUtils.elementType(typeRef.getType()), localResolver); } + if (rawType == AtomicReferenceArray.class) { + return new ScalarCodecs.AtomicReferenceArrayCodec( + CodecUtils.elementType(typeRef.getType()), localResolver); + } if (Calendar.class.isAssignableFrom(rawType)) { return ScalarCodecs.CalendarCodec.INSTANCE; } @@ -190,6 +202,10 @@ public ObjectCodecs compileObject(BaseObjectCodec codec, JsonTypeResolver localR return codegen == null ? null : codegen.compile(codec, localResolver); } + boolean propertyDiscoveryEnabled() { + return propertyDiscoveryEnabled; + } + private void registerExactCodecs() { exactCodecs.put(Object.class, ScalarCodecs.NaturalCodec.INSTANCE); exactCodecs.put(void.class, ScalarCodecs.VoidCodec.INSTANCE); @@ -219,7 +235,9 @@ private void registerExactCodecs() { exactCodecs.put(StringBuffer.class, ScalarCodecs.StringBufferCodec.INSTANCE); exactCodecs.put(AtomicBoolean.class, ScalarCodecs.AtomicBooleanCodec.INSTANCE); exactCodecs.put(AtomicInteger.class, ScalarCodecs.AtomicIntegerCodec.INSTANCE); + exactCodecs.put(AtomicIntegerArray.class, ScalarCodecs.AtomicIntegerArrayCodec.INSTANCE); exactCodecs.put(AtomicLong.class, ScalarCodecs.AtomicLongCodec.INSTANCE); + exactCodecs.put(AtomicLongArray.class, ScalarCodecs.AtomicLongArrayCodec.INSTANCE); exactCodecs.put(Currency.class, ScalarCodecs.CurrencyCodec.INSTANCE); exactCodecs.put(File.class, ScalarCodecs.FileCodec.INSTANCE); exactCodecs.put(URI.class, ScalarCodecs.UriCodec.INSTANCE); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 0b23bbb38b..8b0e43e744 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -81,7 +81,7 @@ private BaseObjectCodec buildObjectCodec(Class type) { if (cached != null) { return cached; } - ObjectCodec codec = BaseObjectCodec.build(type); + ObjectCodec codec = BaseObjectCodec.build(type, sharedRegistry.propertyDiscoveryEnabled()); // 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); 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 index ebe10dcffc..c273195f9d 100644 --- 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 @@ -30,7 +30,8 @@ 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; + // Pooled writers should retain medium buffers to avoid reallocating common JSON outputs. + private static final int RETAINED_CAPACITY = 64 * 1024; 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); 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 index 5ba1ec5e08..218016b163 100644 --- 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 @@ -19,28 +19,44 @@ package org.apache.fory.json.writer; +import java.io.IOException; +import java.io.OutputStream; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.UUID; 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; + // Pooled writers should retain medium buffers to avoid reallocating common JSON outputs. + private static final int RETAINED_CAPACITY = 64 * 1024; 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 EIGHT_DIGITS = 100_000_000L; private static final long HIGH_BITS = 0x8080808080808080L; private static final int INT_HIGH_BITS = 0x80808080; + private static final int SHORT_HIGH_BITS = 0x8080; private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; private static final int INT_ASCII_CONTROL_OFFSET = 0x60606060; + private static final int SHORT_ASCII_CONTROL_OFFSET = 0x6060; private static final long ONE_BYTES = 0x0101010101010101L; private static final int INT_ONE_BYTES = 0x01010101; + private static final int SHORT_ONE_BYTES = 0x0101; + private static final byte[] HEX_DIGITS = + "0123456789abcdef".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); private static final long QUOTE_BYTES_COMPLEMENT = ~0x2222222222222222L; private static final int INT_QUOTE_BYTES_COMPLEMENT = ~0x22222222; + private static final int SHORT_QUOTE_BYTES_COMPLEMENT = ~0x2222; private static final long BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C5C5C5C5CL; private static final int INT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C; + private static final int SHORT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C; 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]; @@ -88,6 +104,14 @@ public byte[] toJsonBytes() { return Arrays.copyOf(buffer, position); } + public void writeTo(OutputStream output) { + try { + output.write(buffer, 0, position); + } catch (IOException e) { + throw new ForyJsonException("Cannot write JSON output", e); + } + } + @Override public void writeNull() { writeAscii("null"); @@ -115,16 +139,7 @@ public void writeLong(long value) { 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); + writePositiveLongNoEnsure(value); } @Override @@ -132,7 +147,7 @@ public void writeFloat(float value) { if (!Float.isFinite(value)) { throw new ForyJsonException("JSON does not support non-finite float " + value); } - writeAscii(Float.toString(value)); + writeAsciiNumber(Float.toString(value)); } @Override @@ -140,12 +155,12 @@ public void writeDouble(double value) { if (!Double.isFinite(value)) { throw new ForyJsonException("JSON does not support non-finite double " + value); } - writeAscii(Double.toString(value)); + writeAsciiNumber(Double.toString(value)); } @Override public void writeNumber(String value) { - writeAscii(value); + writeAsciiNumber(value); } @Override @@ -176,6 +191,61 @@ public void writeString(String value) { writeStringChars(value); } + public void writeUuid(UUID value) { + ensure(38); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + long high = value.getMostSignificantBits(); + pos = writeHex(bytes, pos, high, 60, 8); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, high, 28, 4); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, high, 12, 4); + long low = value.getLeastSignificantBits(); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, low, 60, 4); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, low, 44, 12); + bytes[pos++] = (byte) '"'; + position = pos; + } + + public void writeLocalDate(LocalDate value) { + int year = value.getYear(); + if (year < 0 || year > 9999) { + writeString(value.toString()); + return; + } + ensure(12); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + pos = writeDateParts(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + bytes[pos++] = (byte) '"'; + position = pos; + } + + public void writeOffsetDateTime(OffsetDateTime value) { + int year = value.getYear(); + if (year < 0 || year > 9999 || value.getOffset().getTotalSeconds() != 0) { + writeString(value.toString()); + return; + } + ensure(32); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + pos = writeDateParts(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + bytes[pos++] = (byte) 'T'; + pos = + writeTime( + bytes, pos, value.getHour(), value.getMinute(), value.getSecond(), value.getNano()); + bytes[pos++] = (byte) 'Z'; + bytes[pos++] = (byte) '"'; + position = pos; + } + private void writeStringChars(String value) { int length = value.length(); ensure(length + 2); @@ -264,6 +334,12 @@ public void writeIntField(byte[] prefix, int value) { writeIntNoEnsure(value); } + public void writeIntField(long prefix0, long prefix1, int prefixLength, int value) { + ensurePackedPrefix(prefixLength, 11); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeIntNoEnsure(value); + } + public void writeObjectIntField(byte[] namePrefix, int value) { ensure(namePrefix.length + 12); buffer[position++] = (byte) '{'; @@ -271,6 +347,13 @@ public void writeObjectIntField(byte[] namePrefix, int value) { writeIntNoEnsure(value); } + public void writeObjectIntField(long prefix0, long prefix1, int prefixLength, int value) { + ensurePackedPrefix(prefixLength, 12); + buffer[position++] = (byte) '{'; + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeIntNoEnsure(value); + } + public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, long value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeLongField(prefix, value); @@ -282,6 +365,12 @@ public void writeLongField(byte[] prefix, long value) { writeLongNoEnsure(value); } + public void writeLongField(long prefix0, long prefix1, int prefixLength, long value) { + ensurePackedPrefix(prefixLength, 20); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeLongNoEnsure(value); + } + public void writeObjectLongField(byte[] namePrefix, long value) { ensure(namePrefix.length + 21); buffer[position++] = (byte) '{'; @@ -289,6 +378,13 @@ public void writeObjectLongField(byte[] namePrefix, long value) { writeLongNoEnsure(value); } + public void writeObjectLongField(long prefix0, long prefix1, int prefixLength, long value) { + ensurePackedPrefix(prefixLength, 21); + buffer[position++] = (byte) '{'; + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeLongNoEnsure(value); + } + public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeStringField(prefix, value); @@ -318,12 +414,79 @@ public void writeStringField(byte[] prefix, String value) { writeStringFieldChars(prefix, value); } + public void writeStringField(long prefix0, long prefix1, int prefixLength, String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + int start = position; + if (StringSerializer.isLatin1Coder(stringCoder)) { + ensurePackedPrefix(prefixLength, bytes.length + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + position = start; + } else if (StringSerializer.isUtf16Coder(stringCoder)) { + ensurePackedPrefix(prefixLength, (bytes.length >> 1) * 3 + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + position = start; + } + } + writeStringFieldChars(prefix0, prefix1, prefixLength, value); + } + private void writeStringFieldChars(byte[] prefix, String value) { ensure(prefix.length + value.length() * 3 + 2); writeRawNoEnsure(prefix); writeStringNoEnsure(value); } + private void writeStringFieldChars(long prefix0, long prefix1, int prefixLength, String value) { + ensurePackedPrefix(prefixLength, value.length() * 3 + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeStringNoEnsure(value); + } + + public void writeStringCollection(Collection values) { + writeArrayStart(); + if (values.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) values; + for (int i = 0, size = list.size(); i < size; i++) { + writeStringElement(i, list.get(i)); + } + } else { + int index = 0; + for (String value : values) { + writeStringElement(index++, value); + } + } + writeArrayEnd(); + } + + public void writeStringArray(String[] values) { + writeArrayStart(); + for (int i = 0; i < values.length; i++) { + writeStringElement(i, values[i]); + } + writeArrayEnd(); + } + + public void writeLongArray(long[] values) { + ensure(2); + buffer[position++] = '['; + for (int i = 0; i < values.length; i++) { + ensure(22); + if (i != 0) { + buffer[position++] = ','; + } + writeLongNoEnsure(values[i]); + } + buffer[position++] = ']'; + } + public void writeStringElement(int index, String value) { int comma = index == 0 ? 0 : 1; if (value == null) { @@ -377,6 +540,11 @@ public void writeRawValue(byte[] value) { writeRaw(value); } + public void writeRawValue(long prefix0, long prefix1, int prefixLength) { + ensure(packedPrefixSize(prefixLength)); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + } + @Override public void writeObjectStart() { writeByteRaw((byte) '{'); @@ -407,11 +575,22 @@ public void writeComma(int index) { private boolean writeLatin1String(byte[] value) { int length = value.length; ensure(length + 2); - return writeLatin1StringNoEnsure(value); + return writeLatin1StringNoEnsure(value, length); } private boolean writeLatin1StringNoEnsure(byte[] value) { int length = value.length; + return writeLatin1StringNoEnsure(value, length); + } + + private boolean writeLatin1StringNoEnsure(byte[] value, int length) { + if (length <= 32) { + return writeShortLatin1StringNoEnsure(value, length); + } + return writeLongLatin1StringNoEnsure(value, length); + } + + private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { byte[] bytes = buffer; int start = position; int pos = start; @@ -445,6 +624,15 @@ private boolean writeLatin1StringNoEnsure(byte[] value) { i += 4; } } + if (i + 2 <= length) { + int word = (value[i] & 0xFF) | ((value[i + 1] & 0xFF) << 8); + if (isJsonAsciiShort(word)) { + bytes[pos] = (byte) word; + bytes[pos + 1] = (byte) (word >>> 8); + pos += 2; + i += 2; + } + } for (; i < length; i++) { byte ch = value[i]; if (isJsonAsciiByte(ch)) { @@ -459,6 +647,68 @@ private boolean writeLatin1StringNoEnsure(byte[] value) { return true; } + private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { + byte[] bytes = buffer; + int start = position; + int pos = start; + bytes[pos++] = (byte) '"'; + int i = 0; + if (length > 15) { + long word0 = LittleEndian.getInt64(value, 0); + long word1 = LittleEndian.getInt64(value, 8); + if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + pos += 16; + i = 16; + } + if (i + 8 <= length) { + long word = LittleEndian.getInt64(value, i); + if (!isJsonAsciiWord(word)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos, word); + pos += 8; + i += 8; + } + if (i + 4 <= length) { + int word = LittleEndian.getInt32(value, i); + if (!isJsonAsciiInt(word)) { + position = start; + return false; + } + LittleEndian.putInt32(bytes, pos, word); + pos += 4; + i += 4; + } + if (i + 2 <= length) { + int word = (value[i] & 0xFF) | ((value[i + 1] & 0xFF) << 8); + if (!isJsonAsciiShort(word)) { + position = start; + return false; + } + bytes[pos] = (byte) word; + bytes[pos + 1] = (byte) (word >>> 8); + pos += 2; + i += 2; + } + if (i < length) { + byte ch = value[i]; + if (!isJsonAsciiByte(ch)) { + position = start; + return false; + } + bytes[pos++] = ch; + } + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + private boolean writeUtf16String(byte[] value) { int length = value.length; ensure((length >> 1) * 3 + 2); @@ -657,6 +907,15 @@ private void writeAsciiNoEnsure(String value) { } } + private void writeAsciiNumber(String value) { + if (STRING_BYTES_BACKED + && StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(value))) { + writeRaw(StringSerializer.getStringBytes(value)); + return; + } + writeAscii(value); + } + private void writeRaw(byte[] bytes) { ensure(bytes.length); writeRawNoEnsure(bytes); @@ -667,19 +926,27 @@ private void writeRawNoEnsure(byte[] bytes) { position += bytes.length; } + private void writePackedRawNoEnsure(long prefix0, long prefix1, int prefixLength) { + LittleEndian.putInt64(buffer, position, prefix0); + if (prefixLength > Long.BYTES) { + LittleEndian.putInt64(buffer, position + Long.BYTES, prefix1); + } + position += prefixLength; + } + + private void ensurePackedPrefix(int prefixLength, int additionalAfterPrefix) { + ensure(Math.max(packedPrefixSize(prefixLength), prefixLength + additionalAfterPrefix)); + } + + private static int packedPrefixSize(int prefixLength) { + return prefixLength <= Long.BYTES ? Long.BYTES : Long.BYTES * 2; + } + 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) { @@ -721,6 +988,14 @@ private static boolean isJsonAsciiInt(int word) { == INT_HIGH_BITS; } + private static boolean isJsonAsciiShort(int word) { + return (((word + SHORT_ASCII_CONTROL_OFFSET) & ~word) & SHORT_HIGH_BITS) == SHORT_HIGH_BITS + && (((word ^ SHORT_QUOTE_BYTES_COMPLEMENT) + SHORT_ONE_BYTES) & SHORT_HIGH_BITS) + == SHORT_HIGH_BITS + && (((word ^ SHORT_BACKSLASH_BYTES_COMPLEMENT) + SHORT_ONE_BYTES) & SHORT_HIGH_BITS) + == SHORT_HIGH_BITS; + } + private static int packUtf16Ascii(long word) { return (int) ((word & 0xFFL) @@ -750,40 +1025,124 @@ private void writeLongNoEnsure(long value) { buffer[position++] = (byte) '-'; value = -value; } + writePositiveLongNoEnsure(value); + } + + private void writePositiveIntNoEnsure(int value) { + position = writePositiveInt(buffer, position, value); + } + + private void writePositiveLongNoEnsure(long 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; + long high = value / EIGHT_DIGITS; + int low = (int) (value - high * EIGHT_DIGITS); + if (high <= Integer.MAX_VALUE) { + pos = writePositiveInt(bytes, pos, (int) high); + } else { + long top = high / EIGHT_DIGITS; + int middle = (int) (high - top * EIGHT_DIGITS); + pos = writePositiveInt(bytes, pos, (int) top); + pos = writePadded8Digits(bytes, pos, middle); + } + position = writePadded8Digits(bytes, pos, low); + } + + private static int writePositiveInt(byte[] bytes, int pos, int value) { if (value < 10000) { - position = writeIntUpTo4(bytes, pos, value); - return; + return writeIntUpTo4(bytes, pos, value); } int high = divide10000(value); int low = value - high * 10000; if (high < 10000) { if (high >= 1000) { - position = writePadded8(bytes, pos, high, low); - return; + return writePadded8(bytes, pos, high, low); } pos = writeIntUpTo4(bytes, pos, high); - position = writePadded4(bytes, pos, low); - return; + return writePadded4(bytes, pos, low); } int top = divide10000(high); int middle = high - top * 10000; pos = writeIntUpTo4(bytes, pos, top); - position = writePadded8(bytes, pos, middle, low); + return writePadded8(bytes, pos, middle, low); + } + + private static int writePadded8Digits(byte[] bytes, int pos, int value) { + int high = divide10000(value); + int low = value - high * 10000; + return writePadded8(bytes, pos, high, low); + } + + private static int writeHex(byte[] bytes, int pos, long value, int shift, int count) { + for (int i = 0; i < count; i++) { + bytes[pos++] = HEX_DIGITS[(int) ((value >>> shift) & 0xF)]; + shift -= 4; + } + return pos; + } + + private static int writeDateParts(byte[] bytes, int pos, int year, int month, int day) { + pos = writePadded4(bytes, pos, year); + bytes[pos++] = (byte) '-'; + pos = writeTwoDigits(bytes, pos, month); + bytes[pos++] = (byte) '-'; + return writeTwoDigits(bytes, pos, day); + } + + private static int writeTime(byte[] bytes, int pos, int hour, int minute, int second, int nano) { + pos = writeTwoDigits(bytes, pos, hour); + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, minute); + if (second != 0 || nano != 0) { + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, second); + if (nano != 0) { + bytes[pos++] = (byte) '.'; + pos = writeNano(bytes, pos, nano); + } + } + return pos; + } + + private static int writeNano(byte[] bytes, int pos, int nano) { + if (nano % 1_000_000 == 0) { + return writePadded3(bytes, pos, nano / 1_000_000); + } + if (nano % 1000 == 0) { + int micros = nano / 1000; + int high = micros / 1000; + int low = micros - high * 1000; + pos = writePadded3(bytes, pos, high); + return writePadded3(bytes, pos, low); + } + int first = nano / 100000000; + int rem = nano - first * 100000000; + int middle = rem / 10000; + int low = rem - middle * 10000; + bytes[pos++] = (byte) ('0' + first); + pos = writePadded4(bytes, pos, middle); + return writePadded4(bytes, pos, low); + } + + private static int writePadded3(byte[] bytes, int pos, int value) { + int high = value / 100; + int rem = value - high * 100; + int middle = rem / 10; + bytes[pos++] = (byte) ('0' + high); + bytes[pos++] = (byte) ('0' + middle); + bytes[pos++] = (byte) ('0' + (rem - middle * 10)); + return pos; + } + + private static int writeTwoDigits(byte[] bytes, int pos, int value) { + int high = value / 10; + bytes[pos++] = (byte) ('0' + high); + bytes[pos++] = (byte) ('0' + (value - high * 10)); + return pos; } private static int divide10000(int value) { 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 index 43bec41c67..d441b9de3a 100644 --- 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 @@ -31,6 +31,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.concurrent.atomic.AtomicLongArray; +import java.util.concurrent.atomic.AtomicReferenceArray; import org.apache.fory.json.data.FastContainers; import org.apache.fory.json.data.MapKeyFields; import org.apache.fory.json.data.Nested; @@ -119,6 +122,27 @@ public void parsedContainersStartSmall() { assertTrue(list instanceof ArrayList); assertEquals(arrayCapacity((ArrayList) list), 1); + List strings = + json.fromJson( + "[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\"]".getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}); + assertTrue(strings instanceof ArrayList); + assertEquals(strings, Arrays.asList("a", "b", "c", "d", "e", "f", "g")); + assertEquals(arrayCapacity((ArrayList) strings), 7); + + List notes = + json.fromJson( + ("[{\"title\":\"a\"},{\"title\":\"b\"},null,{\"title\":\"d\"}," + + "{\"title\":\"e\"},{\"title\":\"f\"},{\"title\":\"g\"}]") + .getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}); + assertTrue(notes instanceof ArrayList); + assertEquals(notes.size(), 7); + assertEquals(notes.get(0).title, "a"); + assertEquals(notes.get(2), null); + assertEquals(notes.get(6).title, "g"); + assertEquals(arrayCapacity((ArrayList) notes), 7); + JSONObject object = json.fromJson("{\"x\":1}", JSONObject.class); assertEquals(mapCapacity(object), 2); assertEquals(mapCapacity(json.fromJson("{}", JSONObject.class)), 0); @@ -261,6 +285,174 @@ public void readPrimitiveArrayRoots() { assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", int[].class)); } + @Test + public void readUtf8StringArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals( + json.fromJson("[\"a\"]".getBytes(StandardCharsets.UTF_8), String[].class), + new String[] {"a"}); + assertEquals( + json.fromJson( + "[\"a\",null,\"b\",\"c\",\"d\",\"e\",\"f\",\"g\"]".getBytes(StandardCharsets.UTF_8), + String[].class), + new String[] {"a", null, "b", "c", "d", "e", "f", "g"}); + assertEquals( + json.fromJson( + "[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\",\"h\",\"i\"]" + .getBytes(StandardCharsets.UTF_8), + String[].class), + new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i"}); + } + + @Test + public void readUtf8LongArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals( + json.fromJson("[7]".getBytes(StandardCharsets.UTF_8), long[].class), new long[] {7L}); + assertEquals( + json.fromJson("[1,2,3,4,5,6,7,8]".getBytes(StandardCharsets.UTF_8), long[].class), + new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L}); + assertEquals( + json.fromJson("[1,2,3,4,5,6,7,8,9]".getBytes(StandardCharsets.UTF_8), long[].class), + new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L}); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("[1,null]".getBytes(StandardCharsets.UTF_8), long[].class)); + } + + @Test + public void writeReadBoxedPrimitiveArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new Integer[] {1, null, -2}), "[1,null,-2]"); + assertEquals( + json.fromJson("[1,null,-2]".getBytes(StandardCharsets.UTF_8), Integer[].class), + new Integer[] {1, null, -2}); + assertEquals( + json.fromJson("[9223372036854775807,null,-9]", Long[].class), + new Long[] {Long.MAX_VALUE, null, -9L}); + assertEquals( + json.fromJson("[true,null,false]".getBytes(StandardCharsets.UTF_8), Boolean[].class), + new Boolean[] {Boolean.TRUE, null, Boolean.FALSE}); + assertEquals( + json.fromJson("[32767,null,-32768]", Short[].class), + new Short[] {Short.MAX_VALUE, null, Short.MIN_VALUE}); + assertEquals( + json.fromJson("[127,null,-128]", Byte[].class), + new Byte[] {Byte.MAX_VALUE, null, Byte.MIN_VALUE}); + assertEquals( + json.fromJson("[\"a\",null,\"你\"]", Character[].class), new Character[] {'a', null, '你'}); + assertEquals( + json.fromJson("[1.5,null,-2.25]", Float[].class), new Float[] {1.5f, null, -2.25f}); + assertEquals( + json.fromJson("[1.5,null,-2.25]".getBytes(StandardCharsets.UTF_8), Double[].class), + new Double[] {1.5d, null, -2.25d}); + assertEquals( + new String(json.toJsonBytes(new Character[] {'x', null, '文'}), StandardCharsets.UTF_8), + "[\"x\",null,\"文\"]"); + + assertThrows(ForyJsonException.class, () -> json.fromJson("[128]", Byte[].class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("[\"ab\"]", Character[].class)); + } + + @Test + public void readReferenceObjectArrays() { + ForyJson json = ForyJson.builder().build(); + Note[] notes = json.fromJson("[{\"title\":\"one\"},null,{\"title\":\"two\"}]", Note[].class); + assertEquals(notes.getClass(), Note[].class); + assertEquals(notes.length, 3); + assertEquals(notes[0].title, "one"); + assertEquals(notes[1], null); + assertEquals(notes[2].title, "two"); + + Note[] empty = json.fromJson("[]".getBytes(StandardCharsets.UTF_8), Note[].class); + assertEquals(empty.getClass(), Note[].class); + assertEquals(empty.length, 0); + + Note[][] grid = + json.fromJson( + "[[{\"title\":\"left\"}],null,[{\"title\":\"right\"},null]]" + .getBytes(StandardCharsets.UTF_8), + Note[][].class); + assertEquals(grid.getClass(), Note[][].class); + assertEquals(grid.length, 3); + assertEquals(grid[0].getClass(), Note[].class); + assertEquals(grid[0][0].title, "left"); + assertEquals(grid[1], null); + assertEquals(grid[2].getClass(), Note[].class); + assertEquals(grid[2][0].title, "right"); + assertEquals(grid[2][1], null); + } + + @Test + public void readReentrantObjectArray() { + ForyJson json = ForyJson.builder().build(); + ArrayNode[] roots = + json.fromJson( + "[{\"name\":\"root\",\"children\":[{\"name\":\"leaf\",\"children\":[]}]}]" + .getBytes(StandardCharsets.UTF_8), + ArrayNode[].class); + assertEquals(roots.getClass(), ArrayNode[].class); + assertEquals(roots.length, 1); + assertEquals(roots[0].name, "root"); + assertEquals(roots[0].children.getClass(), ArrayNode[].class); + assertEquals(roots[0].children.length, 1); + assertEquals(roots[0].children[0].name, "leaf"); + assertEquals(roots[0].children[0].children.getClass(), ArrayNode[].class); + assertEquals(roots[0].children[0].children.length, 0); + + ArrayNode[] deep = + json.fromJson(arrayNodeJson(9).getBytes(StandardCharsets.UTF_8), ArrayNode[].class); + assertEquals(deep.getClass(), ArrayNode[].class); + assertEquals(deep.length, 1); + ArrayNode node = deep[0]; + for (int i = 0; i < 9; i++) { + assertEquals(node.name, "node-" + i); + assertEquals(node.children.getClass(), ArrayNode[].class); + if (i == 8) { + assertEquals(node.children.length, 0); + } else { + assertEquals(node.children.length, 1); + node = node.children[0]; + } + } + } + + @Test + public void writeReadAtomicArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new AtomicIntegerArray(new int[] {1, -2, 3})), "[1,-2,3]"); + assertAtomicInts(json.fromJson("[1,-2,3]", AtomicIntegerArray.class), 1, -2, 3); + assertEquals( + new String( + json.toJsonBytes(new AtomicLongArray(new long[] {4L, -5L})), StandardCharsets.UTF_8), + "[4,-5]"); + assertAtomicLongs( + json.fromJson("[4,-5]".getBytes(StandardCharsets.UTF_8), AtomicLongArray.class), 4L, -5L); + + AtomicReferenceArray refs = + new AtomicReferenceArray<>(new String[] {"a", null, ZH_TEXT}); + assertEquals(json.toJson(refs), "[\"a\",null,\"你好,Fory\"]"); + assertAtomicRefs( + json.fromJson( + "[\"a\",null,\"你好,Fory\"]".getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}), + "a", + null, + ZH_TEXT); + + String fieldsJson = "{\"ints\":[7,8],\"longs\":[9,-10],\"refs\":[\"b\",null]}"; + AtomicArrayFields fields = + json.fromJson(fieldsJson.getBytes(StandardCharsets.UTF_8), AtomicArrayFields.class); + assertAtomicInts(fields.ints, 7, 8); + assertAtomicLongs(fields.longs, 9L, -10L); + assertAtomicRefs(fields.refs, "b", null); + assertEquals(json.toJson(fields), fieldsJson); + + assertThrows( + ForyJsonException.class, () -> json.fromJson("[1,null]", AtomicIntegerArray.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", AtomicLongArray.class)); + } + public static final class Shelf { public NoteList notes; } @@ -271,7 +463,48 @@ public static final class Note { public String title; } + public static final class ArrayNode { + public String name; + public ArrayNode[] children; + } + public static final class PaletteGroups extends HashMap {} public static final class PaletteCodes extends HashMap {} + + public static final class AtomicArrayFields { + public AtomicIntegerArray ints = new AtomicIntegerArray(new int[] {7, 8}); + public AtomicLongArray longs = new AtomicLongArray(new long[] {9L, -10L}); + public AtomicReferenceArray refs = new AtomicReferenceArray<>(new String[] {"b", null}); + } + + private static String arrayNodeJson(int depth) { + return "[" + nodeJson(0, depth) + "]"; + } + + private static String nodeJson(int index, int depth) { + String children = index + 1 == depth ? "[]" : "[" + nodeJson(index + 1, depth) + "]"; + return "{\"name\":\"node-" + index + "\",\"children\":" + children + "}"; + } + + private static void assertAtomicInts(AtomicIntegerArray array, int... expected) { + assertEquals(array.length(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(array.get(i), expected[i]); + } + } + + private static void assertAtomicLongs(AtomicLongArray array, long... expected) { + assertEquals(array.length(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(array.get(i), expected[i]); + } + } + + private static void assertAtomicRefs(AtomicReferenceArray array, String... expected) { + assertEquals(array.length(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(array.get(i), expected[i]); + } + } } 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 index cf6a10d7d1..bceb48e096 100644 --- 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 @@ -20,6 +20,8 @@ package org.apache.fory.json; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -28,6 +30,11 @@ import org.apache.fory.json.data.RecursiveParent; import org.apache.fory.json.data.TokenGroup; import org.apache.fory.json.data.TokenValues; +import org.apache.fory.json.meta.JsonAsciiToken; +import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.serializer.StringSerializer; import org.testng.annotations.Test; public class JsonGeneratedCodecTest extends ForyJsonTestModels { @@ -119,4 +126,125 @@ public void readGeneratedCollectionFields() { json.fromJson(input.getBytes(StandardCharsets.UTF_8), GeneratedCollectionFields.class)); assertGeneratedWhenSupported(json, GeneratedCollectionFields.class); } + + @Test + public void readLongAsciiFieldToken() { + String token = "\"favoriteFruit\":"; + long prefix = JsonAsciiToken.prefix(token); + long suffix = JsonAsciiToken.suffixLong(token); + long suffixMask = JsonAsciiToken.suffixMask(token.length()); + Utf8JsonReader utf8 = + new Utf8JsonReader((token + "\"apple\"").getBytes(StandardCharsets.UTF_8)); + assertTrue(utf8.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(utf8.readNullableStringToken(), "apple"); + + String tailToken = "\"registered\":"; + long tailPrefix = JsonAsciiToken.prefix(tailToken); + long tailSuffix = JsonAsciiToken.suffixLong(tailToken); + long tailSuffixMask = JsonAsciiToken.suffixMask(tailToken.length()); + Utf8JsonReader tailUtf8 = + new Utf8JsonReader((tailToken + "1").getBytes(StandardCharsets.UTF_8)); + assertTrue( + tailUtf8.tryReadNextFieldNameToken8( + tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); + assertEquals(tailUtf8.readIntTokenValue(), 1); + if (StringSerializer.isBytesBackedString()) { + Latin1JsonReader latin1 = new Latin1JsonReader(token + "\"pear\""); + assertTrue(latin1.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(latin1.readNullableStringToken(), "pear"); + + Latin1JsonReader tailLatin1 = new Latin1JsonReader(tailToken + "2"); + assertTrue( + tailLatin1.tryReadNextFieldNameToken8( + tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); + assertEquals(tailLatin1.readIntTokenValue(), 2); + } + + Utf8JsonReader mismatch = + new Utf8JsonReader("\"favoriteSeed\":\"pit\"".getBytes(StandardCharsets.UTF_8)); + assertFalse(mismatch.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(mismatch.readFieldNameHash(), JsonFieldNameHash.hash("favoriteSeed")); + mismatch.expectNextToken(':'); + assertEquals(mismatch.readNextNullableString(), "pit"); + } + + @Test + public void readGeneratedLongAsciiFields() { + ForyJson json = ForyJson.builder().build(); + String input = + "{\"registered\":\"today\",\"longitude\":12.5,\"favoriteFruit\":\"apple\"," + + "\"shortName\":\"core\"}"; + assertLongAsciiFields(json.fromJson(input, LongAsciiFields.class)); + assertLongAsciiFields( + json.fromJson(input.getBytes(StandardCharsets.UTF_8), LongAsciiFields.class)); + assertGeneratedWhenSupported(json, LongAsciiFields.class); + } + + @Test + public void readSplitGeneratedFields() { + ForyJson json = ForyJson.builder().build(); + String ordered = + "{\"f0\":0,\"f1\":\"one\",\"f2\":2,\"f3\":\"three\",\"f4\":4,\"f5\":\"five\"," + + "\"f6\":6,\"f7\":\"seven\",\"f8\":8,\"f9\":\"nine\",\"f10\":10," + + "\"f11\":\"eleven\",\"f12\":12,\"f13\":\"thirteen\"}"; + assertWideFields(json.fromJson(ordered, WideFields.class)); + assertWideFields(json.fromJson(ordered.getBytes(StandardCharsets.UTF_8), WideFields.class)); + + String boundaryFallback = + "{\"f0\":0,\"f2\":2,\"f1\":\"one\",\"f3\":\"three\",\"f4\":4,\"f5\":\"five\"," + + "\"f6\":6,\"f7\":\"seven\",\"f8\":8,\"f9\":\"nine\",\"f10\":10," + + "\"f11\":\"eleven\",\"f12\":12,\"f13\":\"thirteen\"}"; + assertWideFields(json.fromJson(boundaryFallback, WideFields.class)); + assertWideFields( + json.fromJson(boundaryFallback.getBytes(StandardCharsets.UTF_8), WideFields.class)); + assertGeneratedWhenSupported(json, WideFields.class); + } + + private static void assertWideFields(WideFields value) { + assertEquals(value.f0, 0); + assertEquals(value.f1, "one"); + assertEquals(value.f2, 2); + assertEquals(value.f3, "three"); + assertEquals(value.f4, 4); + assertEquals(value.f5, "five"); + assertEquals(value.f6, 6); + assertEquals(value.f7, "seven"); + assertEquals(value.f8, 8); + assertEquals(value.f9, "nine"); + assertEquals(value.f10, 10); + assertEquals(value.f11, "eleven"); + assertEquals(value.f12, 12); + assertEquals(value.f13, "thirteen"); + } + + private static void assertLongAsciiFields(LongAsciiFields value) { + assertEquals(value.registered, "today"); + assertEquals(value.longitude, 12.5d); + assertEquals(value.favoriteFruit, "apple"); + assertEquals(value.shortName, "core"); + } + + public static class LongAsciiFields { + public String registered; + public double longitude; + public String favoriteFruit; + public String shortName; + } + + public static class WideFields { + public int f0; + public String f1; + public int f2; + public String f3; + public int f4; + public String f5; + public int f6; + public String f7; + public int f8; + public String f9; + public int f10; + public String f11; + public int f12; + public String f13; + } } 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 index 1cd303cb3d..1fc6ba3bca 100644 --- 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 @@ -20,7 +20,11 @@ package org.apache.fory.json; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertThrows; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -48,6 +52,34 @@ public void writePublicFields() { "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); } + @Test + public void writeJsonToOutputStream() { + ForyJson json = ForyJson.builder().build(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + json.writeJsonTo(new PublicFields(), output); + assertEquals( + new String(output.toByteArray(), StandardCharsets.UTF_8), + "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); + + output.reset(); + json.writeJsonTo(null, output); + assertEquals(new String(output.toByteArray(), StandardCharsets.UTF_8), "null"); + + OutputStream failing = + new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException("closed"); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + throw new IOException("closed"); + } + }; + assertThrows(ForyJsonException.class, () -> json.writeJsonTo(new PublicFields(), failing)); + } + @Test public void writeFirstIntGenerated() { ForyJson json = ForyJson.builder().build(); @@ -126,15 +158,15 @@ public void writeNullFields() { ForyJson json = ForyJson.builder().writeNullFields(true).build(); assertEquals( json.toJson(new PublicFields()), - "{\"active\":true,\"id\":7,\"missing\":null,\"name\":\"fory\"}"); + "{\"active\":true,\"id\":7,\"name\":\"fory\",\"missing\":null}"); } @Test - public void ignoreMethods() { - ForyJson json = ForyJson.builder().build(); + public void fieldOnlyModeIgnoresMethods() { + ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); assertEquals( json.toJson(new MethodsIgnored()), - "{\"hidden\":\"hidden\",\"setterCalls\":0,\"value\":\"field\"}"); + "{\"setterCalls\":0,\"value\":\"field\",\"hidden\":\"hidden\"}"); MethodsIgnored value = json.fromJson("{\"hidden\":\"json\",\"value\":\"json\"}", MethodsIgnored.class); assertEquals(hiddenValue(value), "json"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java new file mode 100644 index 0000000000..904f0bb4e3 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java @@ -0,0 +1,131 @@ +/* + * 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 org.apache.fory.json.data.BeanProperties.BooleanBean; +import org.apache.fory.json.data.BeanProperties.ConflictingTypesBean; +import org.apache.fory.json.data.BeanProperties.DuplicateGetterBean; +import org.apache.fory.json.data.BeanProperties.FinalFieldBean; +import org.apache.fory.json.data.BeanProperties.GetterBean; +import org.apache.fory.json.data.BeanProperties.GetterOnlyBean; +import org.apache.fory.json.data.BeanProperties.InheritedChild; +import org.apache.fory.json.data.BeanProperties.InheritedParent; +import org.apache.fory.json.data.BeanProperties.InvalidAccessorBean; +import org.apache.fory.json.data.BeanProperties.MixedBean; +import org.apache.fory.json.data.BeanProperties.OverloadedSetterBean; +import org.apache.fory.json.data.BeanProperties.SetterBean; +import org.apache.fory.json.data.BeanProperties.SetterOnlyBean; +import org.testng.annotations.Test; + +public class JsonPropertyTest extends ForyJsonTestModels { + @Test + public void writePrivateGetters() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new GetterBean()), "{\"id\":17,\"name\":\"getter-field\"}"); + assertGeneratedWhenSupported(json, GetterBean.class); + } + + @Test + public void readPrivateSetters() { + ForyJson json = ForyJson.builder().build(); + SetterBean value = json.fromJson("{\"id\":4,\"name\":\"alpha\"}", SetterBean.class); + assertEquals(SetterBean.id(value), 5); + assertEquals(SetterBean.name(value), "set-alpha"); + assertEquals(SetterBean.setterCalls(value), 2); + assertGeneratedWhenSupported(json, SetterBean.class); + } + + @Test + public void roundTripMixedProperties() { + ForyJson json = ForyJson.builder().build(); + String text = "{\"count\":3,\"name\":\"mixed\",\"score\":5}"; + assertEquals(json.toJson(new MixedBean()), text); + assertEquals(json.toJson(json.fromJson(text, MixedBean.class)), text); + assertGeneratedWhenSupported(json, MixedBean.class); + } + + @Test + public void writeBooleanIsGetter() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new BooleanBean()), "{\"ready\":true}"); + } + + @Test + public void getterOnlyWrites() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new GetterOnlyBean()), "{\"computed\":6}"); + GetterOnlyBean value = json.fromJson("{\"computed\":99}", GetterOnlyBean.class); + assertEquals(json.toJson(value), "{\"computed\":6}"); + } + + @Test + public void setterOnlyReads() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new SetterOnlyBean()), "{}"); + SetterOnlyBean value = json.fromJson("{\"secret\":\"alpha\"}", SetterOnlyBean.class); + assertEquals(SetterOnlyBean.received(value), "set-alpha"); + } + + @Test + public void fieldOnlyMode() { + ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); + assertEquals(json.toJson(new GetterBean()), "{\"id\":7,\"name\":\"field\"}"); + assertEquals(json.toJson(new GetterOnlyBean()), "{}"); + SetterOnlyBean value = json.fromJson("{\"secret\":\"alpha\"}", SetterOnlyBean.class); + assertEquals(SetterOnlyBean.received(value), null); + } + + @Test + public void rejectPropertyConflicts() { + ForyJson json = ForyJson.builder().build(); + assertThrows(ForyJsonException.class, () -> json.toJson(new DuplicateGetterBean())); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("{\"value\":\"alpha\"}", OverloadedSetterBean.class)); + assertThrows(ForyJsonException.class, () -> json.toJson(new ConflictingTypesBean())); + } + + @Test + public void inheritedProperties() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new InheritedChild()), "{\"id\":4,\"name\":\"child\"}"); + InheritedChild value = json.fromJson("{\"id\":7,\"name\":\"json\"}", InheritedChild.class); + assertEquals(InheritedParent.id(value), 8); + assertEquals(value.name, "json"); + } + + @Test + public void ignoreInvalidAccessors() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new InvalidAccessorBean()), "{\"value\":\"field\"}"); + } + + @Test + public void finalFieldsStayReadOnly() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new FinalFieldBean()), "{\"id\":1,\"name\":\"field\"}"); + FinalFieldBean value = json.fromJson("{\"id\":9,\"name\":\"json\"}", FinalFieldBean.class); + assertEquals(value.id, 1); + assertEquals(value.name, "json"); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java index b65328b5bc..d2b23f264f 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java @@ -50,7 +50,7 @@ public void writeReadRecordClass() throws Exception { .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\"]}"; + "{\"id\":7,\"name\":\"你好,Fory\",\"tags\":[\"a\",\"b\"]," + "\"child\":{\"label\":\"kid\"}}"; assertEquals(json.toJson(value), expected); assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); assertGeneratedWhenSupported(json, type); 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 index 84c1f8afbf..bcae107bb4 100644 --- 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 @@ -23,19 +23,38 @@ import static org.testng.Assert.assertThrows; import java.io.File; +import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.Optional; +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 org.apache.fory.json.codec.JsonCodec; 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.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; import org.testng.annotations.Test; public class JsonScalarTest extends ForyJsonTestModels { @@ -115,6 +134,66 @@ public void readNumericBoundaries() { "{\"intMax\":2147483648,\"text\":\"" + ZH_TEXT + "\"}", NumericBoundaries.class)); } + @Test + public void readUtf8DoubleTokens() { + assertEquals( + new Utf8JsonReader("12.375".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue(), + 12.375d); + assertEquals( + Double.doubleToRawLongBits( + new Utf8JsonReader("-0.0".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue()), + Double.doubleToRawLongBits(-0.0d)); + assertEquals( + new Utf8JsonReader("1.25e2".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue(), + 125.0d); + assertThrows( + ForyJsonException.class, + () -> new Utf8JsonReader("01.5".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue()); + } + + @Test + public void readUtf8LongBlocks() { + assertEquals( + new Utf8JsonReader("123456789012345678".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + 123456789012345678L); + assertEquals( + new Utf8JsonReader("-123456789012345678".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + -123456789012345678L); + assertEquals( + new Utf8JsonReader("9223372036854775807".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + Long.MAX_VALUE); + assertEquals( + new Utf8JsonReader("-9223372036854775808".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + Long.MIN_VALUE); + assertThrows( + ForyJsonException.class, + () -> + new Utf8JsonReader("9223372036854775808".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue()); + } + + @Test + public void writeNumericBoundaries() { + ForyJson json = ForyJson.builder().build(); + NumericBoundaries value = new NumericBoundaries(); + value.intMax = Integer.MAX_VALUE; + value.intMin = Integer.MIN_VALUE; + value.longMax = Long.MAX_VALUE; + value.longMin = Long.MIN_VALUE; + value.small = -7; + value.text = "ok"; + String expected = + "{\"intMax\":2147483647,\"intMin\":-2147483648," + + "\"longMax\":9223372036854775807,\"longMin\":-9223372036854775808," + + "\"small\":-7,\"text\":\"ok\"}"; + assertEquals(json.toJson(value), expected); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + } + @Test public void writeReadCoreScalarFields() { ForyJson json = ForyJson.builder().build(); @@ -150,6 +229,88 @@ public void writeReadCoreScalarFields() { assertEquals(read.uuid, value.uuid); } + @Test + public void writeReadAtomicScalars() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new AtomicBoolean(true)), "true"); + assertEquals(json.fromJson("false", AtomicBoolean.class).get(), false); + assertEquals(json.toJson(new AtomicInteger(12)), "12"); + assertEquals(json.fromJson("13", AtomicInteger.class).get(), 13); + assertEquals(json.toJson(new AtomicLong(14L)), "14"); + assertEquals(json.fromJson("15", AtomicLong.class).get(), 15L); + assertEquals(json.toJson(new AtomicReference<>("value")), "\"value\""); + + AtomicReference value = + json.fromJson("\"typed\"", new TypeRef>() {}); + assertEquals(value.get(), "typed"); + AtomicReference nullValue = + json.fromJson("null", new TypeRef>() {}); + assertEquals(nullValue.get(), null); + } + + @Test + public void writeUtf8ScalarFormats() { + ForyJson json = ForyJson.builder().build(); + UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + assertEquals( + new String(json.toJsonBytes(uuid), StandardCharsets.UTF_8), + "\"123e4567-e89b-12d3-a456-426614174000\""); + assertEquals( + new String(json.toJsonBytes(LocalDate.of(2024, 2, 3)), StandardCharsets.UTF_8), + "\"2024-02-03\""); + + OffsetDateTimeFields fields = new OffsetDateTimeFields(); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 0, 0, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 0, 1_000_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:00.001Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 120_000_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.120Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123_400_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.123400Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 1_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.000001Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.123456789Z\"}"); + + OffsetDateTime offset = + OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.ofHoursMinutes(8, 30)); + fields.value = offset; + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"" + offset + "\"}"); + } + + @Test + public void writeGeneratedUtf8Scalars() { + ForyJson json = ForyJson.builder().build(); + Utf8ScalarFields fields = new Utf8ScalarFields(); + fields.uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + fields.decimal = new BigDecimal("12345.6789"); + fields.date = LocalDate.of(2024, 2, 3); + fields.timestamp = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.UTC); + String expected = + "{\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"," + + "\"decimal\":12345.6789," + + "\"date\":\"2024-02-03\"," + + "\"timestamp\":\"2024-02-03T04:05:06.123456789Z\"}"; + assertEquals(new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), expected); + assertEquals(json.toJson(fields), expected); + assertGeneratedWhenSupported(json, Utf8ScalarFields.class); + } + @Test public void readScalarRoots() { ForyJson json = ForyJson.builder().build(); @@ -159,6 +320,32 @@ public void readScalarRoots() { assertEquals( json.fromJson("\"\uD83D\uDE00\u1234\"".getBytes(StandardCharsets.UTF_8), String.class), "\uD83D\uDE00\u1234"); + assertEquals( + json.fromJson("0.100".getBytes(StandardCharsets.UTF_8), BigDecimal.class), + new BigDecimal("0.100")); + assertEquals( + json.fromJson( + "12345678901234567890.123".getBytes(StandardCharsets.UTF_8), BigDecimal.class), + new BigDecimal("12345678901234567890.123")); + assertEquals( + json.fromJson( + "\"123e4567-e89b-12d3-a456-426614174000\"".getBytes(StandardCharsets.UTF_8), + UUID.class), + UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); + } + + @Test + public void readGeneratedUtf8BigDecimal() { + ForyJson json = ForyJson.builder().build(); + byte[] input = + ("{\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"," + + "\"decimal\":0.12345678901234567," + + "\"date\":\"2024-02-03\"," + + "\"timestamp\":\"2024-02-03T04:05:06Z\"}") + .getBytes(StandardCharsets.UTF_8); + Utf8ScalarFields fields = json.fromJson(input, Utf8ScalarFields.class); + assertEquals(fields.uuid, UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); + assertEquals(fields.decimal, new BigDecimal("0.12345678901234567")); } @Test @@ -225,11 +412,50 @@ public void readLocalDateFromDateTime() { ForyJson json = ForyJson.builder().build(); LocalDate expected = LocalDate.of(2023, 7, 2); assertEquals(json.fromJson("\"2023-07-02T16:00:00.000Z\"", LocalDate.class), expected); + assertEquals( + json.fromJson( + "\"2023-07-02T16:00:00.000Z\"".getBytes(StandardCharsets.UTF_8), LocalDate.class), + expected); LocalDateFields fields = json.fromJson("{\"value\":\"2023-07-02T16:00:00.000Z\"}", LocalDateFields.class); assertEquals(fields.value, expected); } + @Test + public void readOffsetDateTime() { + ForyJson json = ForyJson.builder().build(); + OffsetDateTime utc = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 0, ZoneOffset.UTC); + assertEquals(json.fromJson("\"2024-02-03T04:05:06Z\"", OffsetDateTime.class), utc); + assertEquals( + json.fromJson( + "\"2024-02-03T04:05:06\\u005A\"".getBytes(StandardCharsets.UTF_8), + OffsetDateTime.class), + utc); + + OffsetDateTime nanos = + OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.ofHoursMinutes(8, 30)); + assertEquals( + json.fromJson( + "\"2024-02-03T04:05:06.123456789+08:30\"".getBytes(StandardCharsets.UTF_8), + OffsetDateTime.class), + nanos); + + OffsetDateTime minutePrecision = + OffsetDateTime.of(2024, 2, 3, 4, 5, 0, 0, ZoneOffset.ofHoursMinutes(-5, -30)); + OffsetDateTimeFields fields = + json.fromJson("{\"value\":\"2024-02-03T04:05-05:30\"}", OffsetDateTimeFields.class); + assertEquals(fields.value, minutePrecision); + } + + @Test + public void objectFieldUsesUtf8Codec() { + ForyJson json = + ForyJson.builder().registerCodec(ModeAwareValue.class, new ModeAwareCodec()).build(); + ModeAwareHolder holder = + json.fromJson("{\"value\":{}}".getBytes(StandardCharsets.UTF_8), ModeAwareHolder.class); + assertEquals(holder.value.mode, "utf8"); + } + @Test public void wrapStringScalarParseErrors() { ForyJson json = ForyJson.builder().build(); @@ -263,4 +489,71 @@ public static final class FilePathFields { public static final class LocalDateFields { public LocalDate value; } + + public static final class OffsetDateTimeFields { + public OffsetDateTime value; + } + + public static final class Utf8ScalarFields { + public UUID uuid; + public BigDecimal decimal; + public LocalDate date; + public OffsetDateTime timestamp; + } + + public static final class ModeAwareHolder { + public ModeAwareValue value; + } + + public static final class ModeAwareValue { + public final String mode; + + ModeAwareValue(String mode) { + this.mode = mode; + } + } + + private static final class ModeAwareCodec implements JsonCodec { + @Override + public void write(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + public void writeString(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + public void writeUtf8(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("generic"); + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("latin1"); + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("utf16"); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("utf8"); + } + } } 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 index 823873b160..d8e6143c3a 100644 --- 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 @@ -119,7 +119,7 @@ public void stringWriterShrinksOnReset() throws Exception { assertTrue(writerBufferLength(writer) > 8192); writer.toJson(); writer.reset(); - assertEquals(writerBufferLength(writer), 8192); + assertEquals(writerBufferLength(writer), 65536); writer.writeString("café"); assertEquals(writer.toJson(), "\"café\""); } @@ -227,7 +227,7 @@ public void writeSurrogatePair() { @Test public void readStringScanBoundaries() { ForyJson json = ForyJson.builder().build(); - for (int length : new int[] {7, 8, 15, 16, 23, 24}) { + for (int length : new int[] {7, 8, 15, 16, 23, 24, 31, 32, 63, 64, 65}) { String value = repeat('a', length); String input = "\"" + value + "\""; assertEquals(json.fromJson(input, String.class), value); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java b/java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java new file mode 100644 index 0000000000..b54169e461 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java @@ -0,0 +1,186 @@ +/* + * 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 BeanProperties { + private BeanProperties() {} + + public static final class GetterBean { + private int id = 7; + private String name = "field"; + + public int getId() { + return id + 10; + } + + public String getName() { + return "getter-" + name; + } + } + + public static final class SetterBean { + private int id; + private String name; + private int setterCalls; + + public void setId(int id) { + this.id = id + 1; + setterCalls++; + } + + public void setName(String name) { + this.name = "set-" + name; + setterCalls++; + } + + public static int id(SetterBean value) { + return value.id; + } + + public static String name(SetterBean value) { + return value.name; + } + + public static int setterCalls(SetterBean value) { + return value.setterCalls; + } + } + + public static final class MixedBean { + public int count = 3; + private String name = "mixed"; + private int score = 5; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + } + + public static final class BooleanBean { + private boolean ready = true; + + public boolean isReady() { + return ready; + } + } + + public static final class GetterOnlyBean { + private transient int seed = 3; + + public int getComputed() { + return seed * 2; + } + } + + public static final class SetterOnlyBean { + private transient String received; + + public void setSecret(String secret) { + received = "set-" + secret; + } + + public static String received(SetterOnlyBean value) { + return value.received; + } + } + + public static final class DuplicateGetterBean { + public boolean getActive() { + return true; + } + + public boolean isActive() { + return true; + } + } + + public static final class OverloadedSetterBean { + public void setValue(int value) {} + + public void setValue(String value) {} + } + + public static final class ConflictingTypesBean { + private String value = "text"; + + public String getValue() { + return value; + } + + public void setValue(int value) {} + } + + public static class InheritedParent { + private int id = 4; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id + 1; + } + + public static int id(InheritedParent value) { + return value.id; + } + } + + public static final class InheritedChild extends InheritedParent { + public String name = "child"; + } + + public static final class InvalidAccessorBean { + public String value = "field"; + + public static String getStaticValue() { + return "static"; + } + + public String getWithArg(String ignored) { + return ignored; + } + + public void getVoidValue() {} + + public String setWrongReturn(String value) { + return value; + } + + public void setNoValue() {} + } + + public static final class FinalFieldBean { + public final int id = 1; + public String name = "field"; + } +}