Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1515 fix date formatting in JSONObject, add tests #1887

Merged
merged 1 commit into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions engine/src/main/java/com/arcadedb/serializer/JsonSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
import com.arcadedb.query.sql.executor.ResultSet;
import com.arcadedb.serializer.json.JSONArray;
import com.arcadedb.serializer.json.JSONObject;
import com.google.gson.JsonNull;

import java.lang.reflect.*;
import java.util.*;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public class JsonSerializer {
private boolean useCollectionSize = false;
Expand All @@ -38,7 +40,9 @@ public class JsonSerializer {

public JSONObject serializeDocument(final Document document) {
final Database database = document.getDatabase();
final JSONObject object = new JSONObject().setDateFormat(database.getSchema().getDateTimeFormat());
final JSONObject object = new JSONObject()
.setDateFormat(database.getSchema().getDateTimeFormat())
.setDateTimeFormat(database.getSchema().getDateTimeFormat());

if (document.getIdentity() != null)
object.put("@rid", document.getIdentity().toString());
Expand All @@ -53,9 +57,9 @@ public JSONObject serializeDocument(final Document document) {
value = JSONObject.NULL;
else if (value instanceof Document)
value = serializeDocument((Document) value);
else if (value instanceof Collection) {
else if (value instanceof Collection)
serializeCollection(database, (Collection<?>) value);
} else if (value instanceof Map)
else if (value instanceof Map)
value = serializeMap(database, (Map<Object, Object>) value);

value = convertNonNumbers(value);
Expand All @@ -69,7 +73,9 @@ else if (value instanceof Collection) {
}

public JSONObject serializeResult(final Database database, final Result result) {
final JSONObject object = new JSONObject().setDateFormat(database.getSchema().getDateTimeFormat());
final JSONObject object = new JSONObject()
.setDateFormat(database.getSchema().getDateFormat())
.setDateTimeFormat(database.getSchema().getDateTimeFormat());

if (result.isElement()) {
final Document document = result.toElement();
Expand All @@ -85,12 +91,12 @@ public JSONObject serializeResult(final Database database, final Result result)

if (value == null)
value = JSONObject.NULL;
else if (value instanceof Document)
value = serializeDocument((Document) value);
else if (value instanceof Result)
value = serializeResult(database, (Result) value);
else if (value instanceof Collection)
value = serializeCollection(database, (Collection<?>) value);
else if (value instanceof Document document)
value = serializeDocument(document);
else if (value instanceof Result res)
value = serializeResult(database, res);
else if (value instanceof Collection<?> coll)
value = serializeCollection(database, coll);
else if (value instanceof Map)
value = serializeMap(database, (Map<Object, Object>) value);
else if (value.getClass().isArray())
Expand Down Expand Up @@ -147,7 +153,9 @@ private Object serializeMap(final Database database, final Map<Object, Object> v
if (useCollectionSize) {
result = value.size();
} else {
final JSONObject map = new JSONObject().setDateFormat(database.getSchema().getDateTimeFormat());
final JSONObject map = new JSONObject()
.setDateFormat(database.getSchema().getDateFormat())
.setDateTimeFormat(database.getSchema().getDateTimeFormat());
for (final Map.Entry<Object, Object> entry : value.entrySet()) {
Object o = entry.getValue();
if (o instanceof Document)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,11 @@ public class JSONFactory {
private JSONFactory() {
gson = new GsonBuilder()//
.serializeNulls()//
//.registerTypeAdapter(Date.class, new DateDeserializer())//
.create();

gsonPrettyPrint = new GsonBuilder()//
.serializeNulls()//
.setPrettyPrinting()//
//.registerTypeAdapter(Date.class, new DateDeserializer())
.create();
}

Expand Down
93 changes: 63 additions & 30 deletions engine/src/main/java/com/arcadedb/serializer/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,27 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.Strictness;
import com.google.gson.stream.JsonReader;

import java.io.*;
import java.math.*;
import java.text.*;
import java.time.*;
import java.time.format.*;
import java.time.temporal.*;
import java.util.*;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
* JSON object.<br>
Expand All @@ -50,6 +62,8 @@
private final JsonObject object;
private String dateFormatAsString = null;
private DateTimeFormatter dateFormat = null;
private String dateTimeFormatAsString;

Check warning on line 65 in engine/src/main/java/com/arcadedb/serializer/json/JSONObject.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

engine/src/main/java/com/arcadedb/serializer/json/JSONObject.java#L65

Avoid unused private fields such as 'dateTimeFormatAsString'.

Check warning on line 65 in engine/src/main/java/com/arcadedb/serializer/json/JSONObject.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

engine/src/main/java/com/arcadedb/serializer/json/JSONObject.java#L65

Perhaps 'dateTimeFormatAsString' could be replaced by a local variable.
private DateTimeFormatter dateTimeFormat;

public JSONObject() {
this.object = new JsonObject();
Expand All @@ -62,7 +76,7 @@
public JSONObject(final String input) {
try {
final JsonReader reader = new JsonReader(new StringReader(input));
reader.setLenient(false);
reader.setStrictness(Strictness.LENIENT);
object = (JsonObject) JsonParser.parseReader(reader);
} catch (Exception e) {
throw new JSONException("Invalid JSON object format", e);
Expand Down Expand Up @@ -105,14 +119,14 @@

if (value == null || value instanceof JsonNull)
object.add(name, NULL);
else if (value instanceof String)
object.addProperty(name, (String) value);
else if (value instanceof Number)
object.addProperty(name, (Number) value);
else if (value instanceof Boolean)
object.addProperty(name, (Boolean) value);
else if (value instanceof Character)
object.addProperty(name, (Character) value);
else if (value instanceof String string)
object.addProperty(name, string);
else if (value instanceof Number number)
object.addProperty(name, number);
else if (value instanceof Boolean bool)
object.addProperty(name, bool);
else if (value instanceof Character character)
object.addProperty(name, character);
else if (value instanceof JSONObject)
object.add(name, ((JSONObject) value).getInternal());
else if (value instanceof String[])
Expand All @@ -130,33 +144,42 @@
// RETRY
}
}
} else if (value instanceof Enum) {
object.addProperty(name, ((Enum<?>) value).name());
} else if (value instanceof Date) {
} else if (value instanceof Enum<?> enumValue) {
object.addProperty(name, enumValue.name());
} else if (value instanceof Date date) {
if (dateFormatAsString == null)
// SAVE AS TIMESTAMP
object.addProperty(name, ((Date) value).getTime());
object.addProperty(name, date.getTime());
else
// SAVE AS STRING
object.addProperty(name, new SimpleDateFormat(dateFormatAsString).format((Date) value));
} else if (value instanceof LocalDateTime || value instanceof ZonedDateTime || value instanceof Instant) {
if (dateFormat == null)
object.addProperty(name, dateFormat.format(date.toInstant().atZone(ZoneId.systemDefault())));
} else if (value instanceof LocalDate localDate) {
if (dateFormatAsString == null)
// SAVE AS TIMESTAMP
object.addProperty(name,
(localDate.atStartOfDay().toInstant(ZoneId.systemDefault().getRules().getOffset(Instant.now()))
.toEpochMilli()));
else
// SAVE AS STRING
object.addProperty(name, dateFormat.format(localDate.atStartOfDay()));
} else if (value instanceof TemporalAccessor temporalAccessor) {
if (dateFormatAsString == null)
// SAVE AS TIMESTAMP
object.addProperty(name,
DateUtils.dateTimeToTimestamp(value, ChronoUnit.NANOS)); // ALWAYS USE NANOS TO AVOID PRECISION LOSS
else
// SAVE AS STRING
object.addProperty(name, dateFormat.format((TemporalAccessor) value));
} else if (value instanceof Duration) {
object.addProperty(name, dateTimeFormat.format(temporalAccessor));
} else if (value instanceof Duration duration) {
object.addProperty(name,
Double.valueOf(String.format("%d.%d", ((Duration) value).toSeconds(), ((Duration) value).toNanosPart())));
} else if (value instanceof Identifiable) {
object.addProperty(name, ((Identifiable) value).getIdentity().toString());
Double.valueOf(String.format("%d.%d", duration.toSeconds(), duration.toNanosPart())));
} else if (value instanceof Identifiable identifiable) {
object.addProperty(name, identifiable.getIdentity().toString());
} else if (value instanceof Map) {
final JSONObject embedded = new JSONObject((Map<String, Object>) value);
object.add(name, embedded.getInternal());
} else if (value instanceof Class) {
object.addProperty(name, ((Class<?>) value).getName());
} else if (value instanceof Class<?> clazz) {
object.addProperty(name, clazz.getName());
} else
// GENERIC CASE: TRANSFORM IT TO STRING
object.addProperty(name, value.toString());
Expand Down Expand Up @@ -299,6 +322,16 @@
return this;
}

public JSONObject setDateTimeFormat(final String dateFormat) {
this.dateTimeFormatAsString = dateFormat;
try {
this.dateTimeFormat = DateTimeFormatter.ofPattern(dateFormat);
} catch (IllegalArgumentException e) {
throw new JSONException("Invalid date format: " + dateFormat, e);
}
return this;
}

@Override
public boolean equals(final Object o) {
if (this == o)
Expand Down
74 changes: 63 additions & 11 deletions engine/src/test/java/com/arcadedb/serializer/json/JSONTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
import com.arcadedb.TestHelper;
import org.junit.jupiter.api.Test;

import java.util.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -33,25 +38,26 @@
public class JSONTest extends TestHelper {
@Test
public void testDates() {
final Date date = new Date();
JSONObject json = new JSONObject().put("date", date);
JSONObject json = new JSONObject()
.put("date", new Date())
.put("dateTime", LocalDateTime.now())
.put("localDate", LocalDate.now());
final String serialized = json.toString();
JSONObject deserialized = new JSONObject(serialized);
assertThat(deserialized).isEqualTo(json);
}

@Test
public void testLists() {
JSONObject json = new JSONObject().put("list", Collections.unmodifiableList(List.of(1, 2, 3)));
JSONObject json = new JSONObject().put("list", List.of(1, 2, 3));
final String serialized = json.toString();
JSONObject deserialized = new JSONObject(serialized);
assertThat(deserialized).isEqualTo(json);
}

@Test
public void testListsOfLists() {
final List<List<Integer>> list = List.of(Collections.unmodifiableList(List.of(1, 2, 3)),
Collections.unmodifiableList(List.of(7, 8, 9)));
final List<List<Integer>> list = List.of(List.of(1, 2, 3), List.of(7, 8, 9));
JSONObject json = new JSONObject().put("list", list);
final String serialized = json.toString();
JSONObject deserialized = new JSONObject(serialized);
Expand All @@ -60,8 +66,12 @@ public void testListsOfLists() {

@Test
public void testDatesWithFormat() {
final Date date = new Date();
JSONObject json = new JSONObject().setDateFormat(database.getSchema().getDateTimeFormat()).put("date", date);
JSONObject json = new JSONObject()
.setDateFormat(database.getSchema().getDateFormat())
.setDateTimeFormat(database.getSchema().getDateTimeFormat())
.put("date", new Date())
.put("dateTime", LocalDateTime.now())
.put("localDate", LocalDate.now());

final String serialized = json.toString();
JSONObject deserialized = new JSONObject(serialized);
Expand Down Expand Up @@ -96,14 +106,56 @@ public void testMalformedTrailingCommas() {

@Test
public void testNaN() {
final JSONObject json = new JSONObject().put("a", 10);
json.put("nan", Double.NaN);
json.put("arrayNan", new JSONArray().put(0).put(Double.NaN).put(5));
final JSONObject json = new JSONObject()
.put("a", 10)
.put("nan", Double.NaN)
.put("arrayNan", new JSONArray().put(0).put(Double.NaN).put(5));

json.validate();

assertThat(json.getInt("nan")).isEqualTo(0);
assertThat(json.getJSONArray("arrayNan").get(0)).isEqualTo(0);
assertThat(json.getJSONArray("arrayNan").get(1)).isEqualTo(0);
assertThat(json.getJSONArray("arrayNan").get(2)).isEqualTo(5);
}

@Test
void testMixedTypes() {
JSONObject json = new JSONObject()
.put("int", 10)
.put("float", 10.5f)
.put("double", 10.5d)
.put("long", 10L)
.put("string", "hello")
.put("boolean", true)
.put("null", (String) null)
.put("array", List.of(1, 2, 3))
.put("stringArray", new String[] { "one", "two", "three" })
.put("map", Map.of("a", 1, "b", 2, "c", 3));
json.validate();

assertThat(json.getInt("int")).isEqualTo(10);
assertThat(json.getFloat("float")).isEqualTo(10.5f);
assertThat(json.getDouble("double")).isEqualTo(10.5d);
assertThat(json.getLong("long")).isEqualTo(10L);
assertThat(json.getString("string")).isEqualTo("hello");
assertThat(json.getBoolean("boolean")).isTrue();
assertThat(json.isNull("null")).isTrue();
assertThat(json.getJSONArray("array").length()).isEqualTo(3);
assertThat(json.getJSONArray("stringArray").length()).isEqualTo(3);
assertThat(json.getJSONObject("map").length()).isEqualTo(3);

Map<String, Object> map = json.toMap();
assertThat(map.get("int")).isEqualTo(10);
assertThat(map.get("float")).isEqualTo(10.5);
assertThat(map.get("double")).isEqualTo(10.5);
assertThat(map.get("long")).isEqualTo(10);
assertThat(map.get("string")).isEqualTo("hello");
assertThat(map.get("boolean")).isEqualTo(true);
assertThat(map.get("null")).isNull();
assertThat(map.get("array")).isEqualTo(List.of(1, 2, 3));
assertThat(map.get("stringArray")).isEqualTo(List.of( "one", "two", "three" ));
assertThat(map.get("map")).isEqualTo(Map.of("a", 1, "b", 2, "c", 3));

}
}
Loading
Loading