Merge remote-tracking branch 'upstream/feature/2641-ToStringBuilder'

This commit is contained in:
Roland Praml
2022-04-11 09:10:52 +02:00
15 changed files with 447 additions and 48 deletions
@@ -18,7 +18,7 @@ import java.util.Set;
* <em>java.util.Collection</em>. 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<E> extends Serializable {
public interface BeanCollection<E> extends Serializable, ToStringAware {
enum ModifyListenMode {
/**
@@ -11,7 +11,7 @@ import java.io.Serializable;
* general application consumption.
* </p>
*/
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();
}
}
@@ -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);
}
@@ -0,0 +1,163 @@
package io.ebean.bean;
import java.util.Collection;
import java.util.IdentityHashMap;
/**
* Helps build toString content taking into account recursion.
* <p>
* 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<Object, Integer> 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) + " <trimmed>";
}
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;
}
}
@@ -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<E> extends AbstractBeanCollection<E> 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<E> extends AbstractBeanCollection<E> 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<deferred>";
} else {
sb.append("size[").append(list.size()).append("] ");
sb.append("list").append(list);
return list.toString();
}
return sb.toString();
}
/**
@@ -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<K, E> extends AbstractBeanCollection<E> implements Ma
super(ebeanServer, ownerBean, propertyName);
}
@Override
public void toString(ToStringBuilder builder) {
if (map == null || map.isEmpty()) {
builder.addRaw("{}");
} else {
builder.addRaw("{");
for (Entry<K, E> 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<K, E> extends AbstractBeanCollection<E> 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<deferred>";
} 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
@@ -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<E> extends AbstractBeanCollection<E> implements Set<E
super(loader, ownerBean, propertyName);
}
@Override
public void toString(ToStringBuilder builder) {
builder.addCollection(set);
}
@Override
public void reset(EntityBean ownerBean, String propertyName) {
this.ownerBean = ownerBean;
@@ -169,18 +171,11 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
@Override
public String toString() {
StringBuilder sb = new StringBuilder(50);
sb.append("BeanSet ");
if (isReadOnly()) {
sb.append("readOnly ");
}
if (set == null) {
sb.append("deferred ");
return "BeanSet<deferred>";
} else {
sb.append("size[").append(set.size()).append("]");
sb.append(" set").append(set);
return set.toString();
}
return sb.toString();
}
/**
@@ -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<String>(null))).isEqualTo("[]");
assertThat(toStringFor(new BeanList<String>(Collections.emptyList()))).isEqualTo("[]");
}
@Test
void beanSet_null_empty() {
assertThat(toStringFor(new BeanSet<String>(null))).isEqualTo("[]");
assertThat(toStringFor(new BeanSet<String>(Collections.emptySet()))).isEqualTo("[]");
}
@Test
void beanMap_null_empty() {
assertThat(toStringFor(new BeanMap<String, String>(null))).isEqualTo("{}");
assertThat(toStringFor(new BeanMap<String, String>(Collections.emptyMap()))).isEqualTo("{}");
}
@Test
void beanList_some() {
BeanList<Recurse> 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<Recurse> 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<String, Recurse> under = new LinkedHashMap<>();
under.put("a", new Recurse(1, "a"));
under.put("b", new Recurse(2, "b"));
BeanMap<String, Recurse> 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();
}
}
}
@@ -1 +1 @@
ebean-version: 129
ebean-version: 133
@@ -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}");
}
}
@@ -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]");
}
}
@@ -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]");
}
}
@@ -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}");
}
}
@@ -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]");
}
}
+2 -2
View File
@@ -46,8 +46,8 @@
<ebean-migration.version>13.0.0</ebean-migration.version>
<ebean-test-docker.version>4.5</ebean-test-docker.version>
<ebean-datasource.version>7.5</ebean-datasource.version>
<ebean-agent.version>13.2.0</ebean-agent.version>
<ebean-maven-plugin.version>13.2.0</ebean-maven-plugin.version>
<ebean-agent.version>13.2.1-RC1</ebean-agent.version>
<ebean-maven-plugin.version>13.2.1-RC1</ebean-maven-plugin.version>
<surefire.useModulePath>false</surefire.useModulePath>
</properties>