From ad57f00b258a4f9c2f6634f2d04fac0bf242137f Mon Sep 17 00:00:00 2001 From: Laurent Garnier Date: Thu, 7 Sep 2023 17:50:35 +0200 Subject: [PATCH 1/3] more frugal enumset + correctly typed empty enumsets --- .../com/cedarsoftware/util/io/JsonWriter.java | 81 +++++++++++++++++++ .../cedarsoftware/util/io/ObjectResolver.java | 12 ++- .../com/cedarsoftware/util/io/Resolver.java | 42 +++++++++- .../util/io/TestEmptyEnumSetOnJDK17.java | 42 ++++++++-- .../util/io/TestJDK9Immutable.java | 32 +++----- 5 files changed, 180 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/cedarsoftware/util/io/JsonWriter.java b/src/main/java/com/cedarsoftware/util/io/JsonWriter.java index de7e31375..8aff7c787 100644 --- a/src/main/java/com/cedarsoftware/util/io/JsonWriter.java +++ b/src/main/java/com/cedarsoftware/util/io/JsonWriter.java @@ -1089,6 +1089,10 @@ public void writeImpl(Object obj, boolean showType, boolean allowRef, boolean al { writeArray(obj, showType); } + else if (obj instanceof EnumSet) + { + writeEnumSet((EnumSet)obj, showType); + } else if (obj instanceof Collection) { writeCollection((Collection) obj, showType); @@ -2207,6 +2211,83 @@ else if (neverShowType && MetaUtils.isPrimitive(o.getClass())) } } + public void writeEnumSet(final EnumSet enumSet, boolean showType) throws IOException + { + out.write('{'); + tabIn(); + + boolean referenced = objsReferenced.containsKey(enumSet); + if (referenced) + { + writeId(getId(enumSet)); + out.write(','); + newLine(); + } + + if (showType) { + writeType(enumSet, out); + out.write(","); + newLine(); + } + + writeJsonUtf8String("@enum", out); + out.write(':'); + + Field elementTypeField = MetaUtils.getField(EnumSet.class, "elementType"); + Class elementType = (Class) getValueByReflect(enumSet, elementTypeField); + writeJsonUtf8String(elementType.getName(), out); + + var mapOfFileds = MetaUtils.getDeepDeclaredFields(elementType); + //Field[] enumFields = elementType.getDeclaredFields(); + int enumFieldsCount = mapOfFileds.size(); + + if (!enumSet.isEmpty()) { + out.write(","); + newLine(); + + writeJsonUtf8String("@items", out); + out.write(":["); + if (enumFieldsCount > 2) + { + newLine(); + } + + boolean firstInSet = true; + for (var e : enumSet) { + if (!firstInSet) + { + out.write(","); + if (enumFieldsCount > 2) + { + newLine(); + } + } + firstInSet = false; + + if (enumFieldsCount <= 2) + { + writeJsonUtf8String(e.name(), out); + } + else + { + boolean firstInEntry = true; + out.write('{'); + for (var f : mapOfFileds.values()) + { + firstInEntry = writeField(e, firstInEntry, f.getName(), f, false); + } + out.write('}'); + } + } + + out.write("]"); + } + + + tabOut(); + out.write('}'); + } + /** * @param obj Object to be written in JSON format * @param showType boolean true means show the "@type" field, false diff --git a/src/main/java/com/cedarsoftware/util/io/ObjectResolver.java b/src/main/java/com/cedarsoftware/util/io/ObjectResolver.java index 6828955bc..106e2a712 100644 --- a/src/main/java/com/cedarsoftware/util/io/ObjectResolver.java +++ b/src/main/java/com/cedarsoftware/util/io/ObjectResolver.java @@ -414,6 +414,13 @@ protected void traverseCollection(final Deque> stack, } return; } + + Class mayEnumClass = null; + String mayEnumClasName = (String)jsonObj.get("@enum"); + if (mayEnumClasName != null) { + mayEnumClass = MetaUtils.classForName(mayEnumClasName, classLoader); + } + final boolean isImmutable = className != null && className.startsWith("java.util.Immutable"); final Collection col = isImmutable ? new ArrayList() : (Collection) jsonObj.target; final boolean isList = col instanceof List; @@ -436,7 +443,10 @@ else if ((special = readIfMatching(element, null, stack)) != null) } else if (element instanceof String || element instanceof Boolean || element instanceof Double || element instanceof Long) { // Allow Strings, Booleans, Longs, and Doubles to be "inline" without Java object decoration (@id, @type, etc.) - col.add(element); + if (mayEnumClass == null) + col.add(element); + else + col.add(Enum.valueOf(mayEnumClass, (String)element)); } else if (element.getClass().isArray()) { diff --git a/src/main/java/com/cedarsoftware/util/io/Resolver.java b/src/main/java/com/cedarsoftware/util/io/Resolver.java index 9f6d741a9..508c82d89 100644 --- a/src/main/java/com/cedarsoftware/util/io/Resolver.java +++ b/src/main/java/com/cedarsoftware/util/io/Resolver.java @@ -380,7 +380,7 @@ else if (Enum.class.isAssignableFrom(c)) // anonymous subclass of an enum } else if (EnumSet.class.isAssignableFrom(c)) { - mate = getEnumSet(c, jsonObj); + mate = extractEnumSet(c, jsonObj); } else if ((mate = coerceCertainTypes(c.getName())) != null) { // if coerceCertainTypes() returns non-null, it did the work @@ -426,7 +426,7 @@ else if (Enum.class.isAssignableFrom(clazz)) // anonymous subclass of an enum } else if (EnumSet.class.isAssignableFrom(clazz)) // anonymous subclass of an enum { - mate = getEnumSet(clazz, jsonObj); + mate = extractEnumSet(clazz, jsonObj); } else if ((mate = coerceCertainTypes(clazz.getName())) != null) { // if coerceCertainTypes() returns non-null, it did the work @@ -533,7 +533,7 @@ private static enum OneEnum { /* /* java 17 don't allow to call reflect on internal java api like EnumSet's implement, so need to create like this */ - private Object getEmptyEnumSet() { + private EnumSet getEmptyEnumSet() { return EnumSet.noneOf(OneEnum.class); } @@ -568,6 +568,42 @@ private Object getEnumSet(Class c, JsonObject jsonObj) return enumSet; } + protected EnumSet extractEnumSet(Class c, JsonObject jsonObj) + { + String enumClassName = (String) jsonObj.get("@enum"); + Class enumClass = MetaUtils.classForName(enumClassName, reader.getClassLoader()); + Object[] items = jsonObj.getArray(); + if (items == null || items.length == 0) + { + return EnumSet.noneOf(enumClass); + } + + EnumSet enumSet = null; + for (Object item : items) + { + Enum enumItem; + if (item instanceof String) + { + enumItem = Enum.valueOf(enumClass, (String)item); + } + else + { + JsonObject jsItem = (JsonObject) item; + enumItem = Enum.valueOf(c, (String) jsItem.get("name")); + } + + if (enumSet == null) + { // Lazy init the EnumSet + enumSet = EnumSet.of(enumItem); + } + else + { + enumSet.add(enumItem); + } + } + return enumSet; + } + /** * For all fields where the value was "@ref":"n" where 'n' was the id of an object * that had not yet been encountered in the stream, make the final substitution. diff --git a/src/test/java/com/cedarsoftware/util/io/TestEmptyEnumSetOnJDK17.java b/src/test/java/com/cedarsoftware/util/io/TestEmptyEnumSetOnJDK17.java index dc6295f33..484d0d1b1 100644 --- a/src/test/java/com/cedarsoftware/util/io/TestEmptyEnumSetOnJDK17.java +++ b/src/test/java/com/cedarsoftware/util/io/TestEmptyEnumSetOnJDK17.java @@ -1,12 +1,9 @@ package com.cedarsoftware.util.io; -import com.google.gson.Gson; import org.junit.Test; -import java.util.ArrayList; import java.util.EnumSet; -import java.util.List; -import java.util.Map; +import java.util.Objects; /** * @author John DeRegnaucourt (jdereg@gmail.com) @@ -29,7 +26,29 @@ public class TestEmptyEnumSetOnJDK17 { static enum TestEnum { - V1 + V1, V2, V3 + } + + static class MultiVersioned + { + EnumSet versions; + String dummy; + + public MultiVersioned(EnumSet versions, String dummy) { + this.versions = versions; + this.dummy = dummy; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MultiVersioned that = (MultiVersioned) o; + return Objects.equals(versions, that.versions) && Objects.equals(dummy, that.dummy); + } + + public int hashCode() { + return Objects.hash(versions, dummy); + } } @Test @@ -46,11 +65,22 @@ public void testEmptyEnumSetOnJDK17() @Test public void testEnumSetOnJDK17() { - EnumSet source = EnumSet.of(TestEnum.V1); + EnumSet source = EnumSet.of(TestEnum.V1, TestEnum.V3); String json = JsonWriter.objectToJson(source); EnumSet target = (EnumSet) JsonReader.jsonToJava(json); assert source.equals(target); } + @Test + + public void testEnumSetInPoJoOnJDK17() + { + MultiVersioned m = new MultiVersioned(EnumSet.of(TestEnum.V1, TestEnum.V3), "what"); + + String json = JsonWriter.objectToJson(m); + MultiVersioned target = (MultiVersioned) JsonReader.jsonToJava(json); + + assert m.equals(target); + } } diff --git a/src/test/java/com/cedarsoftware/util/io/TestJDK9Immutable.java b/src/test/java/com/cedarsoftware/util/io/TestJDK9Immutable.java index 1f6cbf828..ce366f74f 100644 --- a/src/test/java/com/cedarsoftware/util/io/TestJDK9Immutable.java +++ b/src/test/java/com/cedarsoftware/util/io/TestJDK9Immutable.java @@ -27,7 +27,7 @@ public void testCopyOfListOf() { final Object o = new ArrayList<>(List.of()); String json = JsonWriter.objectToJson(o); - List es = (List) JsonReader.jsonToJava(json); + List es = (List) JsonReader.jsonToJava(json); Assert.assertEquals(0, es.size()); Assert.assertEquals(o.getClass(), es.getClass()); @@ -38,7 +38,7 @@ public void testListOf() { final Object o = List.of(); String json = JsonWriter.objectToJson(o); - List es = (List) JsonReader.jsonToJava(json); + List es = (List) JsonReader.jsonToJava(json); Assert.assertEquals(0, es.size()); Assert.assertEquals(o.getClass(), es.getClass()); @@ -49,7 +49,7 @@ public void testListOfOne() { final Object o = List.of("One"); String json = JsonWriter.objectToJson(o); - List es = (List) JsonReader.jsonToJava(json); + List es = (List) JsonReader.jsonToJava(json); Assert.assertEquals(1, es.size()); Assert.assertEquals(o.getClass(), es.getClass()); @@ -60,7 +60,7 @@ public void testListOfTwo() { final Object o = List.of("One", "Two"); String json = JsonWriter.objectToJson(o); - List es = (List) JsonReader.jsonToJava(json); + List es = (List) JsonReader.jsonToJava(json); Assert.assertEquals(2, es.size()); Assert.assertEquals(o.getClass(), es.getClass()); @@ -71,7 +71,7 @@ public void testListOfThree() { final Object o = List.of("One", "Two", "Three"); String json = JsonWriter.objectToJson(o); - List es = (List) JsonReader.jsonToJava(json); + List es = (List) JsonReader.jsonToJava(json); Assert.assertEquals(3, es.size()); Assert.assertEquals(o.getClass(), es.getClass()); @@ -82,7 +82,7 @@ public void testSetOf() { final Object o = Set.of(); String json = JsonWriter.objectToJson(o); - Set es = (Set) JsonReader.jsonToJava(json); + Set es = (Set) JsonReader.jsonToJava(json); Assert.assertEquals(0, es.size()); Assert.assertEquals(o.getClass(), es.getClass()); @@ -93,7 +93,7 @@ public void testSetOfOne() { final Object o = Set.of("One"); String json = JsonWriter.objectToJson(o); - Set es = (Set) JsonReader.jsonToJava(json); + Set es = (Set) JsonReader.jsonToJava(json); Assert.assertEquals(1, es.size()); Assert.assertEquals(o.getClass(), es.getClass()); @@ -104,7 +104,7 @@ public void testSetOfTwo() { final Object o = Set.of("One", "Two"); String json = JsonWriter.objectToJson(o); - Set es = (Set) JsonReader.jsonToJava(json); + Set es = (Set) JsonReader.jsonToJava(json); Assert.assertEquals(2, es.size()); Assert.assertEquals(o.getClass(), es.getClass()); @@ -115,7 +115,7 @@ public void testSetOfThree() { final Object o = Set.of("One", "Two", "Three"); String json = JsonWriter.objectToJson(o); - Set es = (Set) JsonReader.jsonToJava(json); + Set es = (Set) JsonReader.jsonToJava(json); Assert.assertEquals(3, es.size()); Assert.assertEquals(o.getClass(), es.getClass()); @@ -129,7 +129,6 @@ public void testListOfThreeRecsMutableBoth() { rec2.link = rec1; rec1.mlinks = new ArrayList<>(List.of()); rec2.mlinks = new ArrayList<>(List.of(rec1)); - //List ol = new ArrayList<>(List.of(rec1, rec2, rec1)); List ol = new ArrayList<>(List.of(rec1, rec2, rec1)); String json = JsonWriter.objectToJson(ol); @@ -161,7 +160,6 @@ public void testListOfThreeRecsMutableInside() { rec2.link = rec1; rec1.mlinks = new ArrayList<>(List.of()); rec2.mlinks = new ArrayList<>(List.of(rec1)); - //List ol = new ArrayList<>(List.of(rec1, rec2, rec1)); List ol = List.of(rec1, rec2, rec1); String json = JsonWriter.objectToJson(ol); @@ -198,11 +196,11 @@ public void testListOfThreeRecsBoth() { List ol = List.of(rec1, rec2, rec1); String json = JsonWriter.objectToJson(ol); - List es = (List) JsonReader.jsonToJava(json); + Object es = (List) JsonReader.jsonToJava(json); Assert.assertEquals(((Object) ol).getClass(), es.getClass()); - List recs = es; + List recs = (List) es; Assert.assertEquals(ol.size(), recs.size()); Assert.assertEquals(ol.get(0).s, recs.get(0).s); @@ -229,26 +227,22 @@ public void testListOfThreeRecsBoth() { public void testListOfThreeRecsImmutableOnly() { Rec rec1 = new Rec("OneOrThree", 0); Rec rec2 = new Rec("Two", 2); - //rec1.link = rec2; - //rec2.link = rec1; rec1.ilinks = List.of(rec2, rec1); rec2.ilinks = List.of(); List ol = List.of(rec1, rec2, rec1); String json = JsonWriter.objectToJson(ol); - List es = (List) JsonReader.jsonToJava(json); + Object es = JsonReader.jsonToJava(json); Assert.assertEquals(((Object) ol).getClass(), es.getClass()); - List recs = es; + List recs = (List) es; Assert.assertEquals(ol.size(), recs.size()); Assert.assertEquals(ol.get(0).s, recs.get(0).s); Assert.assertEquals(ol.get(0).i, recs.get(0).i); - //Assert.assertEquals(recs.get(1), recs.get(0).link); Assert.assertEquals(ol.get(1).s, recs.get(1).s); Assert.assertEquals(ol.get(1).i, recs.get(1).i); - //Assert.assertEquals(recs.get(0), recs.get(1).link); Assert.assertEquals(recs.get(0), recs.get(2)); From e528996b1a9364d1e09e77db13472025253d3a28 Mon Sep 17 00:00:00 2001 From: Laurent Garnier Date: Mon, 11 Sep 2023 13:13:41 +0200 Subject: [PATCH 2/3] no var variable type for compatibility with 1.8 --- src/main/java/com/cedarsoftware/util/io/JsonWriter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cedarsoftware/util/io/JsonWriter.java b/src/main/java/com/cedarsoftware/util/io/JsonWriter.java index 8aff7c787..9b40e56ae 100644 --- a/src/main/java/com/cedarsoftware/util/io/JsonWriter.java +++ b/src/main/java/com/cedarsoftware/util/io/JsonWriter.java @@ -2237,7 +2237,7 @@ public void writeEnumSet(final EnumSet enumSet, boolean showType) throws IOEx Class elementType = (Class) getValueByReflect(enumSet, elementTypeField); writeJsonUtf8String(elementType.getName(), out); - var mapOfFileds = MetaUtils.getDeepDeclaredFields(elementType); + Map mapOfFileds = MetaUtils.getDeepDeclaredFields(elementType); //Field[] enumFields = elementType.getDeclaredFields(); int enumFieldsCount = mapOfFileds.size(); @@ -2253,7 +2253,7 @@ public void writeEnumSet(final EnumSet enumSet, boolean showType) throws IOEx } boolean firstInSet = true; - for (var e : enumSet) { + for (Enum e : enumSet) { if (!firstInSet) { out.write(","); @@ -2272,7 +2272,7 @@ public void writeEnumSet(final EnumSet enumSet, boolean showType) throws IOEx { boolean firstInEntry = true; out.write('{'); - for (var f : mapOfFileds.values()) + for (Field f : mapOfFileds.values()) { firstInEntry = writeField(e, firstInEntry, f.getName(), f, false); } From 2fb18e2f05955a0214eaf796c3f52e1487ee8f8c Mon Sep 17 00:00:00 2001 From: Laurent Garnier Date: Tue, 12 Sep 2023 09:57:29 +0200 Subject: [PATCH 3/3] enumset without the unuseful set implementation --- .../com/cedarsoftware/util/io/JsonWriter.java | 48 +++++++++++++------ .../com/cedarsoftware/util/io/MetaUtils.java | 2 + .../com/cedarsoftware/util/io/Resolver.java | 34 ++++++++++--- .../util/io/TestEmptyEnumSetOnJDK17.java | 6 ++- 4 files changed, 67 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/cedarsoftware/util/io/JsonWriter.java b/src/main/java/com/cedarsoftware/util/io/JsonWriter.java index 9b40e56ae..e6e841581 100644 --- a/src/main/java/com/cedarsoftware/util/io/JsonWriter.java +++ b/src/main/java/com/cedarsoftware/util/io/JsonWriter.java @@ -1091,7 +1091,7 @@ public void writeImpl(Object obj, boolean showType, boolean allowRef, boolean al } else if (obj instanceof EnumSet) { - writeEnumSet((EnumSet)obj, showType); + writeEnumSet((EnumSet)obj); } else if (obj instanceof Collection) { @@ -2211,7 +2211,7 @@ else if (neverShowType && MetaUtils.isPrimitive(o.getClass())) } } - public void writeEnumSet(final EnumSet enumSet, boolean showType) throws IOException + public void writeEnumSet(final EnumSet enumSet) throws IOException { out.write('{'); tabIn(); @@ -2224,24 +2224,44 @@ public void writeEnumSet(final EnumSet enumSet, boolean showType) throws IOEx newLine(); } - if (showType) { - writeType(enumSet, out); - out.write(","); - newLine(); - } - writeJsonUtf8String("@enum", out); out.write(':'); + Enum> ee = null; + if (!enumSet.isEmpty()) + { + ee = enumSet.iterator().next(); + } + else + { + EnumSet> complement = EnumSet.complementOf(enumSet); + if (!complement.isEmpty()) + { + ee = complement.iterator().next(); + } + } + Field elementTypeField = MetaUtils.getField(EnumSet.class, "elementType"); - Class elementType = (Class) getValueByReflect(enumSet, elementTypeField); + Class elementType = (Class) getValueByReflect(enumSet, elementTypeField); + if ( elementType != null) + { + // nice we got the right to sneak into + } + else if (ee == null) + { + elementType = MetaUtils.Dumpty.class; + } + else + { + elementType = ee.getClass(); + } writeJsonUtf8String(elementType.getName(), out); - Map mapOfFileds = MetaUtils.getDeepDeclaredFields(elementType); - //Field[] enumFields = elementType.getDeclaredFields(); - int enumFieldsCount = mapOfFileds.size(); - if (!enumSet.isEmpty()) { + Map mapOfFields = MetaUtils.getDeepDeclaredFields(elementType); + //Field[] enumFields = elementType.getDeclaredFields(); + int enumFieldsCount = mapOfFields.size(); + out.write(","); newLine(); @@ -2272,7 +2292,7 @@ public void writeEnumSet(final EnumSet enumSet, boolean showType) throws IOEx { boolean firstInEntry = true; out.write('{'); - for (Field f : mapOfFileds.values()) + for (Field f : mapOfFields.values()) { firstInEntry = writeField(e, firstInEntry, f.getName(), f, false); } diff --git a/src/main/java/com/cedarsoftware/util/io/MetaUtils.java b/src/main/java/com/cedarsoftware/util/io/MetaUtils.java index e18da62d6..cfaf4e7a1 100644 --- a/src/main/java/com/cedarsoftware/util/io/MetaUtils.java +++ b/src/main/java/com/cedarsoftware/util/io/MetaUtils.java @@ -36,6 +36,8 @@ */ public class MetaUtils { + public enum Dumpty {} + private MetaUtils () {} private static final Map> classMetaCache = new ConcurrentHashMap<>(); private static final Set prims = new HashSet<>(); diff --git a/src/main/java/com/cedarsoftware/util/io/Resolver.java b/src/main/java/com/cedarsoftware/util/io/Resolver.java index 508c82d89..4fdd828ba 100644 --- a/src/main/java/com/cedarsoftware/util/io/Resolver.java +++ b/src/main/java/com/cedarsoftware/util/io/Resolver.java @@ -313,13 +313,21 @@ protected Object createJavaObjectInstance(Class clazz, JsonObject jsonObj) String type = jsonObj.type; // We can't set values to an Object, so well try to use the contained type instead - if ("java.lang.Object".equals(type)) + if ("java.lang.Object".equals(type)) { - Object value = jsonObj.get("value"); - if (jsonObj.keySet().size() == 1 && value != null) + Object value = jsonObj.get("value"); + if (jsonObj.keySet().size() == 1 && value != null) { - type = value.getClass().getName(); - } + type = value.getClass().getName(); + } + } + if (type == null) + { + Object mayEnumSpecial = jsonObj.get("@enum"); + if (mayEnumSpecial instanceof String) + { + type = "java.util.EnumSet"; + } } Object mate; @@ -571,12 +579,24 @@ private Object getEnumSet(Class c, JsonObject jsonObj) protected EnumSet extractEnumSet(Class c, JsonObject jsonObj) { String enumClassName = (String) jsonObj.get("@enum"); - Class enumClass = MetaUtils.classForName(enumClassName, reader.getClassLoader()); + Class enumClass = enumClassName == null ? null + : MetaUtils.classForName(enumClassName, reader.getClassLoader()); Object[] items = jsonObj.getArray(); if (items == null || items.length == 0) { - return EnumSet.noneOf(enumClass); + if (enumClass != null) + { + return EnumSet.noneOf(enumClass); + } + else + { + return EnumSet.noneOf(MetaUtils.Dumpty.class); + } } + else if (enumClass == null) + { + throw new JsonIoException("Could not figure out Enum of the not empty set " + jsonObj); + } EnumSet enumSet = null; for (Object item : items) diff --git a/src/test/java/com/cedarsoftware/util/io/TestEmptyEnumSetOnJDK17.java b/src/test/java/com/cedarsoftware/util/io/TestEmptyEnumSetOnJDK17.java index 484d0d1b1..cfa875af0 100644 --- a/src/test/java/com/cedarsoftware/util/io/TestEmptyEnumSetOnJDK17.java +++ b/src/test/java/com/cedarsoftware/util/io/TestEmptyEnumSetOnJDK17.java @@ -33,10 +33,12 @@ static class MultiVersioned { EnumSet versions; String dummy; + EnumSet states; - public MultiVersioned(EnumSet versions, String dummy) { + public MultiVersioned(EnumSet versions, String dummy, EnumSet states) { this.versions = versions; this.dummy = dummy; + this.states = states; } public boolean equals(Object o) { @@ -76,7 +78,7 @@ public void testEnumSetOnJDK17() public void testEnumSetInPoJoOnJDK17() { - MultiVersioned m = new MultiVersioned(EnumSet.of(TestEnum.V1, TestEnum.V3), "what"); + MultiVersioned m = new MultiVersioned(EnumSet.of(TestEnum.V1, TestEnum.V3), "what", EnumSet.of(Thread.State.NEW)); String json = JsonWriter.objectToJson(m); MultiVersioned target = (MultiVersioned) JsonReader.jsonToJava(json);