diff --git a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java index 6271d4051..96a37bb4a 100644 --- a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java +++ b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java @@ -18,7 +18,7 @@ import java.util.Set; * java.util.Collection. The reason being that java.util.Map is not a * Collection. I realise this makes this name confusing so I apologise for that. */ -public interface BeanCollection extends Serializable { +public interface BeanCollection extends Serializable, ToStringAware { enum ModifyListenMode { /** diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java index ee54991ad..e12621829 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java @@ -11,7 +11,7 @@ import java.io.Serializable; * general application consumption. *

*/ -public interface EntityBean extends Serializable { +public interface EntityBean extends Serializable, ToStringAware { /** * Return all the property names in defined order. @@ -109,4 +109,8 @@ public interface EntityBean extends Serializable { throw new NotEnhancedException(); } + @Override + default void toString(ToStringBuilder builder) { + throw new NotEnhancedException(); + } } diff --git a/ebean-api/src/main/java/io/ebean/bean/ToStringAware.java b/ebean-api/src/main/java/io/ebean/bean/ToStringAware.java new file mode 100644 index 000000000..bb86d9629 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/ToStringAware.java @@ -0,0 +1,12 @@ +package io.ebean.bean; + +/** + * A type that can participate in building toString content with ToStringBuilder. + */ +public interface ToStringAware { + + /** + * Append to the ToStringBuilder. + */ + void toString(ToStringBuilder builder); +} diff --git a/ebean-api/src/main/java/io/ebean/bean/ToStringBuilder.java b/ebean-api/src/main/java/io/ebean/bean/ToStringBuilder.java new file mode 100644 index 000000000..1dd87495a --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/ToStringBuilder.java @@ -0,0 +1,163 @@ +package io.ebean.bean; + +import java.util.Collection; +import java.util.IdentityHashMap; + +/** + * Helps build toString content taking into account recursion. + *

+ * That is, it detects and handles the case where there are relationships that recurse + * and would otherwise become an infinite loop (e.g. bidirectional parent child). + */ +public final class ToStringBuilder { + + /** + * The max number of objects that we allow before stopping content being appended. + */ + private static final int MAX = 100; + + /** + * Max length of content in string form added for any given value. + */ + private static final int TRIM_LENGTH = 500; + + /** + * The max total content after which we stop content being appended. + */ + private static final int MAX_TOTAL_CONTENT = 2000; + + private final IdentityHashMap id = new IdentityHashMap<>(); + private final StringBuilder sb = new StringBuilder(50); + private boolean first = true; + private int counter; + + @Override + public String toString() { + return sb.toString(); + } + + /** + * Set of an object being added. + */ + public void start(Object bean) { + if (counter == 0) { + id.putIfAbsent(bean, 0); + } + if (counter <= MAX) { + sb.append(bean.getClass().getSimpleName()).append("@").append(counter).append("("); + } + } + + /** + * Add a property as name value pair. + */ + public void add(String name, Object value) { + if (value != null && counter <= MAX) { + if (value instanceof BeanCollection) { + if (((BeanCollection)value).isReference()) { + // suppress unloaded bean collections + return; + } + } + key(name); + value(value); + } + } + + /** + * Add raw content. + */ + public void addRaw(String content) { + sb.append(content); + } + + /** + * End of an object. + */ + public void end() { + if (counter <= MAX) { + sb.append(")"); + } + } + + private void key(String name) { + if (counter > MAX) { + return; + } + if (first) { + first = false; + } else { + sb.append(", "); + } + sb.append(name).append(":"); + } + + private void value(Object value) { + if (counter > MAX) { + return; + } + if (value instanceof ToStringAware) { + if (value instanceof BeanCollection) { + ((ToStringAware) value).toString(this); + } else if (push(value)) { + ((ToStringAware) value).toString(this); + } + } else if (value instanceof Collection) { + addCollection((Collection) value); + } else { + String content = String.valueOf(value); + if (content.length() > TRIM_LENGTH) { + content = content.substring(0, TRIM_LENGTH) + " "; + } + sb.append(content); + if (sb.length() >= MAX_TOTAL_CONTENT) { + sb.append(" ..."); + counter += MAX; + } + } + } + + /** + * Add a collection of values. + */ + public void addCollection(Collection c) { + if (c == null || c.isEmpty()) { + sb.append("[]"); + return; + } + boolean firstElement = true; + sb.append("["); + for (Object o : c) { + if (firstElement) { + firstElement = false; + } else { + sb.append(", "); + } + value(o); + if (counter > MAX) { + return; + } + } + sb.append("]"); + } + + private boolean push(Object bean) { + if (counter > MAX) { + return false; + } + if (counter == MAX) { + sb.append(" ..."); + counter++; + return false; + } + Integer idx = id.putIfAbsent(bean, counter++); + if (idx != null) { + --counter; + sb.append(bean.getClass().getSimpleName()).append("@").append(idx); + return false; + } + first = true; + return true; + } + +} diff --git a/ebean-api/src/main/java/io/ebean/common/BeanList.java b/ebean-api/src/main/java/io/ebean/common/BeanList.java index ba0adca99..0a04eb472 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanList.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanList.java @@ -1,9 +1,6 @@ package io.ebean.common; -import io.ebean.bean.BeanCollection; -import io.ebean.bean.BeanCollectionAdd; -import io.ebean.bean.BeanCollectionLoader; -import io.ebean.bean.EntityBean; +import io.ebean.bean.*; import java.io.Serializable; import java.util.ArrayList; @@ -47,6 +44,11 @@ public final class BeanList extends AbstractBeanCollection implements List super(loader, ownerBean, propertyName); } + @Override + public void toString(ToStringBuilder builder) { + builder.addCollection(list); + } + @Override public void reset(EntityBean ownerBean, String propertyName) { this.ownerBean = ownerBean; @@ -191,18 +193,11 @@ public final class BeanList extends AbstractBeanCollection implements List @Override public String toString() { - StringBuilder sb = new StringBuilder(50); - sb.append("BeanList "); - if (isReadOnly()) { - sb.append("readOnly "); - } if (list == null) { - sb.append("deferred "); + return "BeanList"; } else { - sb.append("size[").append(list.size()).append("] "); - sb.append("list").append(list); + return list.toString(); } - return sb.toString(); } /** diff --git a/ebean-api/src/main/java/io/ebean/common/BeanMap.java b/ebean-api/src/main/java/io/ebean/common/BeanMap.java index 643febc14..454642d55 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanMap.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanMap.java @@ -3,6 +3,7 @@ package io.ebean.common; import io.ebean.bean.BeanCollection; import io.ebean.bean.BeanCollectionLoader; import io.ebean.bean.EntityBean; +import io.ebean.bean.ToStringBuilder; import java.util.Collection; import java.util.Collections; @@ -40,6 +41,19 @@ public final class BeanMap extends AbstractBeanCollection implements Ma super(ebeanServer, ownerBean, propertyName); } + @Override + public void toString(ToStringBuilder builder) { + if (map == null || map.isEmpty()) { + builder.addRaw("{}"); + } else { + builder.addRaw("{"); + for (Entry entry : map.entrySet()) { + builder.add(String.valueOf(entry.getKey()), entry.getValue()); + } + builder.addRaw("}"); + } + } + @Override public void reset(EntityBean ownerBean, String propertyName) { this.ownerBean = ownerBean; @@ -183,27 +197,20 @@ public final class BeanMap extends AbstractBeanCollection implements Ma @Override public String toString() { - StringBuilder sb = new StringBuilder(50); - sb.append("BeanMap "); - if (isReadOnly()) { - sb.append("readOnly "); - } if (map == null) { - sb.append("deferred "); + return "BeanMap"; } else { - sb.append("size[").append(map.size()).append("]"); - sb.append(" map").append(map); + return map.toString(); } - return sb.toString(); } /** - * Equal if obj is a Map and equal in a Map sense. + * Equal if object is a Map and equal in a Map sense. */ @Override - public boolean equals(Object obj) { + public boolean equals(Object object) { init(); - return map.equals(obj); + return map.equals(object); } @Override diff --git a/ebean-api/src/main/java/io/ebean/common/BeanSet.java b/ebean-api/src/main/java/io/ebean/common/BeanSet.java index 0c6f0c508..683b2c1f9 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanSet.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanSet.java @@ -1,9 +1,6 @@ package io.ebean.common; -import io.ebean.bean.BeanCollection; -import io.ebean.bean.BeanCollectionAdd; -import io.ebean.bean.BeanCollectionLoader; -import io.ebean.bean.EntityBean; +import io.ebean.bean.*; import java.io.Serializable; import java.util.Collection; @@ -41,6 +38,11 @@ public final class BeanSet extends AbstractBeanCollection implements Set extends AbstractBeanCollection implements Set"; } else { - sb.append("size[").append(set.size()).append("]"); - sb.append(" set").append(set); + return set.toString(); } - return sb.toString(); } /** diff --git a/ebean-api/src/test/java/io/ebean/bean/ToStringBuilderTest.java b/ebean-api/src/test/java/io/ebean/bean/ToStringBuilderTest.java new file mode 100644 index 000000000..99f125ee5 --- /dev/null +++ b/ebean-api/src/test/java/io/ebean/bean/ToStringBuilderTest.java @@ -0,0 +1,223 @@ +package io.ebean.bean; + +import io.ebean.common.BeanList; +import io.ebean.common.BeanMap; +import io.ebean.common.BeanSet; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; + +class ToStringBuilderTest { + + @Test + void basic() { + ToStringBuilder builder = new ToStringBuilder(); + builder.start(new Object()); + builder.end(); + assertThat(builder.toString()).isEqualTo("Object@0()"); + } + + @Test + void fields() { + ToStringBuilder builder = new ToStringBuilder(); + builder.start(new Object()); + builder.add("a", 1); + builder.add("b", "B"); + builder.end(); + assertThat(builder.toString()).isEqualTo("Object@0(a:1, b:B)"); + } + + @Test + void max100Beans_expect_noMoreContentAfterMax100() { + + String loop100 = addLoopForMaxBeans(100); + String loop101 = addLoopForMaxBeans(101); + String loop900 = addLoopForMaxBeans(900); + + assertThat(loop100).isEqualTo(loop101); + assertThat(loop101).isEqualTo(loop900); + } + + private String addLoopForMaxBeans(int loopMax) { + ToStringBuilder builder = new ToStringBuilder(); + builder.start(new Object()); + for (int i = 0; i <= loopMax; i++) { + addForMax(builder, i); + } + builder.end(); + return builder.toString(); + } + + private void addForMax(ToStringBuilder builder, int i) { + builder.add("c", new B(i)); + } + + @Test + void maxContent_expect_noMoreContentAfterMax2000() { + String loop23 = addContentLoop(23); + String loop24 = addContentLoop(24); + String loop25 = addContentLoop(25); + + assertThat(loop23).isEqualTo(loop24); + assertThat(loop23).isEqualTo(loop25); + } + + private String addContentLoop(int loopMax) { + ToStringBuilder builder = new ToStringBuilder(); + builder.start(new Object()); + for (int i = 0; i <= loopMax; i++) { + addForMaxContent(builder, i); + } + builder.end(); + return builder.toString(); + } + + private void addForMaxContent(ToStringBuilder builder, int i) { + builder.add("someContentThatAddsUp", new Recurse(i, "SomeContentThatAddsUp_SomeContentThatAddsUp!!")); + } + + @Test + void add_referenceBeanCollection_expect_nothingAdded() { + ToStringBuilder builder = new ToStringBuilder(); + builder.add("ref", new BeanList<>(null)); + assertThat(builder.toString()).isEqualTo(""); + } + + @Test + void addCollection_some() { + ToStringBuilder builder = new ToStringBuilder(); + builder.addCollection( new BeanList<>(List.of("A","B"))); + assertThat(builder.toString()).isEqualTo("[A, B]"); + } + + @Test + void addCollection_null() { + ToStringBuilder builder = new ToStringBuilder(); + builder.addCollection(null); + assertThat(builder.toString()).isEqualTo("[]"); + } + + @Test + void addCollection_empty() { + ToStringBuilder builder = new ToStringBuilder(); + builder.addCollection(Collections.emptyList()); + assertThat(builder.toString()).isEqualTo("[]"); + } + + @Test + void flatBean() { + Recurse instance0 = new Recurse(42, "java"); + assertThat(instance0.toString()).isEqualTo("Recurse@0(id:42, nm:java)"); + } + + @Test + void recursive_expect_reference() { + Recurse instance0 = new Recurse(42, "java"); + instance0.other = instance0; + + assertThat(instance0.toString()).isEqualTo("Recurse@0(id:42, nm:java, other:Recurse@0)"); + } + + @Test + void notRecursive() { + Recurse instance0 = new Recurse(42, "java"); + instance0.other = new Recurse(43, "jvm"); + assertThat(instance0.toString()).isEqualTo("Recurse@0(id:42, nm:java, other:Recurse@1(id:43, nm:jvm))"); + } + + @Test + void beanList_null_empty() { + assertThat(toStringFor(new BeanList(null))).isEqualTo("[]"); + assertThat(toStringFor(new BeanList(Collections.emptyList()))).isEqualTo("[]"); + } + + @Test + void beanSet_null_empty() { + assertThat(toStringFor(new BeanSet(null))).isEqualTo("[]"); + assertThat(toStringFor(new BeanSet(Collections.emptySet()))).isEqualTo("[]"); + } + + @Test + void beanMap_null_empty() { + assertThat(toStringFor(new BeanMap(null))).isEqualTo("{}"); + assertThat(toStringFor(new BeanMap(Collections.emptyMap()))).isEqualTo("{}"); + } + + @Test + void beanList_some() { + BeanList list = new BeanList<>(List.of(new Recurse(1, "a"), new Recurse(2, "b"))); + assertThat(toStringFor(list)).isEqualTo("[Recurse@1(id:1, nm:a), Recurse@2(id:2, nm:b)]"); + } + + @Test + void beanSet_some() { + BeanSet list = new BeanSet<>(new LinkedHashSet<>(List.of(new Recurse(1, "a"), new Recurse(2, "b")))); + assertThat(toStringFor(list)).isEqualTo("[Recurse@1(id:1, nm:a), Recurse@2(id:2, nm:b)]"); + } + + @Test + void beanMap_some() { + Map under = new LinkedHashMap<>(); + under.put("a", new Recurse(1, "a")); + under.put("b", new Recurse(2, "b")); + BeanMap list = new BeanMap<>(under); + assertThat(toStringFor(list)).isEqualTo("{a:Recurse@1(id:1, nm:a), b:Recurse@2(id:2, nm:b)}"); + } + + private String toStringFor(ToStringAware aware) { + ToStringBuilder builder = new ToStringBuilder(); + aware.toString(builder); + return builder.toString(); + } + + static final class Recurse implements ToStringAware { + + final int id; + final String nm; + Recurse other; + + Recurse(int id, String nm) { + this.id = id; + this.nm = nm; + } + + public String toString() { + ToStringBuilder builder = new ToStringBuilder(); + toString(builder); + return builder.toString(); + } + + @Override + public void toString(ToStringBuilder builder) { + builder.start(this); + builder.add("id", id); + builder.add("nm", nm); + builder.add("other", other); + builder.end(); + } + } + + static final class B implements ToStringAware { + + final int id; + + B(int id) { + this.id = id; + } + + public String toString() { + ToStringBuilder builder = new ToStringBuilder(); + toString(builder); + return builder.toString(); + } + + @Override + public void toString(ToStringBuilder builder) { + builder.start(this); + builder.add("b", id); + builder.end(); + } + } +} diff --git a/ebean-core/src/main/resources/META-INF/ebean-version.mf b/ebean-core/src/main/resources/META-INF/ebean-version.mf index 6bcacf1d6..2eb6e1e8e 100644 --- a/ebean-core/src/main/resources/META-INF/ebean-version.mf +++ b/ebean-core/src/main/resources/META-INF/ebean-version.mf @@ -1 +1 @@ -ebean-version: 129 +ebean-version: 133 diff --git a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java index 0bf01e2e2..0e5b01853 100644 --- a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java +++ b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java @@ -192,6 +192,6 @@ public class TestElementCollectionBasicMap extends BaseTestCase { final EcmPerson fromJson = DB.json().toBean(EcmPerson.class, asJson); assertThat(fromJson.getName()).isEqualTo("Fiona021"); assertThat(fromJson.getPhoneNumbers()).hasSize(2); - assertThat(fromJson.getPhoneNumbers().toString()).isEqualTo("BeanMap size[2] map{home=021 1234, work=021 4321}"); + assertThat(fromJson.getPhoneNumbers().toString()).isEqualTo("{home=021 1234, work=021 4321}"); } } diff --git a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicSet.java b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicSet.java index 36bb4c28f..8860f6958 100644 --- a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicSet.java +++ b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicSet.java @@ -183,6 +183,6 @@ class TestElementCollectionBasicSet extends BaseTestCase { final EcsPerson fromJson = DB.json().toBean(EcsPerson.class, asJson); assertThat(fromJson.getName()).isEqualTo("Fiona021"); assertThat(fromJson.getPhoneNumbers()).hasSize(2); - assertThat(fromJson.getPhoneNumbers().toString()).isEqualTo("BeanSet size[2] set[021 1234, 021 4321]"); + assertThat(fromJson.getPhoneNumbers().toString()).isEqualTo("[021 1234, 021 4321]"); } } diff --git a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedList.java b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedList.java index 508c84156..a3ec36ab7 100644 --- a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedList.java +++ b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedList.java @@ -190,6 +190,6 @@ public class TestElementCollectionEmbeddedList extends BaseTestCase { final EcblPerson fromJson = DB.json().toBean(EcblPerson.class, asJson); assertThat(fromJson.getName()).isEqualTo("Fiona64021"); assertThat(fromJson.getPhoneNumbers()).hasSize(2); - assertThat(fromJson.getPhoneNumbers().toString()).isEqualTo("BeanList size[2] list[64-021-1234, 64-021-4321]"); + assertThat(fromJson.getPhoneNumbers().toString()).isEqualTo("[64-021-1234, 64-021-4321]"); } } diff --git a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedMap.java b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedMap.java index 0c9ab75a0..bc3cacff1 100644 --- a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedMap.java +++ b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedMap.java @@ -181,6 +181,6 @@ public class TestElementCollectionEmbeddedMap extends BaseTestCase { final EcbmPerson fromJson = DB.json().toBean(EcbmPerson.class, asJson); assertThat(fromJson.getName()).isEqualTo("Fiona64021"); assertThat(fromJson.getPhoneNumbers()).hasSize(2); - assertThat(fromJson.getPhoneNumbers().toString()).isEqualTo("BeanMap size[2] map{home=64-021-1234, work=64-021-4321}"); + assertThat(fromJson.getPhoneNumbers().toString()).isEqualTo("{home=64-021-1234, work=64-021-4321}"); } } diff --git a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEnumSet.java b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEnumSet.java index 27d856347..9482671f1 100644 --- a/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEnumSet.java +++ b/ebean-test/src/test/java/org/tests/model/elementcollection/TestElementCollectionEnumSet.java @@ -45,6 +45,6 @@ public class TestElementCollectionEnumSet extends BaseTestCase { final EcEnumPerson fromJson = DB.json().toBean(EcEnumPerson.class, asJson); assertThat(fromJson.getName()).isEqualTo("Enum Person"); assertThat(fromJson.getTags()).hasSize(2); - assertThat(fromJson.getTags().toString()).isEqualTo("BeanSet size[2] set[BLUE, RED]"); + assertThat(fromJson.getTags().toString()).isEqualTo("[BLUE, RED]"); } } diff --git a/pom.xml b/pom.xml index 475869efb..879400e1e 100644 --- a/pom.xml +++ b/pom.xml @@ -46,8 +46,8 @@ 13.0.0 4.5 7.5 - 13.2.0 - 13.2.0 + 13.2.1-RC1 + 13.2.1-RC1 false