package org.royaldev.thehumanity;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* A class similar to {@link com.google.common.base.MoreObjects.ToStringHelper}, but it auto-populates the String using
* reflection to get fields.
*/
public class ReflectiveToStringHelper {
private final Class<?> clazz;
private final Object instance;
private final Include include;
private final StringBuilder sb = new StringBuilder();
private final Map<String, String> fields = Maps.newHashMap();
private String separator = ",";
private String equality = "=";
private String hashCodeSymbol = "@";
private boolean hashCode;
private Comparator<? super Field> fieldComparator;
private Comparator<? super Entry<String, String>> entryComparator;
/**
* Should not be constructed during runtime.
*
* @see #of(Object)
* @see #of(Object, Include)
*/
private ReflectiveToStringHelper(final Class<?> clazz, final Object instance, final Include include) {
Preconditions.checkNotNull(include, "include was null");
this.clazz = clazz;
this.instance = instance;
this.include = include;
this.addDeclaredFields();
this.addCustoms();
}
/**
* Returns a toString String for the given object, only showing public fields.
* <p>{@code ClassName{field=value,field2=value2,...}}
*
* @param o Object to generate toString for
* @return toString String, ready for returning
* @see #of(Object, Include)
*/
public static ReflectiveToStringHelper of(final Object o) {
return ReflectiveToStringHelper.of(o, Include.create().publics(true));
}
/**
* Returns a toString String for the given object, showing fields specified by {@code include}.
*
* @param o Object to generate toString for
* @param include Include object
* @return toString String, ready for returning
* @see #of(Object)
*/
public static ReflectiveToStringHelper of(final Object o, final Include include) {
Preconditions.checkNotNull(include, "include was null");
return new ReflectiveToStringHelper(o == null ? null : o.getClass(), o, include);
}
/**
* Appends the custom fields to the StringBuilder.
*/
private void addCustoms() {
this.include.custom.forEach((k, v) -> this.fields.put(k, v == null ? "null" : v.toString()));
}
/**
* Appends declared fields to the StringBuilder. This will use {@code includes} to determine which fields to append.
* <p>Note that this does not add "{" or "}".
*/
private void addDeclaredFields() {
if (this.instance == null) return;
final Field[] fieldArray = this.clazz.getDeclaredFields();
final List<Field> fields;
if (this.fieldComparator != null) {
fields = Arrays.stream(fieldArray).sorted(this.fieldComparator).collect(Collectors.toList());
} else {
fields = Arrays.asList(fieldArray);
}
for (final Field field : fields) {
final Tuple<Object, Throwable> value = this.getFieldValue(field);
if (!this.isSafeToInclude(field, value)) continue;
this.addField(field, value);
}
}
private void addField(final Field field, final Tuple<Object, Throwable> value) {
final StringBuilder valueString = new StringBuilder();
if (value.left == null && value.right != null) {
valueString
.append("{")
.append(value.right.getClass().getSimpleName())
.append(":")
.append(value.right.getMessage())
.append("}");
} else {
valueString.append(value.left == null ? "null" : value.left.toString());
}
this.fields.put(this.getFieldName(field), valueString.toString());
}
/**
* Appends the class name to the StringBuilder. Uses {@link Class#getSimpleName()}.
*/
private void appendClassName() {
this.sb.append(this.clazz.getSimpleName());
}
private void appendFields() {
Stream<Entry<String, String>> stream = this.fields.entrySet().stream();
if (this.entryComparator != null) {
stream = stream.sorted(this.entryComparator);
}
this.sb.append(
stream
.map(e -> e.getKey() + this.equality + e.getValue())
.collect(Collectors.joining(this.separator))
);
}
/**
* Gets the name of this field, using mapped names from {@code includes} if necessary.
*
* @param field Field to get name of
* @return Name of field
*/
private String getFieldName(final Field field) {
final String fieldName = field.getName();
return this.include.mappedNames.entrySet().stream()
.filter(e -> e.getKey().equals(fieldName))
.map(Entry::getValue)
.findFirst()
.orElse(fieldName);
}
/**
* Gets the value of a field. If there was any sort of throwable in retrieving the value, the right side of the
* tuple will contain the exception, and the left side will be null.
* <p>If all goes well, the value will be contained on the left side, and null will be contained on the right side.
*
* @param field Field to get value of
* @return Tuple, as described above
*/
private Tuple<Object, Throwable> getFieldValue(final Field field) {
final boolean wasAccessible = field.isAccessible();
field.setAccessible(true);
try {
return new Tuple<>(field.get(this.instance), null);
} catch (final IllegalAccessException ex) {
field.setAccessible(wasAccessible);
return new Tuple<>(null, ex);
}
}
/**
* Checks to see if this field complies with {@code includes}.
*
* @param field Field to check
* @return true if it complies, false if not
*/
private boolean isSafeToInclude(final Field field, final Tuple<Object, Throwable> value) {
final String name = field.getName();
if (value.right == null && this.include.excludeValues.contains(value.left)) {
return false;
}
if (this.include.omitNullValues && value.left == null) {
return false;
}
if (this.include.excludeNames.contains(name) || this.include.excludeClasses.contains(field.getType())) {
return false;
}
if (this.matchesExcludeNameAndClassOrValue(field, value)) {
return false;
}
if (value.right == null && this.include.ensureValues.contains(value.left)) {
return true;
}
if (this.include.ensureNames.contains(name) || this.include.ensureClasses.contains(field.getType())) {
return true;
}
if (this.matchesEnsureNameAndClassOrValue(field, value)) {
return true;
}
final int mods = field.getModifiers();
boolean include = this.include.privates && Modifier.isPrivate(mods);
include = include || (this.include.packages && !Modifier.isPrivate(mods) && !Modifier.isPublic(mods) && !Modifier.isProtected(mods));
include = include || (this.include.protecteds && Modifier.isProtected(mods));
include = include || (this.include.publics && Modifier.isPublic(mods));
include = include && (this.include.keepFinals || !Modifier.isFinal(mods));
include = include && (this.include.keepTransients || !Modifier.isTransient(mods));
include = include && (this.include.keepVolatiles || !Modifier.isVolatile(mods));
return include;
}
/**
* Checks if this field and its value match any ensues.
*
* @param field Field to check
* @param value Value to check
* @return true if ensured, false if not
*/
private boolean matchesEnsureNameAndClassOrValue(final Field field, final Tuple<Object, Throwable> value) {
final Class<?> type = this.include.ensureNamesAndClasses.get(field.getName());
final Tuple<Class<?>, Object> includeValue = this.include.ensureNamesClassesAndValues.get(field.getName());
final boolean matchesType = type == field.getType();
final boolean matchesTypeAndValue = includeValue != null && value.right == null && includeValue.left == field.getType() && Objects.equals(includeValue.right, value.left);
return matchesType || matchesTypeAndValue;
}
/**
* Checks if this field and its value match any excludes.
*
* @param field Field to check
* @param value Value to check
* @return true if excluded, false if not
*/
private boolean matchesExcludeNameAndClassOrValue(final Field field, final Tuple<Object, Throwable> value) {
final Class<?> type = this.include.excludeNamesAndClasses.get(field.getName());
final Tuple<Class<?>, Object> includeValue = this.include.excludeNamesClassesAndValues.get(field.getName());
final boolean matchesType = type == field.getType();
final boolean matchesTypeAndValue = includeValue != null && value.right == null && includeValue.left == field.getType() && Objects.equals(includeValue.right, value.left);
return matchesType || matchesTypeAndValue;
}
/**
* Provides a comparator for the declared and custom fields, represented by Entries. The key is the name of the
* field, and the value is the String representation of the value.
*
* @param entryComparator Comparator for declared and custom fields
* @return this
*/
public ReflectiveToStringHelper entryComparator(final Comparator<? super Entry<String, String>> entryComparator) {
this.entryComparator = entryComparator;
return this;
}
/**
* Sets the equality symbol in the toString. Default is "="
*
* @param equality Equality symbol for fields
* @return this
*/
public ReflectiveToStringHelper equality(final String equality) {
Preconditions.checkNotNull(equality, "equality was null");
this.equality = equality;
return this;
}
/**
* Provides a comparator for the declared fields. To unset, use null.
*
* @param fieldComparator Comparator for declared fields
* @return this
*/
public ReflectiveToStringHelper fieldComparator(final Comparator<? super Field> fieldComparator) {
this.fieldComparator = fieldComparator;
return this;
}
/**
* Produces the String for use with a toString method. This will populate the internal StringBuilder and then clear
* it.
*
* @return Generated string
*/
public String generate() {
if (this.instance == null) {
return "null";
}
this.appendClassName();
if (this.hashCode) {
this.sb.append(this.hashCodeSymbol).append(Integer.toHexString(this.instance.hashCode()));
}
this.sb.append("{");
this.appendFields();
this.sb.append("}");
final String generated = this.sb.toString();
this.sb.setLength(0);
return generated;
}
/**
* Sets whether to include the hash code in the toString or not.
*
* @param hashCode Status of including hashCod
* @return this
*/
public ReflectiveToStringHelper hashCode(final boolean hashCode) {
this.hashCode = hashCode;
return this;
}
/**
* Sets what the hashCode symbol should be. Default to "@"
*
* @param symbol Symbol to use for the hashCode
* @return this
*/
public ReflectiveToStringHelper hashCodeSymbol(final String symbol) {
this.hashCodeSymbol = symbol;
return this;
}
/**
* Sets the field separator in the toString. Default is ","
*
* @param separator Separator for fields
* @return this
*/
public ReflectiveToStringHelper separator(final String separator) {
Preconditions.checkNotNull(separator, "separator was null");
this.separator = separator;
return this;
}
/**
* Class indicating what should be included by {@link ReflectiveToStringHelper}.
*/
public static class Include {
private final List<String> ensureNames = Lists.newArrayList();
private final List<String> excludeNames = Lists.newArrayList();
private final List<Class<?>> ensureClasses = Lists.newArrayList();
private final List<Class<?>> excludeClasses = Lists.newArrayList();
private final List<Object> ensureValues = Lists.newArrayList();
private final List<Object> excludeValues = Lists.newArrayList();
private final Map<String, Class<?>> ensureNamesAndClasses = Maps.newHashMap();
private final Map<String, Class<?>> excludeNamesAndClasses = Maps.newHashMap();
private final Map<String, Tuple<Class<?>, Object>> ensureNamesClassesAndValues = Maps.newHashMap();
private final Map<String, Tuple<Class<?>, Object>> excludeNamesClassesAndValues = Maps.newHashMap();
private final Map<String, String> mappedNames = Maps.newHashMap();
private final Map<String, Object> custom = Maps.newHashMap();
private boolean omitNullValues;
/**
* Should public fields be included?
*/
private boolean publics;
/**
* Should protected fields be included?
*/
private boolean protecteds;
/**
* Should package-local fields be included?
*/
private boolean packages;
/**
* Should private fields be included?
*/
private boolean privates;
/**
* Should final fields be kept? Defaults to {@code true}.
* <p>Note that this only ever removes inclusion. If something does not specifically include fields, this will
* have no effect.
*/
private boolean keepFinals = true;
/**
* Should transient fields be kept? Default to {@code true}.
* <p>Note that this only ever removes inclusion. If something does not specifically include fields, this will
* have no effect.
*/
private boolean keepTransients = true;
/**
* Should volatile fields be kept? Default to {@code true}.
* <p>Note that this only ever removes inclusion. If something does not specifically include fields, this will
* have no effect.
*/
private boolean keepVolatiles = true;
/**
* Creates a new, default Include set.
*/
public Include() {
}
/**
* Creates a new Include set.
*
* @param publics Should public fields be included?
* @param protecteds Should protected fields be included?
* @param packages Should package-local fields be included?
* @param privates Should private fields be included?
* @see ReflectiveToStringHelper
*/
public Include(final boolean publics, final boolean protecteds, final boolean packages, final boolean privates) {
this.publics = publics;
this.protecteds = protecteds;
this.packages = packages;
this.privates = privates;
}
/**
* Creates an empty (default) Include set. This includes nothing until told to.
*
* @return Empty Include set
*/
public static Include create() {
return new Include();
}
/**
* Convenience method to set all visibilities to one value.
*
* @param allVisibilities Status of all visibilities
* @return this
* @see #publics(boolean)
* @see #protecteds(boolean)
* @see #packages(boolean)
* @see #privates(boolean)
*/
public Include allVisibilities(final boolean allVisibilities) {
this.publics = this.protecteds = this.packages = this.privates = allVisibilities;
return this;
}
/**
* Adds a custom value to the toString.
*
* @param key Name (key) of custom value
* @param value Value custom key
* @return this
* @see #remove(String)
*/
public Include custom(final String key, final Object value) {
Preconditions.checkNotNull(key, "key was null");
this.custom.put(key, value);
return this;
}
/**
* Ensure that any field with the given name, class, and value is shown in the toString.
*
* @param name Name of field to ensure
* @param clazz Class of field to ensure
* @param value Required value of field to ensure
* @return this
*/
public Include ensure(final String name, final Class<?> clazz, final Object value) {
Preconditions.checkNotNull(name, "name was null");
Preconditions.checkNotNull(clazz, "clazz was null");
this.ensureNamesClassesAndValues.put(name, new Tuple<>(clazz, value));
return this;
}
/**
* Ensure that any field matching the given class is shown in the toString.
*
* @param clazz Class of fields to ensure
* @return this
*/
public Include ensure(final Class<?> clazz) {
Preconditions.checkNotNull(clazz, "clazz was null");
this.ensureClasses.add(clazz);
return this;
}
/**
* Ensures that any field with the given name is shown in the toString.
*
* @param name Name of field to ensure
* @return this
*/
public Include ensure(final String name) {
Preconditions.checkNotNull(name, "name was null");
this.ensureNames.add(name);
return this;
}
/**
* Ensures that any field with the given name and class is shown in the toString.
*
* @param name Name of field to ensure
* @param clazz Class of field to ensure
* @return this
*/
public Include ensure(final String name, final Class<?> clazz) {
Preconditions.checkNotNull(name, "name was null");
Preconditions.checkNotNull(clazz, "clazz was null");
this.ensureNamesAndClasses.put(name, clazz);
return this;
}
/**
* Ensures that any field with the given value is shown in the toString.
*
* @param value Required value to ensure
* @return this
*/
public Include ensureValue(final Object value) {
this.ensureValues.add(value);
return this;
}
/**
* Excludes any field with the given name, class, and value from the toString.
*
* @param name Name of field to exclude
* @param clazz Class of field to exclude
* @param value Required value of field to exclude
* @return this
*/
public Include exclude(final String name, final Class<?> clazz, final Object value) {
Preconditions.checkNotNull(name, "name was null");
Preconditions.checkNotNull(clazz, "clazz was null");
this.excludeNamesClassesAndValues.put(name, new Tuple<>(clazz, value));
return this;
}
/**
* Excludes any field with the given class from the toString.
*
* @param clazz Class of fields to exclude
* @return this
*/
public Include exclude(final Class<?> clazz) {
Preconditions.checkNotNull(clazz, "clazz was null");
this.excludeClasses.add(clazz);
return this;
}
/**
* Excludes any field with the given name from the toString.
*
* @param name Name of field to exclude
* @return this
*/
public Include exclude(final String name) {
Preconditions.checkNotNull(name, "name was null");
this.excludeNames.add(name);
return this;
}
/**
* Excludes any field with the given name and class from the toString.
*
* @param name Name of field to exclude
* @param clazz Class of field to exclude
* @return this
*/
public Include exclude(final String name, final Class<?> clazz) {
Preconditions.checkNotNull(name, "name was null");
Preconditions.checkNotNull(clazz, "clazz was null");
this.excludeNamesAndClasses.put(name, clazz);
return this;
}
/**
* Excludes any field with the given field from the toString.
*
* @param value Required value to exclude
* @return this
*/
public Include excludeValue(final Object value) {
this.excludeValues.add(value);
return this;
}
/**
* Ignores (or forgets) any field with the given class.
* <p>This negates the effect of the matching {@code ensure} method.
*
* @param clazz Class to ignore
* @return this
*/
public Include ignore(final Class<?> clazz) {
Preconditions.checkNotNull(clazz, "clazz was null");
this.ensureClasses.remove(clazz);
this.excludeClasses.remove(clazz);
return this;
}
/**
* Ignores (or forgets) any field with the given name.
* <p>This negates the effect of the matching {@code ensure} method.
*
* @param name Name to ignore
* @return this
*/
public Include ignore(final String name) {
Preconditions.checkNotNull(name, "name was null");
this.ensureNames.remove(name);
this.excludeNames.remove(name);
this.ensureNamesAndClasses.remove(name);
this.excludeNamesAndClasses.remove(name);
return this;
}
/**
* Ignores (or forgets) any field with the given value.
* <p>This negates the effect of the matching {@code ensure} method.
*
* @param value Required value to ignore
* @return this
*/
public Include ignoreValue(final Object value) {
this.ensureValues.remove(value);
this.excludeValues.remove(value);
return this;
}
/**
* Handles the processing of final fields.
* <p>If finals are to be kept (true), no discernible change will occur in the toString.
* <p>If finals are not to be kept (false), any final field that would have appeared will now be excluded.
*
* @param keepFinals Status of keeping final fields
* @return this
*/
public Include keepFinals(final boolean keepFinals) {
this.keepFinals = keepFinals;
return this;
}
/**
* Handles the processing of transient fields.
* <p>If transients are to be kept (true), no discernible change will occur in the toString.
* <p>If transients are not to be kept (false), any transient field that would have appeared will now be
* excluded.
*
* @param keepTransients Status of keeping transient fields
* @return this
*/
public Include keepTransients(final boolean keepTransients) {
this.keepTransients = keepTransients;
return this;
}
/**
* Handles the processing of volatile fields.
* <p>If volatiles are to be kept (true), no discernible change will occur in the toString.
* <p>If volatiles are not to be kept (false), any volatile field that would have appeared will now be excluded.
*
* @param keepVolatiles Status of keeping final fields
* @return this
*/
public Include keepVolatiles(final boolean keepVolatiles) {
this.keepVolatiles = keepVolatiles;
return this;
}
/**
* Maps the name of a field to something new in the toString.
*
* @param originalName Name of the field to map to something else
* @param newName New name to appear in the toString
* @return this
*/
public Include map(final String originalName, final String newName) {
Preconditions.checkNotNull(originalName, "originalName was null");
Preconditions.checkNotNull(newName, "newName was null");
this.mappedNames.put(originalName, newName);
return this;
}
/**
* Handles the appropriate omission of fields with values that are null.
* <p>If null-value fields are to be omitted (true), any field which has a value equating to null will not
* appear in the toString.
* <p>If null-value fields are to be kept (false, default), the toString will have no discernible change.
*
* @param omitNullValues Status of omitting null-value fields
* @return this
*/
public Include omitNullValues(final boolean omitNullValues) {
this.omitNullValues = omitNullValues;
return this;
}
/**
* Handles the processing of package-local fields.
* <p>If package-local fields are to be included (true), they will appear in the toString.
* <p>If package-local fields are to be excluded (false), they will <em>not</em> appear in the toString.
*
* @param packages Status of including package-local fields
* @return this
*/
public Include packages(final boolean packages) {
this.packages = packages;
return this;
}
/**
* Handles the processing of private fields.
* <p>If private fields are to be included (true), they will appear in the toString.
* <p>If private fields are to be excluded (false), they will <em>not</em> appear in the toString.
*
* @param privates Status of including private fields
* @return this
*/
public Include privates(final boolean privates) {
this.privates = privates;
return this;
}
/**
* Handles the processing of protected fields.
* <p>If protected fields are to be included (true), they will appear in the toString.
* <p>If protected fields are to be excluded (false), they will <em>not</em> appear in the toString.
*
* @param protecteds Status of including package-local fields
* @return this
*/
public Include protecteds(final boolean protecteds) {
this.protecteds = protecteds;
return this;
}
/**
* Handles the processing of public fields.
* <p>If public fields are to be included (true), they will appear in the toString.
* <p>If public fields are to be excluded (false), they will <em>not</em> appear in the toString.
*
* @param publics Status of including package-local fields
* @return this
*/
public Include publics(final boolean publics) {
this.publics = publics;
return this;
}
/**
* The functional opposite of {@link #custom(String, Object)}. This removes a custom value.
*
* @param key Key to remove
* @return this
* @see #custom(String, Object)
*/
public Include remove(final String key) {
Preconditions.checkNotNull(key, "key was null");
this.custom.remove(key);
return this;
}
/**
* Removes the mapping of the name of a field.
*
* @param originalName Name of the field that was mapped
* @return this
*/
public Include unmap(final String originalName) {
Preconditions.checkNotNull(originalName, "originalName was null");
this.mappedNames.remove(originalName);
return this;
}
}
private static class Tuple<L, R> {
private final L left;
private final R right;
private Tuple(final L left, final R right) {
this.left = left;
this.right = right;
}
@Override
public boolean equals(final Object obj) {
if (obj == this) return true;
if (!(obj instanceof Tuple)) return false;
final Tuple other = (Tuple) obj;
return Objects.equals(this.left, other.left) && Objects.equals(this.right, other.right);
}
}
/**
* Returns the result of {@link #generate()}.
*
* @return Generated toString
* @see #generate()
*/
@Override
public String toString() {
return this.generate();
}
}