package org.springframework.data.spel;
import static org.springframework.data.util.StreamUtils.*;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.BeanUtils;
import org.springframework.data.spel.EvaluationContextExtensionInformation.ExtensionTypeInformation.PublicMethodAndFieldFilter;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.Function;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldFilter;
import org.springframework.util.ReflectionUtils.MethodFilter;
class EvaluationContextExtensionInformation {
private final ExtensionTypeInformation extensionTypeInformation;
private final Optional<RootObjectInformation> rootObjectInformation;
public EvaluationContextExtensionInformation(Class<? extends EvaluationContextExtension> type) {
Assert.notNull(type, "Extension type must not be null!");
Class<?> rootObjectType = org.springframework.data.util.ReflectionUtils.findRequiredMethod(type, "getRootObject")
.getReturnType();
this.rootObjectInformation = Optional
.ofNullable(Object.class.equals(rootObjectType) ? null : new RootObjectInformation(rootObjectType));
this.extensionTypeInformation = new ExtensionTypeInformation(type);
}
public ExtensionTypeInformation getExtensionTypeInformation() {
return this.extensionTypeInformation;
}
public RootObjectInformation getRootObjectInformation(Optional<Object> target) {
return target.map(it -> rootObjectInformation.orElseGet(() -> new RootObjectInformation(it.getClass())))
.orElse(RootObjectInformation.NONE);
}
@Getter
public static class ExtensionTypeInformation {
private final Map<String, Object> properties;
private final MultiValueMap<String, Function> functions;
public ExtensionTypeInformation(Class<? extends EvaluationContextExtension> type) {
Assert.notNull(type, "Extension type must not be null!");
this.functions = discoverDeclaredFunctions(type);
this.properties = discoverDeclaredProperties(type);
}
private static MultiValueMap<String, Function> discoverDeclaredFunctions(Class<?> type) {
MultiValueMap<String, Function> map = CollectionUtils.toMultiValueMap(new HashMap<>());
ReflectionUtils.doWithMethods(type,
method -> map.add(method.getName(), new Function(method, null)),
PublicMethodAndFieldFilter.STATIC);
return CollectionUtils.unmodifiableMultiValueMap(map);
}
@RequiredArgsConstructor
static class PublicMethodAndFieldFilter implements MethodFilter, FieldFilter {
public static final PublicMethodAndFieldFilter STATIC = new PublicMethodAndFieldFilter(true);
public static final PublicMethodAndFieldFilter NON_STATIC = new PublicMethodAndFieldFilter(false);
private final boolean staticOnly;
@Override
public boolean matches(Method method) {
if (ReflectionUtils.isObjectMethod(method)) {
return false;
}
boolean methodStatic = Modifier.isStatic(method.getModifiers());
boolean staticMatch = staticOnly ? methodStatic : !methodStatic;
return Modifier.isPublic(method.getModifiers()) && staticMatch;
}
@Override
public boolean matches(Field field) {
boolean fieldStatic = Modifier.isStatic(field.getModifiers());
boolean staticMatch = staticOnly ? fieldStatic : !fieldStatic;
return Modifier.isPublic(field.getModifiers()) && staticMatch;
}
}
}
static class RootObjectInformation {
private static final RootObjectInformation NONE = new RootObjectInformation(Object.class);
private final Map<String, Method> accessors;
private final Collection<Method> methods;
private final Collection<Field> fields;
public RootObjectInformation(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
this.accessors = new HashMap<>();
this.methods = new HashSet<>();
this.fields = new ArrayList<>();
if (Object.class.equals(type)) {
return;
}
Streamable<PropertyDescriptor> descriptors = Streamable.of(BeanUtils.getPropertyDescriptors(type));
ReflectionUtils.doWithMethods(type, method -> {
RootObjectInformation.this.methods.add(method);
descriptors.stream()
.filter(it -> method.equals(it.getReadMethod()))
.forEach(it -> RootObjectInformation.this.accessors.put(it.getName(), method));
}, PublicMethodAndFieldFilter.NON_STATIC);
ReflectionUtils.doWithFields(type, RootObjectInformation.this.fields::add, PublicMethodAndFieldFilter.NON_STATIC);
}
public MultiValueMap<String, Function> getFunctions(Optional<Object> target) {
return target.map(this::getFunctions).orElseGet(() -> new LinkedMultiValueMap<>());
}
private MultiValueMap<String, Function> getFunctions(Object target) {
return methods.stream().collect(toMultiMap(Method::getName, m -> new Function(m, target)));
}
public Map<String, Object> getProperties(Optional<Object> target) {
return target.map(it -> {
Map<String, Object> properties = new HashMap<>();
accessors.entrySet().stream()
.forEach(method -> properties.put(method.getKey(), new Function(method.getValue(), it)));
fields.stream().forEach(field -> properties.put(field.getName(), ReflectionUtils.getField(field, it)));
return Collections.unmodifiableMap(properties);
}).orElseGet(Collections::emptyMap);
}
}
private static Map<String, Object> discoverDeclaredProperties(Class<?> type) {
Map<String, Object> map = new HashMap<>();
ReflectionUtils.doWithFields(type, field -> map.put(field.getName(), field.get(null)),
PublicMethodAndFieldFilter.STATIC);
return map.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(map);
}
}