blob: 8871d92136fee819e02619fbee7779e6bb64b07d [file] [log] [blame]
/**
* Copyright (c) 2014, 2018 itemis AG (http://www.itemis.eu) and others.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.xtext.xbase.lib.util;
import com.google.common.annotations.Beta;
import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import org.eclipse.xtext.xbase.lib.CollectionLiterals;
import org.eclipse.xtext.xbase.lib.Conversions;
import org.eclipse.xtext.xbase.lib.Exceptions;
import org.eclipse.xtext.xbase.lib.Functions.Function1;
import org.eclipse.xtext.xbase.lib.IterableExtensions;
import org.eclipse.xtext.xbase.lib.util.ToStringContext;
/**
* Helps with the construction of good {@link Object#toString()} representations.
* <p>You can customize the output using the builder-style methods {@link ToStringBuilder#singleLine()} {@link ToStringBuilder#skipNulls()} and {@link ToStringBuilder#hideFieldNames()}.</p>
* <p>You can either directly list fields to include via {@link ToStringBuilder#add(String, Object)} and {@link ToStringBuilder#add(Object)}
* or you can let the builder do it automatically using reflection, either including the fields declared in this class or including all superclasses.</p>
* <p>The builder will automatically handle cycles in the object tree. It also pretty prints arrays and Iterables.</p>
*
* This class is not thread safe.
* @since 2.7
*/
@Beta
@GwtCompatible
@SuppressWarnings("all")
public final class ToStringBuilder {
private static ToStringContext toStringContext = ToStringContext.INSTANCE;
private final Object instance;
private final String typeName;
private boolean multiLine = true;
private boolean skipNulls = false;
private boolean showFieldNames = true;
private boolean prettyPrint = true;
private final List<Part> parts = new ArrayList<Part>();
/**
* Creates a new ToStringBuilder for the given object. If you don't use reflection, then this instance
* is only used for obtaining its classes' simple name.
*
* @param instance the object to convert to a String
*/
public ToStringBuilder(final Object instance) {
this.instance = instance;
this.typeName = instance.getClass().getSimpleName();
}
/**
* Fields are printed on a single line, separated by commas instead of newlines
* @return this
*/
public ToStringBuilder singleLine() {
this.multiLine = false;
return this;
}
/**
* Fields with null values will be excluded from the output
* @return this
*/
public ToStringBuilder skipNulls() {
this.skipNulls = true;
return this;
}
/**
* Field names will not be included in the output. Useful for small classes.
* @return this
*/
public ToStringBuilder hideFieldNames() {
this.showFieldNames = false;
return this;
}
/**
* By default, Iterables, Arrays and multiline Strings are pretty-printed.
* Switching to their normal representation makes the toString method significantly faster.
* @since 2.9
* @return this
*/
public ToStringBuilder verbatimValues() {
this.prettyPrint = false;
return this;
}
/**
* Adds all fields declared directly in the object's class to the output
* @return this
*/
@GwtIncompatible("Class.getDeclaredFields")
public ToStringBuilder addDeclaredFields() {
Field[] fields = instance.getClass().getDeclaredFields();
for(Field field : fields) {
addField(field);
}
return this;
}
/**
* Adds all fields declared in the object's class and its superclasses to the output.
* @return this
*/
@GwtIncompatible("Class.getDeclaredFields")
public ToStringBuilder addAllFields() {
List<Field> fields = getAllDeclaredFields(instance.getClass());
for(Field field : fields) {
addField(field);
}
return this;
}
/**
* @param fieldName the name of the field to add to the output using reflection
* @return this
*/
@GwtIncompatible("Class.getDeclaredField(String)")
public ToStringBuilder addField(final String fieldName) {
List<Field> fields = getAllDeclaredFields(instance.getClass());
for(Field field : fields) {
if(fieldName.equals(field.getName())) {
addField(field);
break;
}
}
return this;
}
@GwtIncompatible("java.lang.reflect.Field")
private ToStringBuilder addField(final Field field) {
if (!Modifier.isStatic(field.getModifiers())) {
field.setAccessible(true);
try {
add(field.getName(), field.get(instance));
} catch(IllegalAccessException e) {
throw Exceptions.sneakyThrow(e);
}
}
return this;
}
/**
* @param value the value to add to the output
* @param fieldName the field name to list the value under
* @return this
*/
public ToStringBuilder add(final String fieldName, final Object value) {
return addPart(fieldName, value);
}
/**
* @param value the value to add to the output without a field name
* @return this
*/
public ToStringBuilder add(final Object value) {
return addPart(value);
}
private Part addPart() {
final Part p = new Part();
this.parts.add(p);
return p;
}
private ToStringBuilder addPart(final Object value) {
final Part p = this.addPart();
p.value = value;
return this;
}
private ToStringBuilder addPart(final String fieldName, final Object value) {
final Part p = this.addPart();
p.fieldName = fieldName;
p.value = value;
return this;
}
/**
* @return the String representation of the processed object
*/
@Override
public String toString() {
boolean startProcessing = ToStringBuilder.toStringContext.startProcessing(this.instance);
if (!startProcessing) {
return this.toSimpleReferenceString(this.instance);
}
try {
final IndentationAwareStringBuilder builder = new IndentationAwareStringBuilder();
builder.append(typeName).append(" ");
builder.append("[");
String nextSeparator = "";
if (multiLine) {
builder.increaseIndent();
}
for (Part part : parts) {
if (!skipNulls || part.value != null) {
if (multiLine) {
builder.newLine();
} else {
builder.append(nextSeparator);
nextSeparator = ", ";
}
if (part.fieldName != null && this.showFieldNames) {
builder.append(part.fieldName).append(" = ");
}
this.internalToString(part.value, builder);
}
}
if (multiLine) {
builder.decreaseIndent().newLine();
}
builder.append("]");
return builder.toString();
} finally {
ToStringBuilder.toStringContext.endProcessing(this.instance);
}
}
private void internalToString(final Object object, final IndentationAwareStringBuilder sb) {
if (prettyPrint) {
if (object instanceof Iterable<?>) {
serializeIterable((Iterable<?>)object, sb);
} else if (object instanceof Object[]) {
sb.append(Arrays.toString((Object[])object));
} else if (object instanceof byte[]) {
sb.append(Arrays.toString((byte[])object));
} else if (object instanceof char[]) {
sb.append(Arrays.toString((char[])object));
} else if (object instanceof int[]) {
sb.append(Arrays.toString((int[])object));
} else if (object instanceof boolean[]) {
sb.append(Arrays.toString((boolean[])object));
} else if (object instanceof long[]) {
sb.append(Arrays.toString((long[])object));
} else if (object instanceof float[]) {
sb.append(Arrays.toString((float[])object));
} else if (object instanceof double[]) {
sb.append(Arrays.toString((double[])object));
} else if (object instanceof CharSequence) {
sb.append("\"").append(((CharSequence)object).toString().replace("\n", "\\n").replace("\r", "\\r")).append("\"");
} else if (object instanceof Enum<?>) {
sb.append(((Enum<?>)object).name());
} else {
sb.append(String.valueOf(object));
}
} else {
sb.append(String.valueOf(object));
}
}
private void serializeIterable(final Iterable<?> object, final IndentationAwareStringBuilder sb) {
final Iterator<?> iterator = object.iterator();
sb.append(object.getClass().getSimpleName()).append(" (");
if (multiLine) {
sb.increaseIndent();
}
boolean wasEmpty = true;
while (iterator.hasNext()) {
wasEmpty = false;
if (multiLine) {
sb.newLine();
}
this.internalToString(iterator.next(), sb);
if (iterator.hasNext()) {
sb.append(",");
}
}
if (multiLine) {
sb.decreaseIndent();
}
if (!wasEmpty && this.multiLine) {
sb.newLine();
}
sb.append(")");
}
private String toSimpleReferenceString(final Object obj) {
String simpleName = obj.getClass().getSimpleName();
int identityHashCode = System.identityHashCode(obj);
return simpleName + "@" + Integer.valueOf(identityHashCode);
}
@GwtIncompatible("java.lang.reflect.Field")
private List<Field> getAllDeclaredFields(final Class<?> clazz) {
final ArrayList<Field> result = new ArrayList();
for(Class<?> current = clazz; current != null; current = current.getSuperclass()) {
Field[] declaredFields = current.getDeclaredFields();
result.addAll(Arrays.asList(declaredFields));
}
return result;
}
private static final class Part {
private String fieldName;
private Object value;
}
private static class IndentationAwareStringBuilder {
private final StringBuilder builder = new StringBuilder();
private final String indentationString = " ";
private final String newLineString = "\n";
private int indentation = 0;
public IndentationAwareStringBuilder increaseIndent() {
indentation++;
return this;
}
public IndentationAwareStringBuilder decreaseIndent() {
indentation--;
return this;
}
public IndentationAwareStringBuilder append(final CharSequence string) {
if (indentation > 0) {
String indented = string.toString().replace(
newLineString,
newLineString + Strings.repeat(indentationString, indentation)
);
builder.append(indented);
} else {
builder.append(string);
}
return this;
}
public IndentationAwareStringBuilder newLine() {
builder.append(newLineString).
append(Strings.repeat(this.indentationString, this.indentation));
return this;
}
@Override
public String toString() {
return this.builder.toString();
}
}
}