#2641 - Provide a decent default toString() implementation for entity beans

This commit is contained in:
Rob Bygrave
2022-04-08 17:35:27 +12:00
parent 6e3db70d20
commit 5a7e8456e1
9 changed files with 322 additions and 11 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,153 @@
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) {
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 (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;
}
int collectionPos = 0;
sb.append("[");
for (Object o : c) {
if (collectionPos++ > 0) {
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;
@@ -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;
@@ -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;
@@ -0,0 +1,124 @@
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 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 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();
}
}
}
@@ -1 +1 @@
ebean-version: 129
ebean-version: 133