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