Skip to content

Commit

Permalink
Merge pull request #2642 from ebean-orm/feature/2641-ToStringBuilder
Browse files Browse the repository at this point in the history
#2641 -  Provide a decent default toString() implementation for entity beans
  • Loading branch information
rbygrave authored Apr 11, 2022
2 parents 6e3db70 + 6d6e417 commit 5cc2685
Show file tree
Hide file tree
Showing 15 changed files with 447 additions and 48 deletions.
2 changes: 1 addition & 1 deletion ebean-api/src/main/java/io/ebean/bean/BeanCollection.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* <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 {
/**
Expand Down
6 changes: 5 additions & 1 deletion ebean-api/src/main/java/io/ebean/bean/EntityBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* general application consumption.
* </p>
*/
public interface EntityBean extends Serializable {
public interface EntityBean extends Serializable, ToStringAware {

/**
* Return all the property names in defined order.
Expand Down Expand Up @@ -109,4 +109,8 @@ default Object _ebean_getFieldIntercept(int fieldIndex) {
throw new NotEnhancedException();
}

@Override
default void toString(ToStringBuilder builder) {
throw new NotEnhancedException();
}
}
12 changes: 12 additions & 0 deletions ebean-api/src/main/java/io/ebean/bean/ToStringAware.java
Original file line number Diff line number Diff line change
@@ -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);
}
163 changes: 163 additions & 0 deletions ebean-api/src/main/java/io/ebean/bean/ToStringBuilder.java
Original file line number Diff line number Diff line change
@@ -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;
}

}
21 changes: 8 additions & 13 deletions ebean-api/src/main/java/io/ebean/common/BeanList.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -47,6 +44,11 @@ public BeanList(BeanCollectionLoader loader, EntityBean ownerBean, String proper
super(loader, ownerBean, propertyName);
}

@Override
public void toString(ToStringBuilder builder) {
builder.addCollection(list);
}

@Override
public void reset(EntityBean ownerBean, String propertyName) {
this.ownerBean = ownerBean;
Expand Down Expand Up @@ -191,18 +193,11 @@ public boolean isReference() {

@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();
}

/**
Expand Down
31 changes: 19 additions & 12 deletions ebean-api/src/main/java/io/ebean/common/BeanMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -40,6 +41,19 @@ public BeanMap(BeanCollectionLoader ebeanServer, EntityBean ownerBean, String pr
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;
Expand Down Expand Up @@ -183,27 +197,20 @@ public Collection<?> getActualEntries() {

@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
Expand Down
21 changes: 8 additions & 13 deletions ebean-api/src/main/java/io/ebean/common/BeanSet.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -41,6 +38,11 @@ public BeanSet(BeanCollectionLoader loader, EntityBean ownerBean, String propert
super(loader, ownerBean, propertyName);
}

@Override
public void toString(ToStringBuilder builder) {
builder.addCollection(set);
}

@Override
public void reset(EntityBean ownerBean, String propertyName) {
this.ownerBean = ownerBean;
Expand Down Expand Up @@ -169,18 +171,11 @@ public Collection<?> getActualEntries() {

@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();
}

/**
Expand Down
Loading

0 comments on commit 5cc2685

Please sign in to comment.