ObjectPrinterBuilder.java
package org.codefilarete.trace;
import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorByMethod;
import org.codefilarete.reflection.AccessorByMethodReference;
import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.PropertyAccessor;
import org.codefilarete.reflection.SerializableAccessor;
import org.codefilarete.reflection.ValueAccessPoint;
import org.codefilarete.reflection.ValueAccessPointByMethodReference;
import org.codefilarete.reflection.ValueAccessPointSet;
import org.codefilarete.tool.Experimental;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.StringAppender;
import org.codefilarete.tool.bean.InstanceMethodIterator;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.KeepOrderSet;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
/**
* Builder for {@link ObjectPrinter}. {@link ObjectPrinter} may be used to give a trace of some instances, to be logged or debug.
* Kind of Apache Commons ToStringBuilder with method references.
*
* @author Guillaume Mary
*/
@Experimental(todo = { "implement recursion and overall prevent from stackoverflow", "test !" })
public class ObjectPrinterBuilder<C> {
/**
* Starts a printer configurer that will print all (public) methods of given class (including inherited ones).
* Non wished properties may be removed by using {@link #except(SerializableAccessor)} on result.
*
* @param type the class which methods must be printed
* @return a {@link ObjectPrinterBuilder} that will print all methods of given class, and may be further configured
*/
@Experimental(todo = { "remove addProperty from result" })
public static <T> ObjectPrinterBuilder<T> printerFor(Class<T> type) {
ObjectPrinterBuilder<T> result = new ObjectPrinterBuilder<>();
Iterable<Method> methodIterable = () -> new InstanceMethodIterator(type);
// we add class getters
for (Method method : methodIterable) {
AccessorByMethod<T, Object> accessorByMethod = Reflections.onJavaBeanPropertyWrapperNameGeneric(method.getName(), method,
m -> new AccessorByMethod<>(method),
m -> null,
m -> new AccessorByMethod<>(method),
m -> null /* method is not a getter, we exclude it by returning null (filtered below) */);
if (accessorByMethod != null) {
result.addProperty(accessorByMethod);
}
}
return result;
}
private final KeepOrderSet<PropertyDefinition<? extends C>> printableProperties = new KeepOrderSet<>();
/** @apiNote we use a {@link ValueAccessPointSet} because its supports well contains() method with {@link Accessor} as argument */
private final ValueAccessPointSet<C, ValueAccessPoint<C>> excludedProperties = new ValueAccessPointSet<>();
private final Map<Class, Function<Object, String>> overriddenPrinters = new HashMap<>();
/**
* Adds a property to be printed through its getter
*
* @param getter the method reference that gives access to the property, can be one the parameterized class or one of its subtype
* @return this
*/
public <D extends C> ObjectPrinterBuilder<C> addProperty(SerializableAccessor<D, ?> getter) {
AccessorByMethodReference<D, ?> accessor = new AccessorByMethodReference<>(getter);
return addProperty(accessor);
}
private <D extends C> ObjectPrinterBuilder<C> addProperty(PropertyAccessor<D, ?> getter) {
this.printableProperties.add(new PropertyDefinition<>(getter, null));
return this;
}
public <D extends C, X, S extends Collection<X>> ObjectPrinterBuilder<C> addProperty(SerializableAccessor<D, S> getter, Class<X> componentType) {
AccessorByMethodReference<D, ?> accessor = new AccessorByMethodReference<>(getter);
return addProperty(accessor, componentType);
}
private <D extends C, X> ObjectPrinterBuilder<C> addProperty(PropertyAccessor<D, ?> getter, Class<X> componentType) {
this.printableProperties.add(new PropertyDefinition<>(getter, componentType));
return this;
}
/**
* Excludes a property from being printed, useful in combination of {@link #printerFor(Class)}
*
* @param getter the method reference that gives access to the property
* @return this
*/
public ObjectPrinterBuilder<C> except(SerializableAccessor<C, ?> getter) {
this.excludedProperties.add(new AccessorByMethodReference<>(getter));
return this;
}
/**
* Specifies a printer for a particular type
*
* @param overridenType the type which printing must be changed
* @param printer the function to use for printing
* @param <E> customized type
* @return this
*/
public <E> ObjectPrinterBuilder<C> withPrinter(Class<E> overridenType, Function<E, String> printer) {
this.overriddenPrinters.put(overridenType, (Function<Object, String>) printer);
return this;
}
/**
* Builds final printer
*
* @return a configured printer for current type
*/
public ObjectPrinter<C> build() {
LinkedHashMap<String, PropertyDefinition<C>> printingFunctionByPropertyName = new LinkedHashMap<>();
for (PropertyDefinition<? extends C> printableProperty : printableProperties) {
String methodName = AccessorDefinition.giveDefinition(printableProperty.getGetter()).getName();
if (!excludedProperties.contains(printableProperty.getGetter())) {
printingFunctionByPropertyName.put(methodName, (PropertyDefinition<C>) printableProperty);
}
}
return new ObjectPrinter<>(printingFunctionByPropertyName, overriddenPrinters);
}
/**
* Printer for parameterized type
*
* @param <C> target type to print
*/
public static class ObjectPrinter<C> {
private final Map<String, PropertyDefinition<C>> printableProperties;
private final Map<Class, Function<Object, String>> overriddenPrinters;
/**
* @apiNote private because {@link ObjectPrinterBuilder} is expected to be used for configuration
*/
private ObjectPrinter(LinkedHashMap<String, PropertyDefinition<C>> printableProperties, Map<Class, Function<Object, String>> overriddenPrinters) {
this.printableProperties = printableProperties;
this.overriddenPrinters = overriddenPrinters;
}
/**
* @param object an instance to be printed
* @return a {@link String} representing given instance according to configured properties to print
*/
public String toString(C object) {
StringAppender result = new StringAppender();
String separator = ",";
printableProperties.forEach((propName, getter) -> {
if (getter.getComponentType() != null) {
Object value = getter.getGetter().get(object);
if (value != null) {
StringAppender collectionResult = new StringAppender();
((Collection) value).forEach(item -> {
collectionResult.cat(item.getClass().getSimpleName(), "{");
Entry<Class, Function<Object, String>> foundOverringPrinter = Iterables.find(overriddenPrinters.entrySet(),
e -> getter.getComponentType().isAssignableFrom(e.getKey()));
Object valueToPrint;
if (foundOverringPrinter != null) {
valueToPrint = foundOverringPrinter.getValue().apply(item);
} else {
valueToPrint = item;
}
collectionResult.cat(valueToPrint, "}", separator);
});
collectionResult.cutTail(separator.length());
result.cat(propName, "=[", collectionResult, "]", separator);
} else {
result.cat(propName, "=null", separator);
}
} else {
// we prevent subclass property accessor of being invoked on parent class
boolean getterCompliesWithInstance;
if (getter.getGetter() instanceof ValueAccessPointByMethodReference) {
getterCompliesWithInstance = ((ValueAccessPointByMethodReference<C>) getter.getGetter()).getDeclaringClass().isInstance(object);
} else {
// necessarly AccessorByMethod, see printerFor(Class)
getterCompliesWithInstance = ((AccessorByMethod) getter.getGetter()).getGetter().getDeclaringClass().isInstance(object);
}
if (getterCompliesWithInstance) {
final Object value = getter.getGetter().get(object);
Entry<Class, Function<Object, String>> foundOverringPrinter = Iterables.find(overriddenPrinters.entrySet(),
e -> e.getKey().isInstance(value));
Object valueToPrint;
if (foundOverringPrinter != null) {
String printerValue = foundOverringPrinter.getValue().apply(value);
valueToPrint = foundOverringPrinter.getKey().getSimpleName() + "{" + printerValue + "}";
} else {
valueToPrint = value;
}
result.cat(propName, "=", valueToPrint, separator);
}
}
});
return result.cutTail(separator.length()).toString();
}
}
private static class PropertyDefinition<C> {
private final PropertyAccessor<C, ?> getter;
private final Class<?> componentType;
PropertyDefinition(PropertyAccessor<C, ?> getter, Class<?> componentType) {
this.getter = getter;
this.componentType = componentType;
}
public Accessor<C, ?> getGetter() {
return getter;
}
public Class<?> getComponentType() {
return componentType;
}
}
}