package org.springframework.data.spel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.spel.EvaluationContextExtensionInformation.ExtensionTypeInformation;
import org.springframework.data.spel.EvaluationContextExtensionInformation.RootObjectInformation;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.Function;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Optionals;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@RequiredArgsConstructor
public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider {
private final Map<Class<?>, EvaluationContextExtensionInformation> extensionInformationCache = new ConcurrentHashMap<>();
private final Lazy<? extends Collection<? extends EvaluationContextExtension>> extensions;
private ListableBeanFactory beanFactory;
ExtensionAwareEvaluationContextProvider() {
this(Collections.emptyList());
}
public ExtensionAwareEvaluationContextProvider(ListableBeanFactory beanFactory) {
this(Lazy.of(() -> getExtensionsFrom(beanFactory)));
this.beanFactory = beanFactory;
}
public ExtensionAwareEvaluationContextProvider(Collection<? extends EvaluationContextExtension> extensions) {
this(Lazy.of(extensions));
}
@Override
public StandardEvaluationContext getEvaluationContext(Object rootObject) {
StandardEvaluationContext context = new StandardEvaluationContext();
if (beanFactory != null) {
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
ExtensionAwarePropertyAccessor accessor = new ExtensionAwarePropertyAccessor(extensions.get());
context.addPropertyAccessor(accessor);
context.addPropertyAccessor(new ReflectivePropertyAccessor());
context.addMethodResolver(accessor);
if (rootObject != null) {
context.setRootObject(rootObject);
}
return context;
}
private static Collection<? extends EvaluationContextExtension> getExtensionsFrom(ListableBeanFactory beanFactory) {
return beanFactory.getBeansOfType(EvaluationContextExtension.class, true, false).values();
}
private EvaluationContextExtensionInformation getOrCreateInformation(EvaluationContextExtension extension) {
Class<? extends EvaluationContextExtension> extensionType = extension.getClass();
return extensionInformationCache.computeIfAbsent(extensionType,
type -> new EvaluationContextExtensionInformation(extensionType));
}
private List<EvaluationContextExtensionAdapter> toAdapters(
Collection<? extends EvaluationContextExtension> extensions) {
return extensions.stream()
.sorted(AnnotationAwareOrderComparator.INSTANCE)
.map(it -> new EvaluationContextExtensionAdapter(it, getOrCreateInformation(it)))
.collect(Collectors.toList());
}
private class ExtensionAwarePropertyAccessor implements PropertyAccessor, MethodResolver {
private final List<EvaluationContextExtensionAdapter> adapters;
private final Map<String, EvaluationContextExtensionAdapter> adapterMap;
public ExtensionAwarePropertyAccessor(Collection<? extends EvaluationContextExtension> extensions) {
Assert.notNull(extensions, "Extensions must not be null!");
this.adapters = toAdapters(extensions);
this.adapterMap = adapters.stream()
.collect(Collectors.toMap(EvaluationContextExtensionAdapter::getExtensionId, it -> it));
Collections.reverse(this.adapters);
}
@Override
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) {
if (target instanceof EvaluationContextExtension) {
return true;
}
if (adapterMap.containsKey(name)) {
return true;
}
return adapters.stream().anyMatch(it -> it.getProperties().containsKey(name));
}
@Override
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) {
if (target instanceof EvaluationContextExtensionAdapter) {
return lookupPropertyFrom((EvaluationContextExtensionAdapter) target, name);
}
if (adapterMap.containsKey(name)) {
return new TypedValue(adapterMap.get(name));
}
return adapters.stream()
.filter(it -> it.getProperties().containsKey(name))
.map(it -> lookupPropertyFrom(it, name))
.findFirst().orElse(TypedValue.NULL);
}
@Nullable
@Override
public MethodExecutor resolve(EvaluationContext context, @Nullable Object target, final String name,
List<TypeDescriptor> argumentTypes) {
if (target instanceof EvaluationContextExtensionAdapter) {
return getMethodExecutor((EvaluationContextExtensionAdapter) target, name, argumentTypes).orElse(null);
}
return adapters.stream()
.flatMap(it -> Optionals.toStream(getMethodExecutor(it, name, argumentTypes)))
.findFirst().orElse(null);
}
@Override
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) {
return false;
}
@Override
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) {
}
@Nullable
@Override
public Class<?>[] getSpecificTargetClasses() {
return null;
}
private Optional<MethodExecutor> getMethodExecutor(EvaluationContextExtensionAdapter adapter, String name,
List<TypeDescriptor> argumentTypes) {
return adapter.getFunctions().get(name, argumentTypes).map(FunctionMethodExecutor::new);
}
private TypedValue lookupPropertyFrom(EvaluationContextExtensionAdapter extension, String name) {
Object value = extension.getProperties().get(name);
if (!(value instanceof Function)) {
return new TypedValue(value);
}
Function function = (Function) value;
try {
return new TypedValue(function.invoke(new Object[0]));
} catch (Exception e) {
throw new SpelEvaluationException(e, SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, name,
function.getDeclaringClass());
}
}
}
@RequiredArgsConstructor
private static class FunctionMethodExecutor implements MethodExecutor {
private final @NonNull Function function;
@Override
public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException {
try {
return new TypedValue(function.invoke(arguments));
} catch (Exception e) {
throw new SpelEvaluationException(e, SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, function.getName(),
function.getDeclaringClass());
}
}
}
private static class EvaluationContextExtensionAdapter {
private final EvaluationContextExtension extension;
private final Functions functions = new Functions();
private final Map<String, Object> properties;
public EvaluationContextExtensionAdapter(EvaluationContextExtension extension,
EvaluationContextExtensionInformation information) {
Assert.notNull(extension, "Extension must not be null!");
Assert.notNull(information, "Extension information must not be null!");
Optional<Object> target = Optional.ofNullable(extension.getRootObject());
ExtensionTypeInformation extensionTypeInformation = information.getExtensionTypeInformation();
RootObjectInformation rootObjectInformation = information.getRootObjectInformation(target);
functions.addAll(extension.getFunctions());
functions.addAll(rootObjectInformation.getFunctions(target));
functions.addAll(extensionTypeInformation.getFunctions());
this.properties = new HashMap<>();
this.properties.putAll(extensionTypeInformation.getProperties());
this.properties.putAll(rootObjectInformation.getProperties(target));
this.properties.putAll(extension.getProperties());
this.extension = extension;
}
String getExtensionId() {
return extension.getExtensionId();
}
Functions getFunctions() {
return this.functions;
}
public Map<String, Object> getProperties() {
return this.properties;
}
}
}