/*
* Copyright 2016 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.common.base;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* Utilities for obtaining the fields and getter methods of an object using reflection.
* Useful for first-pass debugging of runtime objects.
*/
public class FieldsAndGetters {
/**
* Returns a {@code Stream} of all public fields and their values for the given object.
*
* @see #fields(Object, Predicate)
*/
public static Stream<Map.Entry<Field, Object>> fields(Object obj) {
return fields(obj, Predicates.alwaysTrue());
}
/**
* Returns a {@code Stream} of all public fields which match {@code predicate} and their values for the given object.
* <p>
* This method uses reflection to find all of the public instance fields of the given object,
* and if they pass the given predicate, it includes them in a stream of {@code Map.Entry<Field, Object>}
* where the entry's value is the value of the field for this object.
*/
public static Stream<Map.Entry<Field, Object>> fields(Object obj, Predicate<Field> predicate) {
Class<?> clazz = obj == null ? ObjectIsNull.class : obj.getClass();
return Arrays.asList(clazz.getFields()).stream()
// gotta be public
.filter(field -> Modifier.isPublic(field.getModifiers()))
// gotta be an instance field
.filter(field -> !Modifier.isStatic(field.getModifiers()))
// gotta pass the predicate
.filter(predicate)
// get its value
.map(field -> createEntry(field, tryCall(field.getName(), () -> field.get(obj))));
}
/**
* Returns a {@code Stream} of all public getter methods and their return values for the given object.
*
* @see #getters(Object, Predicate)
*/
public static Stream<Map.Entry<Method, Object>> getters(Object obj) {
return getters(obj, Predicates.alwaysTrue());
}
/**
* Returns a {@code Stream} of all public getter methods which match {@code predicate} and their return values for the given object.
* <p>
* This method uses reflection to find all of the public instance methods which don't take any arguments
* and return a value. If they pass the given predicate, then they are called, and the return value is
* included in a stream of {@code Map.Entry<Method, Object>}.
* <p>
* Note that there are some methods which have the signature of a getter, but actually mutate the object
* being inspected, e.g. {@link java.io.InputStream#read()}. These will be called unless you manually
* exclude them using the predicate.
*/
public static Stream<Map.Entry<Method, Object>> getters(Object obj, Predicate<Method> predicate) {
Class<?> clazz = obj == null ? ObjectIsNull.class : obj.getClass();
return Arrays.asList(clazz.getMethods()).stream()
// we only want methods that don't take parameters
.filter(method -> method.getParameterTypes().length == 0)
// we only want public methods
.filter(method -> Modifier.isPublic(method.getModifiers()))
// we only want instance methods
.filter(method -> !Modifier.isStatic(method.getModifiers()))
// we only want methods that don't return void
.filter(method -> !method.getReturnType().equals(Void.TYPE))
// we only want methods that pass our predicate
.filter(predicate)
// turn it into Map<Method, Result>
.map(method -> createEntry(method, tryCall(method.getName(), () -> method.invoke(obj))));
}
/** Sentinel class for null objects. */
public static class ObjectIsNull {}
/** Executes the given function, return any exceptions it might throw as wrapped values. */
private static Object tryCall(String methodName, Throwing.Supplier<Object> supplier) {
try {
return supplier.get();
} catch (Throwable error) {
return new CallException(methodName, error);
}
}
/** Exception which wraps up a thrown exception - ensures that users don't think an exception was returned. */
private static class CallException extends Exception {
private static final long serialVersionUID = 1206955156719866328L;
private final String methodName;
private CallException(String methodName, Throwable cause) {
super(cause);
this.methodName = methodName;
}
@Override
public String toString() {
return "When calling " + methodName + ": " + getCause().getMessage();
}
}
/**
* Returns a {@code Stream} of all public fields and getter methods and their values for the given object.
*
* @see #getters(Object, Predicate)
*/
public static Stream<Map.Entry<String, Object>> fieldsAndGetters(Object obj) {
return fieldsAndGetters(obj, Predicates.alwaysTrue());
}
/**
* Returns a {@code Stream} of all public fields and getter methods which match {@code predicate} and their values for the given object.
* <p>
* This method combines the results of {@link #fields(Object, Predicate)} and {@link #getters(Object, Predicate)}. The {@code Predicate<String>}
* will be passed the field names and the getter names (which are postfixed by {@code ()} to mark them as methods).
*
* @see #fields(Object, Predicate)
* @see #getters(Object, Predicate)
*/
public static Stream<Map.Entry<String, Object>> fieldsAndGetters(Object obj, Predicate<String> predicate) {
Stream<Map.Entry<String, Object>> fields = fields(obj, field -> predicate.test(field.getName()))
.map(entry -> createEntry(entry.getKey().getName(), entry.getValue()));
Function<Method, String> methodName = method -> method.getName() + "()";
Stream<Map.Entry<String, Object>> getters = getters(obj, field -> predicate.test(methodName.apply(field)))
.map(entry -> createEntry(methodName.apply(entry.getKey()), entry.getValue()));
return Stream.concat(fields, getters);
}
/**
* Passes each field and getter of {@code obj} to {@code evalPredicate}, grabs its value if it passes, and if the value passes {@code dumpPredicate} then it is dumped to {@code printer}.
* @see #fieldsAndGetters(Object, Predicate)
*/
public static void dumpIf(String name, Object obj, Predicate<String> evalPredicate, Predicate<Map.Entry<String, Object>> dumpPredicate, StringPrinter printer) {
printer.println(name + ": " + obj.getClass().getName());
fieldsAndGetters(obj, evalPredicate).filter(dumpPredicate).forEach(entry -> {
printer.println("\t" + entry.getKey() + " = " + entry.getValue());
});
}
/**
* Dumps all fields and getters of {@code obj} to {@code System.out}.
* @see #dumpIf
*/
public static void dumpAll(String name, Object obj) {
dumpAll(name, obj, StringPrinter.systemOut());
}
/**
* Dumps all non-null fields and getters of {@code obj} to {@code System.out}.
* @see #dumpIf
*/
public static void dumpNonNull(String name, Object obj) {
dumpNonNull(name, obj, StringPrinter.systemOut());
}
/**
* Dumps all fields and getters of {@code obj} to {@code printer}.
* @see #dumpIf
*/
public static void dumpAll(String name, Object obj, StringPrinter printer) {
dumpIf(name, obj, Predicates.alwaysTrue(), Predicates.alwaysTrue(), printer);
}
/**
* Dumps all non-null fields and getters of {@code obj} to {@code printer}.
* @see #dumpIf
*/
public static void dumpNonNull(String name, Object obj, StringPrinter printer) {
dumpIf(name, obj, Predicates.alwaysTrue(), entry -> entry.getValue() != null, printer);
}
/** Creates an immutable Map.Entry. */
private static <K, V> Map.Entry<K, V> createEntry(K key, V value) {
return new Map.Entry<K, V>() {
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
throw new UnsupportedOperationException();
}
};
}
}