package org.jdbi.v3.core.mapper.reflect.internal;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.leangen.geantyref.GenericTypeReflector;
import org.jdbi.v3.core.config.ConfigRegistry;
import org.jdbi.v3.core.config.JdbiCache;
import org.jdbi.v3.core.config.JdbiCaches;
import org.jdbi.v3.core.generic.GenericTypes;
import org.jdbi.v3.core.internal.exceptions.Sneaky;
import org.jdbi.v3.core.internal.exceptions.Unchecked;
import org.jdbi.v3.core.mapper.reflect.internal.BeanPropertiesFactory.BeanPojoProperties.PropertiesHolder;
import org.jdbi.v3.core.qualifier.QualifiedType;
import org.jdbi.v3.core.qualifier.Qualifiers;
import org.jdbi.v3.core.statement.UnableToCreateStatementException;
public class BeanPropertiesFactory {
private static final JdbiCache<Type, PropertiesHolder<?>> PROPERTY_CACHE =
JdbiCaches.declare(PropertiesHolder::new);
private BeanPropertiesFactory() {}
public static PojoProperties<?> propertiesFor(Type t, ConfigRegistry config) {
return new BeanPojoProperties<>(t, config);
}
private static boolean shouldSeeProperty(PropertyDescriptor pd) {
final Method read = pd.getReadMethod();
if (read == null) {
return pd.getWriteMethod() != null;
}
return read.getParameterCount() == 0
&& read.getDeclaringClass() != Object.class;
}
static class BeanPojoProperties<T> extends PojoProperties<T> {
private final ConfigRegistry config;
BeanPojoProperties(Type type, ConfigRegistry config) {
super(type);
this.config = config;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Map<String, BeanPojoProperty<T>> getProperties() {
return (Map) PROPERTY_CACHE.get(getType(), config).properties;
}
@Override
public PojoBuilder<T> create() {
final PropertiesHolder<?> holder = PROPERTY_CACHE.get(getType(), config);
final T instance = (T) holder.constructor.get();
return new PojoBuilder<T>() {
@Override
public void set(String property, Object value) {
holder.properties.get(property)
.setter()
.accept(instance, value);
}
@Override
public T build() {
return instance;
}
};
}
static class BeanPojoProperty<T> implements PojoProperty<T> {
final PropertyDescriptor descriptor;
final QualifiedType<?> qualifiedType;
final ConcurrentMap<Class<?>, Optional<Annotation>> annoCache = new ConcurrentHashMap<>();
final Function<Object, Object> getter;
final BiConsumer<Object, Object> setter;
final Type actualBeanType;
BeanPojoProperty(PropertyDescriptor property, Type actualBeanType) {
this.descriptor = property;
this.actualBeanType = actualBeanType;
this.qualifiedType = determineQualifiedType();
getter = Optional.ofNullable(descriptor.getReadMethod())
.map(Unchecked.function(MethodHandles.lookup()::unreflect))
.map(mh -> mh.asType(MethodType.methodType(Object.class, Object.class)))
.map(mh -> Unchecked.function(mh::invokeExact))
.orElse(null);
setter = Optional.ofNullable(descriptor.getWriteMethod())
.map(Unchecked.function(MethodHandles.lookup()::unreflect))
.map(mh -> mh.asType(MethodType.methodType(void.class, Object.class, Object.class)))
.map(mh -> Unchecked.biConsumer(mh::invokeExact))
.orElse(null);
}
protected Function<Object, Object> getter() {
if (getter == null) {
throw new UnableToCreateStatementException(String.format(
"No getter method found for bean property [%s] on [%s]",
getName(), qualifiedType));
}
return getter;
}
protected BiConsumer<Object, Object> setter() {
if (setter == null) {
throw new UnableToCreateStatementException(String.format(
"No setter method found for bean property [%s] on [%s]",
getName(), qualifiedType));
}
return setter;
}
@Override
public String getName() {
return descriptor.getName();
}
@Override
public QualifiedType<?> getQualifiedType() {
return qualifiedType;
}
private QualifiedType<?> determineQualifiedType() {
Parameter setterParam = Optional.ofNullable(descriptor.getWriteMethod())
.map(m -> m.getParameterCount() > 0 ? m.getParameters()[0] : null)
.orElse(null);
return QualifiedType.of(
Optional.ofNullable(descriptor.getReadMethod())
.map(m -> GenericTypeReflector.getExactReturnType(m, actualBeanType))
.orElseGet(() -> GenericTypeReflector.getExactParameterTypes(descriptor.getWriteMethod(), actualBeanType)[0]))
.withAnnotations(
new Qualifiers().findFor(descriptor.getReadMethod(), descriptor.getWriteMethod(), setterParam));
}
@Override
public <A extends Annotation> Optional<A> getAnnotation(Class<A> anno) {
return annoCache.computeIfAbsent(anno, x ->
Stream.of(descriptor.getReadMethod(), descriptor.getWriteMethod())
.filter(Objects::nonNull)
.map(m -> m.getAnnotation(anno))
.filter(Objects::nonNull)
.findFirst()
.map(Annotation.class::cast))
.map(anno::cast);
}
@Override
public Object get(T pojo) {
return getter().apply(pojo);
}
}
static class PropertiesHolder<T> {
final Supplier<T> constructor;
final Map<String, BeanPojoProperty<?>> properties;
PropertiesHolder(Type type) {
final Class<?> clazz = GenericTypes.getErasedType(type);
try {
properties = Arrays.stream(Introspector.getBeanInfo(clazz).getPropertyDescriptors())
.filter(BeanPropertiesFactory::shouldSeeProperty)
.map(p -> new BeanPojoProperty<>(p, type))
.collect(Collectors.toMap(PojoProperty::getName, Function.identity()));
} catch (IntrospectionException e) {
throw new IllegalArgumentException("Failed to inspect bean " + clazz, e);
}
Supplier<T> myConstructor;
try {
MethodHandle ctorMh = MethodHandles.lookup()
.findConstructor(clazz, MethodType.methodType(void.class))
.asType(MethodType.methodType(Object.class));
myConstructor = Unchecked.supplier(() -> (T) ctorMh.invokeExact());
} catch (ReflectiveOperationException e) {
myConstructor = () -> {
throw Sneaky.throwAnyway(e);
};
}
constructor = myConstructor;
}
}
}
}