Skip to content

Commit

Permalink
Enhancements for Json support.
Browse files Browse the repository at this point in the history
   * on linux unicode problems without + 10 (JsonReader#readHexChar)
   * call no-arg constructor if annotated with @before (needed for e.g. field init)
   * if long value outside javascript safe integer range write it as String
   * support reading long field as String
   * support reading boolean filed as String
   * support reading String field as boolean or number
  • Loading branch information
avrecko committed Feb 19, 2023
1 parent 4bd740b commit bf06b4a
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 29 deletions.
12 changes: 12 additions & 0 deletions src/one/nio/serial/Before.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package one.nio.serial;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface Before {

}
13 changes: 13 additions & 0 deletions src/one/nio/serial/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@

public class Json {

public static final long JS_NUMBER_MAX_SAFE_INTEGER = 9007199254740991L;
public static final long JS_NUMBER_MIN_SAFE_INTEGER = -9007199254740991L;

public static void appendChar(StringBuilder builder, char c) {
builder.append('"');
if (c == '"' || c == '\\') {
Expand All @@ -44,6 +47,16 @@ public static void appendChars(StringBuilder builder, char[] obj) {
builder.append(obj, from, obj.length - from).append('"');
}

public static void appendLong(StringBuilder builder, long obj) {
if (obj < JS_NUMBER_MIN_SAFE_INTEGER | obj > JS_NUMBER_MAX_SAFE_INTEGER) {
builder.append('"');
builder.append(obj);
builder.append('"');
} else {
builder.append(obj);
}
}

public static void appendString(StringBuilder builder, String s) {
int length = s.length();
for (int i = 0; i < length; i++) {
Expand Down
52 changes: 34 additions & 18 deletions src/one/nio/serial/JsonReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public final void expect(int b, String message) throws IOException {
}

public final boolean readBoolean() throws IOException {
if (next == '"') {
return Boolean.parseBoolean(readString());
}
int b = read();
if (b == 't' && read() == 'r' && read() == 'u' && read() == 'e') {
return true;
Expand All @@ -100,6 +103,9 @@ public final int readInt() throws IOException {
}

public final long readLong() throws IOException {
if (next == '"') {
return Long.parseLong(readString());
}
return Long.parseLong(readNumber());
}

Expand Down Expand Up @@ -158,9 +164,9 @@ public final int readHexChar() throws IOException {
if (b >= '0' && b <= '9') {
return b - '0';
} else if (b >= 'A' && b <= 'F') {
return b - 'A';
return b - 'A' + 10;
} else if (b >= 'a' && b <= 'f') {
return b - 'a';
return b - 'a' + 10;
}
throw exception("Invalid escape character");
}
Expand Down Expand Up @@ -193,23 +199,33 @@ public Object readNull() throws IOException {
}

public String readString() throws IOException {
StringBuilder sb = new StringBuilder();
expect('\"', "Expected string");
while (next >= 0 && next != '\"') {
int b = read();
if ((b & 0x80) == 0) {
sb.append(b == '\\' ? readEscapeChar() : (char) b);
} else if ((b & 0xe0) == 0xc0) {
sb.append((char) ((b & 0x1f) << 6 | (read() & 0x3f)));
} else if ((b & 0xf0) == 0xe0) {
sb.append((char) ((b & 0x0f) << 12 | (read() & 0x3f) << 6 | (read() & 0x3f)));
} else {
int v = (b & 0x07) << 18 | (read() & 0x3f) << 12 | (read() & 0x3f) << 6 | (read() & 0x3f);
sb.append((char) (0xd800 | (v - 0x10000) >>> 10)).append((char) (0xdc00 | (v & 0x3ff)));
}
switch (next) {
case '\"':
StringBuilder sb = new StringBuilder();
read();
while (next >= 0 && next != '\"') {
int b = read();
if ((b & 0x80) == 0) {
sb.append(b == '\\' ? readEscapeChar() : (char) b);
} else if ((b & 0xe0) == 0xc0) {
sb.append((char) ((b & 0x1f) << 6 | (read() & 0x3f)));
} else if ((b & 0xf0) == 0xe0) {
sb.append((char) ((b & 0x0f) << 12 | (read() & 0x3f) << 6 | (read() & 0x3f)));
} else {
int v = (b & 0x07) << 18 | (read() & 0x3f) << 12 | (read() & 0x3f) << 6 | (read() & 0x3f);
sb.append((char) (0xd800 | (v - 0x10000) >>> 10)).append((char) (0xdc00 | (v & 0x3ff)));
}
}
expect('\"', "Unexpected end of string");
return sb.toString();
case 'f':
case 'F':
case 't':
case 'T':
return "" + readBoolean();
default:
return readNumber();
}
expect('\"', "Unexpected end of string");
return sb.toString();
}

public byte[] readBinary() throws IOException {
Expand Down
2 changes: 1 addition & 1 deletion src/one/nio/serial/LongSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void skip(DataStream in) throws IOException {

@Override
public void toJson(Long obj, StringBuilder builder) {
builder.append(obj.longValue());
Json.appendLong(builder, obj);
}

@Override
Expand Down
23 changes: 13 additions & 10 deletions src/one/nio/serial/gen/DelegateGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
package one.nio.serial.gen;

import one.nio.gen.BytecodeGenerator;
import one.nio.serial.Default;
import one.nio.serial.FieldDescriptor;
import one.nio.serial.JsonName;
import one.nio.serial.NotSerial;
import one.nio.serial.Repository;
import one.nio.serial.SerializeWith;
import one.nio.serial.*;
import one.nio.util.Hex;
import one.nio.util.JavaFeatures;
import one.nio.util.JavaInternals;
Expand All @@ -38,10 +33,7 @@
import java.io.ObjectOutputStream;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.*;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -401,6 +393,10 @@ private static void generateToJson(ClassVisitor cv, Class cls, FieldDescriptor[]
mv.visitMethodInsn(INVOKESTATIC, "one/nio/serial/Json", "appendChar", "(Ljava/lang/StringBuilder;C)V", false);
mv.visitVarInsn(ALOAD, 2);
break;
case Long:
mv.visitMethodInsn(INVOKESTATIC, "one/nio/serial/Json", "appendLong", "(Ljava/lang/StringBuilder;J)V", false);
mv.visitVarInsn(ALOAD, 2);
break;
default:
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", srcType.appendSignature(), false);
}
Expand Down Expand Up @@ -433,6 +429,13 @@ private static void generateFromJson(ClassVisitor cv, Class cls, FieldDescriptor
// Create instance
mv.visitTypeInsn(NEW, Type.getInternalName(cls));

Constructor<?> constructor = JavaInternals.findConstructor(cls);

if (constructor != null && constructor.getAnnotation(Before.class) != null) {
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,Type.getInternalName(cls) , "<init>", "()V", false);
}

// Prepare a multimap (fieldHash -> fds) for lookupswitch
TreeMap<Integer, FieldDescriptor> fieldHashes = new TreeMap<>();
boolean isRecord = JavaFeatures.isRecord(cls);
Expand Down
46 changes: 46 additions & 0 deletions test/one/nio/serial/JsonReaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,56 @@ public void nullableFields() throws IOException, ClassNotFoundException {
Assert.assertTrue(x.set instanceof Set && x.set.isEmpty());
}

@Test
public void unicodeEscaping() throws IOException, ClassNotFoundException {
String s = "{\"string2\":\"A\u003D\u003dZ\"}";
JsonReader reader = new JsonReader(s.getBytes());
Custom x = reader.readObject(Custom.class);
Assert.assertEquals("A==Z", x.string2);
}

@Test
public void shouldReadBooleansAndNumbersForStringField() throws IOException, ClassNotFoundException {
String s = "{\"string2\": null}";
JsonReader reader = new JsonReader(s.getBytes());
Custom x = reader.readObject(Custom.class);
Assert.assertNull(x.string2);

s = "{\"string2\": false}";
reader = new JsonReader(s.getBytes());
x = reader.readObject(Custom.class);
Assert.assertEquals("false", x.string2);

s = "{\"string2\": true}";
reader = new JsonReader(s.getBytes());
x = reader.readObject(Custom.class);
Assert.assertEquals("true", x.string2);

s = "{\"string2\": 123}";
reader = new JsonReader(s.getBytes());
x = reader.readObject(Custom.class);
Assert.assertEquals("123", x.string2);
}

@Test
public void shouldReadStringForBooleanAndNumberField() throws IOException, ClassNotFoundException {
String s = "{\"aBoolean\": \"true\"}";
JsonReader reader = new JsonReader(s.getBytes());
Custom x = reader.readObject(Custom.class);
Assert.assertTrue(x.aBoolean);

s = "{\"longValue\": \"123\"}";
reader = new JsonReader(s.getBytes());
x = reader.readObject(Custom.class);
Assert.assertEquals(123L, (long)x.longValue);
}

static class Custom implements Serializable {
int intValue;
Long longValue = -77L;
final String string = "zzz";
String string2;
boolean aBoolean;
Object[] FB = {"A", true};
Set<String> set = Collections.emptySet();
final Map<String, Integer> Ea = new HashMap<String, Integer>() {{
Expand Down
40 changes: 40 additions & 0 deletions test/one/nio/serial/JsonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package one.nio.serial;

import org.junit.Assert;
import org.junit.Test;

import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
Expand All @@ -37,8 +40,45 @@ public static void main(String[] args) throws IOException {
System.out.println(Json.toJson(object));
}

@Test
public void testBefore() throws IOException, ClassNotFoundException {
BeforeTestWith beforeTest = Json.fromJson("{}", BeforeTestWith.class);
Assert.assertEquals("foo", beforeTest.name);

BeforeTestWithoutAnnotation beforeTestWithout = Json.fromJson("{}", BeforeTestWithoutAnnotation.class);
Assert.assertNull(beforeTestWithout.name);
}

@Test
public void testSerializeLongAsString() throws IOException, ClassNotFoundException {
TestObject obj = new TestObject();
obj.longField = Long.MAX_VALUE;
String test = Json.toJson(obj);
Assert.assertTrue(test.contains("\"9223372036854775807\""));
Assert.assertEquals(Long.MAX_VALUE, Json.fromJson(test, TestObject.class).longField);
}

public static class TestObject implements Serializable {
@JsonName("test_name")
public String name;

public long longField;
}

public static class BeforeTestWith implements Serializable {
public String name;

@Before
public BeforeTestWith() {
name = "foo";
}
}

public static class BeforeTestWithoutAnnotation implements Serializable {
public String name;

public BeforeTestWithoutAnnotation() {
name = "foo";
}
}
}

0 comments on commit bf06b4a

Please sign in to comment.