ObjectPrinterBuilder.java

package org.codefilarete.trace;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;

import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.ValueAccessPointByMethodReference;
import org.danekja.java.util.function.serializable.SerializableFunction;
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 org.codefilarete.reflection.AccessorByMethod;
import org.codefilarete.reflection.AccessorByMethodReference;
import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.ValueAccessPointSet;

/**
 * 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(SerializableFunction)} 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;
	}
	
	/** @apiNote  */
	private final KeepOrderSet<Accessor<C, Object>> printableProperties = new KeepOrderSet<>();
	/** @apiNote we use a {@link ValueAccessPointSet} because its supports well contains() method with {@link Accessor} as argument */
	private final ValueAccessPointSet<C> excludedProperties = new ValueAccessPointSet<>();
	
	private final Map<Class, Function<Object, String>> overridenPrinters = 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(SerializableFunction<D, Object> getter) {
		return this.addProperty(new AccessorByMethodReference(getter));
	}
	
	private ObjectPrinterBuilder<C> addProperty(Accessor<C, Object> getter) {
		this.printableProperties.add(getter);
		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(SerializableFunction<C, Object> 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.overridenPrinters.put(overridenType, (Function<Object, String>) printer);
		return this;
	}
	
	/**
	 * Builds final printer
	 * 
	 * @return a configured printer for current type
	 */
	public ObjectPrinter<C> build() {
		LinkedHashMap<String, Accessor<C, Object>> printingFunctionByPropertyName = new LinkedHashMap<>();
		for (Accessor<C, Object> printableProperty : printableProperties) {
			String methodName = AccessorDefinition.giveDefinition(printableProperty).getName();
			if (!excludedProperties.contains(printableProperty)) {
				printingFunctionByPropertyName.put(methodName, printableProperty);
			}
		}
		return new ObjectPrinter<>(printingFunctionByPropertyName, overridenPrinters);
	}
	
	/**
	 * Printer for parameterized type
	 * 
	 * @param <C> target type to print
	 */
	public static class ObjectPrinter<C> {
		
		private final Map<String, Accessor<C, Object>> printableProperties;
		private final Map<Class, Function<Object, String>> overridenPrinters;
		
		/**
		 * @apiNote private because {@link ObjectPrinterBuilder} is expected to be used for configuration 
		 */
		private ObjectPrinter(LinkedHashMap<String, Accessor<C, Object>> printableProperties, Map<Class, Function<Object, String>> overridenPrinters) {
			this.printableProperties = printableProperties;
			this.overridenPrinters = overridenPrinters;
		}
		
		/**
		 * @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) -> {
				// we prevent subclass property accessor of being invoked on parent class
				boolean getterCompliesWithInstance;
				if (getter instanceof ValueAccessPointByMethodReference) {
					getterCompliesWithInstance = ((ValueAccessPointByMethodReference<C>) getter).getDeclaringClass().isInstance(object);
				} else {
					// necessarly AccessorByMethod, see printerFor(Class)
					getterCompliesWithInstance = ((AccessorByMethod) getter).getGetter().getDeclaringClass().isInstance(object);
				}
				if (getterCompliesWithInstance) {
					final Object value = getter.get(object);
					Entry<Class, Function<Object, String>> foundOverringPrinter = Iterables.find(overridenPrinters.entrySet(),
							e -> e.getKey().isInstance(value));
					Object valueToPrint;
					if (foundOverringPrinter != null) {
						valueToPrint = foundOverringPrinter.getValue().apply(value);
					} else {
						valueToPrint = value;
					}
					result.cat(propName, "=", valueToPrint, separator);
				}
			});
			return result.cutTail(separator.length()).toString();
		}
	}
}