From 0a91b2abe6a856a41608b31dae96100a76e0bec6 Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Thu, 2 Jan 2025 21:31:18 +0100 Subject: [PATCH] #1515 fix date formatting in JSONObject, add tests --- .../arcadedb/serializer/JsonSerializer.java | 34 ++++--- .../arcadedb/serializer/json/JSONFactory.java | 2 - .../arcadedb/serializer/json/JSONObject.java | 93 +++++++++++++------ .../arcadedb/serializer/json/JSONTest.java | 74 ++++++++++++--- .../java/com/arcadedb/server/ha/HAServer.java | 34 +++++-- .../handler/AbstractServerHttpHandler.java | 4 +- .../java/com/arcadedb/remote/Issue1515IT.java | 48 ++++++++++ 7 files changed, 223 insertions(+), 66 deletions(-) create mode 100644 server/src/test/java/com/arcadedb/remote/Issue1515IT.java diff --git a/engine/src/main/java/com/arcadedb/serializer/JsonSerializer.java b/engine/src/main/java/com/arcadedb/serializer/JsonSerializer.java index be67ed0c5..e730dfa8e 100644 --- a/engine/src/main/java/com/arcadedb/serializer/JsonSerializer.java +++ b/engine/src/main/java/com/arcadedb/serializer/JsonSerializer.java @@ -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; @@ -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()); @@ -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) value); value = convertNonNumbers(value); @@ -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(); @@ -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) value); else if (value.getClass().isArray()) @@ -147,7 +153,9 @@ private Object serializeMap(final Database database, final Map 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 entry : value.entrySet()) { Object o = entry.getValue(); if (o instanceof Document) diff --git a/engine/src/main/java/com/arcadedb/serializer/json/JSONFactory.java b/engine/src/main/java/com/arcadedb/serializer/json/JSONFactory.java index 076e2d997..92d2e924c 100644 --- a/engine/src/main/java/com/arcadedb/serializer/json/JSONFactory.java +++ b/engine/src/main/java/com/arcadedb/serializer/json/JSONFactory.java @@ -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(); } diff --git a/engine/src/main/java/com/arcadedb/serializer/json/JSONObject.java b/engine/src/main/java/com/arcadedb/serializer/json/JSONObject.java index 85746a476..3af97f021 100644 --- a/engine/src/main/java/com/arcadedb/serializer/json/JSONObject.java +++ b/engine/src/main/java/com/arcadedb/serializer/json/JSONObject.java @@ -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.
@@ -50,6 +62,8 @@ public class JSONObject { private final JsonObject object; private String dateFormatAsString = null; private DateTimeFormatter dateFormat = null; + private String dateTimeFormatAsString; + private DateTimeFormatter dateTimeFormat; public JSONObject() { this.object = new JsonObject(); @@ -62,7 +76,7 @@ public JSONObject(final JsonObject input) { 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); @@ -105,14 +119,14 @@ public JSONObject put(final String name, final Object value) { 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[]) @@ -130,33 +144,42 @@ else if (value instanceof Iterable) { // 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) 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()); @@ -299,6 +322,16 @@ public JSONObject setDateFormat(final String dateFormat) { 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) diff --git a/engine/src/test/java/com/arcadedb/serializer/json/JSONTest.java b/engine/src/test/java/com/arcadedb/serializer/json/JSONTest.java index bfba13b38..ca255a36c 100644 --- a/engine/src/test/java/com/arcadedb/serializer/json/JSONTest.java +++ b/engine/src/test/java/com/arcadedb/serializer/json/JSONTest.java @@ -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; @@ -33,8 +38,10 @@ 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); @@ -42,7 +49,7 @@ public void testDates() { @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); @@ -50,8 +57,7 @@ public void testLists() { @Test public void testListsOfLists() { - final List> list = List.of(Collections.unmodifiableList(List.of(1, 2, 3)), - Collections.unmodifiableList(List.of(7, 8, 9))); + final List> 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); @@ -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); @@ -96,9 +106,11 @@ 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); @@ -106,4 +118,44 @@ public void testNaN() { 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 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)); + + } } diff --git a/server/src/main/java/com/arcadedb/server/ha/HAServer.java b/server/src/main/java/com/arcadedb/server/ha/HAServer.java index 74d37c350..c298cdc82 100644 --- a/server/src/main/java/com/arcadedb/server/ha/HAServer.java +++ b/server/src/main/java/com/arcadedb/server/ha/HAServer.java @@ -26,9 +26,9 @@ import com.arcadedb.exception.TimeoutException; import com.arcadedb.exception.TransactionException; import com.arcadedb.log.LogManager; +import com.arcadedb.network.HostUtil; import com.arcadedb.network.binary.ChannelBinaryClient; import com.arcadedb.network.binary.ConnectionException; -import com.arcadedb.network.HostUtil; import com.arcadedb.network.binary.QuorumNotReachedException; import com.arcadedb.network.binary.ServerIsNotTheLeaderException; import com.arcadedb.query.sql.executor.InternalResultSet; @@ -51,12 +51,26 @@ import com.arcadedb.utility.RecordTableFormatter; import com.arcadedb.utility.TableFormatter; -import java.io.*; -import java.net.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import java.util.logging.*; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; public class HAServer implements ServerPlugin { public static final String DEFAULT_PORT = HostUtil.HA_DEFAULT_PORT; @@ -822,9 +836,11 @@ public void printClusterConfiguration() { public JSONObject getStats() { final String dateTimeFormat = GlobalConfiguration.DATE_TIME_FORMAT.getValueAsString(); - final JSONObject result = new JSONObject().setDateFormat(dateTimeFormat); + final JSONObject result = new JSONObject().setDateTimeFormat(dateTimeFormat) + .setDateFormat(GlobalConfiguration.DATE_FORMAT.getValueAsString()); - final JSONObject current = new JSONObject().setDateFormat(dateTimeFormat); + final JSONObject current = new JSONObject().setDateTimeFormat(dateTimeFormat) + .setDateFormat(GlobalConfiguration.DATE_FORMAT.getValueAsString()); current.put("name", getServerName()); current.put("address", getServerAddress()); current.put("role", isLeader() ? "Leader" : "Replica"); diff --git a/server/src/main/java/com/arcadedb/server/http/handler/AbstractServerHttpHandler.java b/server/src/main/java/com/arcadedb/server/http/handler/AbstractServerHttpHandler.java index 9b8c11966..64c97a2e9 100644 --- a/server/src/main/java/com/arcadedb/server/http/handler/AbstractServerHttpHandler.java +++ b/server/src/main/java/com/arcadedb/server/http/handler/AbstractServerHttpHandler.java @@ -207,7 +207,9 @@ protected static void checkRootUser(ServerSecurityUser user) { protected JSONObject createResult(final SecurityUser user, final Database database) { final JSONObject json = new JSONObject(); if (database != null) - json.setDateFormat(database.getSchema().getDateTimeFormat()); + json.setDateFormat(database.getSchema().getDateFormat()) + .setDateTimeFormat(database.getSchema().getDateTimeFormat()); + json.put("user", user.getName()).put("version", Constants.getVersion()) .put("serverName", httpServer.getServer().getServerName()); return json; diff --git a/server/src/test/java/com/arcadedb/remote/Issue1515IT.java b/server/src/test/java/com/arcadedb/remote/Issue1515IT.java new file mode 100644 index 000000000..6caa1a18a --- /dev/null +++ b/server/src/test/java/com/arcadedb/remote/Issue1515IT.java @@ -0,0 +1,48 @@ +package com.arcadedb.remote; + +import com.arcadedb.query.sql.executor.Result; +import com.arcadedb.query.sql.executor.ResultSet; +import com.arcadedb.server.BaseGraphServerTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Issue1515IT extends BaseGraphServerTest { + + protected String getDatabaseName() { + return "issue1515"; + } + + @Test + void rename_me() { + final RemoteDatabase database = new RemoteDatabase("127.0.0.1", 2480, getDatabaseName(), "root", + BaseGraphServerTest.DEFAULT_PASSWORD_FOR_TESTS); + + String script = """ + alter database `arcadedb.dateImplementation` `java.time.LocalDate`; + alter database `arcadedb.dateTimeImplementation` `java.time.LocalDateTime`; + alter database `arcadedb.dateFormat` 'dd MM yyyy GG'; + alter database `arcadedb.dateTimeFormat` 'dd MM yyyy GG HH:mm:ss'; + create property Person.name if not exists String (mandatory true, notnull true); + create index if not exists on Person (name) unique; + create property Person.dateOfBirth if not exists Date; + create property Person.dateOfDeath if not exists Date; + """; + + database.transaction(() -> + database.command("sqlscript", script)); + + database.transaction(() -> + database.command("sql", """ + insert into Person set name = 'Hannibal', + dateOfBirth = date('01 01 0001 BC', 'dd MM yyyy GG'), + dateOfDeath = date('01 01 0001 AD', 'dd MM yyyy GG') + """)); + + ResultSet result = database.query("sql", "select from Person where name = 'Hannibal'"); + Result doc = result.next(); + assertThat(doc.getProperty("dateOfBirth")).isEqualTo("01 01 0001 BC"); + assertThat(doc.getProperty("dateOfDeath")).isEqualTo("01 01 0001 AD"); + + } +}