org.klojang.invoke.BeanReader Maven / Gradle / Ivy
Show all versions of klojang-invoke Show documentation
package org.klojang.invoke;
import org.klojang.check.Check;
import org.klojang.check.Tag;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import static java.util.Map.Entry;
import static org.klojang.check.CommonChecks.*;
import static org.klojang.invoke.IncludeExclude.INCLUDE;
import static org.klojang.invoke.NoSuchPropertyException.noSuchProperty;
/**
* A dynamic bean reader class. This class uses method handles instead of reflection to
* read bean properties. However, it still uses reflection to figure out what those
* properties are in the first place. Therefore, if you use a {@code BeanReader} to read
* beans residing in a Java 9+ module, that module must be "open" to reflective access.
* Reflection is used only transiently. No reflection objects are cached. They are
* disposed of once the required information has been extracted from them.
*
* If you prefer, you can use the {@link BeanReaderBuilder} class to configure
* {@link BeanReader} instances in a completely reflection-free manner. You obtain a
* {@code BeanReaderBuilder} via {@link #forClass(Class) BeanReader.forClass()}.
*
* @param The type of the bean
* @author Ayco Holleman
*/
public final class BeanReader {
/**
* Returns a {@code Builder} for {@code BeanReader} instances. The builder will produce
* a 100% reflection-free {@code BeanReader}, which you may find desirable when writing
* Java 9+ modules.
*
* @param beanClass the class for which to create a {@code BeanReader} (may be a
* {@code record} type)
* @param the type of the objects to be read
* @return a {@code Builder} for {@code BeanReader} instances
*/
public static BeanReaderBuilder forClass(Class beanClass) {
return new BeanReaderBuilder<>(beanClass);
}
private final Class beanClass;
private final Map getters;
private final BeanValueTransformer transformer;
/**
* Creates a {@code BeanReader} for the specified class. You can optionally specify an
* array of properties that you intend to read. If you specify a zero-length array, all
* properties will be readable. If you intend to use this {@code BeanReader} to
* repetitively read just one or two properties from bulky bean types, explicitly
* specifying the properties you intend to read might make the {@code BeanReader}
* slightly more efficient. It is not an error to specify non-existent
* properties. They will be tacitly ignored. A side effect of specifying one or more
* properties is that it forces the values returned from
* {@link #readAllProperties(Object) readAllProperties()} to be in the order in which
* you specify the properties.
*
* @param beanClass the bean class (may be a {@code record} type)
* @param properties the properties to be included/excluded
* @see #getReadableProperties()
*/
public BeanReader(Class beanClass, String... properties) {
this(beanClass, BeanValueTransformer.identity(), properties);
}
/**
* Creates a {@code BeanReader} for the specified class. You can optionally specify an
* array of properties that you intend to read. If you specify a zero-length array, all
* properties will be readable. If you intend to use this {@code BeanReader} to
* repetitively read just one or two properties from bulky bean types, explicitly
* specifying the properties you intend to read might make the {@code BeanReader}
* slightly more efficient. It is not an error to specify non-existent
* properties. They will be tacitly ignored. A side effect of specifying one or more
* properties to be included is that it forces the values returned from
* {@link #readAllProperties(Object) readAllProperties()} to be in the order in which
* you specify the properties.
*
* @param beanClass the bean class (may be a {@code record} type)
* @param includeExclude whether to include or exclude the specified properties
* @param properties the properties to be included/excluded
* @see #getReadableProperties()
* @see #readAllProperties(Object)
*/
public BeanReader(Class beanClass,
IncludeExclude includeExclude,
String... properties) {
this(beanClass, BeanValueTransformer.identity(), includeExclude, properties);
}
/**
* Creates a {@code BeanReader} for the specified class. You can optionally specify an
* array of properties that you intend to read. If you specify a zero-length array, all
* properties will be readable. If you intend to use this {@code BeanReader} to
* repetitively read just one or two properties from bulky bean types, explicitly
* specifying the properties you intend to read might make the {@code BeanReader}
* slightly more efficient. It is not an error to specify non-existent
* properties. They will be tacitly ignored. A side effect of specifying one or more
* properties to be included is that it forces the values returned from
* {@link #readAllProperties(Object) readAllProperties()} to be in the order in which
* you specify the properties.
*
* @param beanClass the bean class (may be a {@code record} type)
* @param strictNaming if {@code false}, all methods with a zero-length parameter
* list and a non-{@code void} return type, except {@code getClass()},
* {@code hashCode()} and {@code toString()}, will be regarded as getters.
* Otherwise JavaBeans naming conventions will be applied regarding which methods
* qualify as getters. By way of exception, methods returning a {@link Boolean}
* are allowed to have a name starting with "is" (just like methods returning a
* {@code boolean}). The {@code strictNaming} parameter is tacitly ignored for
* {@code record} classes. Records are always processed as though
* {@code strictNaming} were {@code false}.
* @param includeExclude whether to include or exclude the subsequently specified
* properties
* @param properties the properties to be included/excluded
*/
public BeanReader(Class beanClass,
boolean strictNaming,
IncludeExclude includeExclude,
String... properties) {
this(beanClass,
strictNaming,
BeanValueTransformer.identity(),
includeExclude,
properties);
}
/**
* Creates a {@code BeanReader} for the specified class. You can optionally specify an
* array of properties that you intend to read. If you specify a zero-length array, all
* properties will be readable. If you intend to use this {@code BeanReader} to
* repetitively read just one or two properties from bulky bean types, explicitly
* specifying the properties you intend to read might make the {@code BeanReader}
* slightly more efficient. It is not an error to specify non-existent
* properties. They will be tacitly ignored. A side effect of specifying one or more
* properties is that it forces the values returned from
* {@link #readAllProperties(Object) readAllProperties()} to be in the order in which
* you specify the properties.
*
* @param beanClass the bean class (may be a {@code record} type)
* @param transformer a conversion function for bean values. The function is
* passed the bean from which the value was retrieved, the property that was read,
* and the value of the property. Using these three parameters, the function can
* compute a new value, which will be the value that is actually returned
* from {@link #read(Object, String) BeanReader.read()}.
* @param properties the properties to be included
*/
public BeanReader(Class beanClass,
BeanValueTransformer transformer,
String... properties) {
this(beanClass, transformer, INCLUDE, properties);
}
/**
* Creates a {@code BeanReader} for the specified class. You can optionally specify an
* array of properties that you intend to read. If you specify a zero-length array, all
* properties will be readable. If you intend to use this {@code BeanReader} to
* repetitively read just one or two properties from bulky bean types, explicitly
* specifying the properties you intend to read might make the {@code BeanReader}
* slightly more efficient. It is not an error to specify non-existent
* properties. They will be tacitly ignored. A side effect of specifying one or more
* properties to be included is that it forces the values returned from
* {@link #readAllProperties(Object) readAllProperties()} to be in the order in which
* you specify the properties.
*
* @param beanClass the bean class (may be a {@code record} type)
* @param transformer a conversion function for bean values. The function is
* passed the bean from which the value was retrieved, the property that was read,
* and the value of the property. Using these three parameters, the function can
* compute a new value, which will be the value that is actually returned
* from {@link #read(Object, String) BeanReader.read()}.
* @param includeExclude whether to include or exclude the subsequently specified
* properties
* @param properties the properties to be included
*/
public BeanReader(Class beanClass,
BeanValueTransformer transformer,
IncludeExclude includeExclude,
String... properties) {
this(beanClass, true, transformer, includeExclude, properties);
}
/**
* Creates a {@code BeanReader} for the specified class. You can optionally specify an
* array of properties that you intend to read. If you specify a zero-length array, all
* properties will be readable. If you intend to use this {@code BeanReader} to
* repetitively read just one or two properties from bulky bean types, explicitly
* specifying the properties you intend to read might make the {@code BeanReader}
* slightly more efficient. It is not an error to specify non-existent
* properties. They will be tacitly ignored. A side effect of specifying one or more
* properties to be included is that it forces the values returned from
* {@link #readAllProperties(Object) readAllProperties()} to be in the order in which
* you specify the properties.
*
* @param beanClass the bean class
* @param strictNaming if {@code false}, all methods with a zero-length parameter
* list and a non-{@code void} return type, except {@code getClass()},
* {@code hashCode()} and {@code toString()}, will be regarded as getters.
* Otherwise JavaBeans naming conventions will be applied regarding which methods
* qualify as getters. By way of exception, methods returning a {@link Boolean}
* are allowed to have a name starting with "is" (just like methods returning a
* {@code boolean}). The {@code strictNaming} parameter is tacitly ignored for
* {@code record} classes. Records are always processed as though
* {@code strictNaming} were {@code false}.
* @param transformer a conversion function for bean values. The function is
* passed the bean from which the value was retrieved, the property that was read,
* and the value of the property. Using these three parameters, the function can
* compute a new value, which will be the value that is actually returned
* from {@link #read(Object, String) BeanReader.read()}.
* @param includeExclude whether to include or exclude the subsequently specified
* properties
* @param properties the properties to be included/excluded
*/
public BeanReader(Class beanClass,
boolean strictNaming,
BeanValueTransformer transformer,
IncludeExclude includeExclude,
String... properties) {
Check.notNull(beanClass, Private.BEAN_CLASS);
Check.notNull(transformer, Private.TRANSFORMER);
Check.notNull(includeExclude, Private.INCLUDE_EXCLUDE);
Check.that(properties, Private.PROPERTIES).is(deepNotNull());
this.beanClass = beanClass;
this.transformer = transformer;
this.getters = getGetters(strictNaming, includeExclude, properties);
}
BeanReader(Class beanClass,
Map getters,
BeanValueTransformer transformer) {
this.beanClass = beanClass;
this.getters = getters;
this.transformer = transformer;
}
/**
* Returns the value of the specified property on the specified bean. If the property
* does not exist a {@link NoSuchPropertyException} is thrown. If this
* {@code BeanReader} was instantiated with a {@link BeanValueTransformer}, the output
* from the transformer is returned.
*
* @param bean the bean instance
* @param property The property
* @param the type of the return value
* @return its value
* @throws NoSuchPropertyException if the specified property does not exist
*/
@SuppressWarnings("unchecked")
public U read(T bean, String property) throws NoSuchPropertyException {
Check.notNull(bean, Tag.BEAN);
Check.notNull(property, Tag.PROPERTY);
Getter getter = getters.get(property);
Check.that(getter).is(notNull(), () -> noSuchProperty(bean, property));
return (U) read(bean, getter);
}
/**
* Returns the values of all readable properties in the specified JavaBean. The values
* will be returned in the same order as the property names returned by
* {@link #getReadableProperties()}.
*
* @param bean the bean from which to read the values
* @return the values of all readable properties in the specified JavaBean
*/
public List