/* * Copyright 2014-2017 the original author or authors. * * 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 org.springframework.data.repository.query; import lombok.NonNull; import lombok.RequiredArgsConstructor; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.stream.Collectors; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.repository.query.EvaluationContextExtensionInformation.ExtensionTypeInformation; import org.springframework.data.repository.query.EvaluationContextExtensionInformation.RootObjectInformation; import org.springframework.data.repository.query.spi.EvaluationContextExtension; import org.springframework.data.repository.query.spi.Function; 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.util.Assert; import org.springframework.util.StringUtils; /** * An {@link EvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of * {@link EvaluationContextExtension} instances. * * @author Thomas Darimont * @author Oliver Gierke * @author Christoph Strobl * @since 1.9 */ public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider, ApplicationContextAware { private final Map<Class<?>, EvaluationContextExtensionInformation> extensionInformationCache = new HashMap<>(); private List<? extends EvaluationContextExtension> extensions; private Optional<ListableBeanFactory> beanFactory = Optional.empty(); /** * Creates a new {@link ExtensionAwareEvaluationContextProvider}. Extensions are being looked up lazily from the * {@link BeanFactory} configured. */ public ExtensionAwareEvaluationContextProvider() { this.extensions = null; } /** * Creates a new {@link ExtensionAwareEvaluationContextProvider} for the given {@link EvaluationContextExtension}s. * * @param adapters must not be {@literal null}. */ public ExtensionAwareEvaluationContextProvider(List<? extends EvaluationContextExtension> extensions) { Assert.notNull(extensions, "List of EvaluationContextExtensions must not be null!"); this.extensions = extensions; } /* * (non-Javadoc) * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.beanFactory = Optional.of(applicationContext); } /* (non-Javadoc) * @see org.springframework.data.jpa.repository.support.EvaluationContextProvider#getEvaluationContext() */ @Override public <T extends Parameters<?, ?>> StandardEvaluationContext getEvaluationContext(T parameters, Object[] parameterValues) { StandardEvaluationContext ec = new StandardEvaluationContext(); beanFactory.ifPresent(it -> ec.setBeanResolver(new BeanFactoryResolver(it))); ExtensionAwarePropertyAccessor accessor = new ExtensionAwarePropertyAccessor(getExtensions()); ec.addPropertyAccessor(accessor); ec.addPropertyAccessor(new ReflectivePropertyAccessor()); ec.addMethodResolver(accessor); // Add parameters for indexed access ec.setRootObject(parameterValues); ec.setVariables(collectVariables(parameters, parameterValues)); return ec; } /** * Exposes variables for all named parameters for the given arguments. Also exposes non-bindable parameters under the * names of their types. * * @param parameters must not be {@literal null}. * @param arguments must not be {@literal null}. * @return */ private <T extends Parameters<?, ?>> Map<String, Object> collectVariables(T parameters, Object[] arguments) { Map<String, Object> variables = new HashMap<>(); parameters.stream()// .filter(Parameter::isSpecialParameter)// .forEach(it -> variables.put(// StringUtils.uncapitalize(it.getType().getSimpleName()), // arguments[it.getIndex()])); parameters.stream()// .filter(Parameter::isNamedParameter)// .forEach(it -> variables.put(// it.getName().orElseThrow(() -> new IllegalStateException("Should never occur!")), // arguments[it.getIndex()])); return variables; } /** * Returns the {@link EvaluationContextExtension} to be used. Either from the current configuration or the configured * {@link BeanFactory}. * * @return */ private List<? extends EvaluationContextExtension> getExtensions() { if (this.extensions != null) { return this.extensions; } this.extensions = Collections.emptyList(); beanFactory.ifPresent(it -> this.extensions = new ArrayList<>( it.getBeansOfType(EvaluationContextExtension.class, true, false).values())); return extensions; } /** * Looks up the {@link EvaluationContextExtensionInformation} for the given {@link EvaluationContextExtension} from * the cache or creates a new one and caches that for later lookup. * * @param extension must not be {@literal null}. * @return */ private EvaluationContextExtensionInformation getOrCreateInformation(EvaluationContextExtension extension) { Class<? extends EvaluationContextExtension> extensionType = extension.getClass(); return extensionInformationCache.computeIfAbsent(extensionType, type -> new EvaluationContextExtensionInformation(extensionType)); } /** * Creates {@link EvaluationContextExtensionAdapter}s for the given {@link EvaluationContextExtension}s. * * @param extensions * @return */ private List<EvaluationContextExtensionAdapter> toAdapters(List<? extends EvaluationContextExtension> extensions) { return extensions.stream()// .sorted(AnnotationAwareOrderComparator.INSTANCE)// .map(it -> new EvaluationContextExtensionAdapter(it, getOrCreateInformation(it)))// .collect(Collectors.toList()); } /** * @author Thomas Darimont * @author Oliver Gierke * @see 1.9 */ private class ExtensionAwarePropertyAccessor implements PropertyAccessor, MethodResolver { private final List<EvaluationContextExtensionAdapter> adapters; private final Map<String, EvaluationContextExtensionAdapter> adapterMap; /** * Creates a new {@link ExtensionAwarePropertyAccessor} for the given {@link EvaluationContextExtension}s. * * @param adapters must not be {@literal null}. */ public ExtensionAwarePropertyAccessor(List<? 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); } /* * (non-Javadoc) * @see org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider.ReadOnlyPropertyAccessor#canRead(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) */ @Override public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { if (target instanceof EvaluationContextExtension) { return true; } if (adapterMap.containsKey(name)) { return true; } return adapters.stream().anyMatch(it -> it.getProperties().containsKey(name)); } /* * (non-Javadoc) * @see org.springframework.expression.PropertyAccessor#read(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) */ @Override public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { 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(null); } /* * (non-Javadoc) * @see org.springframework.expression.MethodResolver#resolve(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.util.List) */ @Override public MethodExecutor resolve(EvaluationContext context, Object target, final String name, List<TypeDescriptor> argumentTypes) throws AccessException { 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); } /* * (non-Javadoc) * @see org.springframework.expression.PropertyAccessor#canWrite(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String) */ @Override public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { return false; } /* * (non-Javadoc) * @see org.springframework.expression.PropertyAccessor#write(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.lang.Object) */ @Override public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { // noop } /* * (non-Javadoc) * @see org.springframework.expression.PropertyAccessor#getSpecificTargetClasses() */ @Override public Class<?>[] getSpecificTargetClasses() { return null; } /** * Returns a {@link MethodExecutor} * * @param adapter * @param name * @param argumentTypes * @return */ private Optional<MethodExecutor> getMethodExecutor(EvaluationContextExtensionAdapter adapter, String name, List<TypeDescriptor> argumentTypes) { return adapter.getFunctions().entrySet().stream()// .filter(entry -> entry.getKey().equals(name))// .findFirst().map(Entry::getValue).map(FunctionMethodExecutor::new); } /** * Looks up the property value for the property of the given name from the given extension. Takes care of resolving * {@link Function} values transitively. * * @param extension must not be {@literal null}. * @param name must not be {@literal null} or empty. * @return */ 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()); } } } /** * {@link MethodExecutor} to invoke {@link Function} instances. * * @author Oliver Gierke * @since 1.9 */ @RequiredArgsConstructor private static class FunctionMethodExecutor implements MethodExecutor { private final @NonNull Function function; /* * (non-Javadoc) * @see org.springframework.expression.MethodExecutor#execute(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.Object[]) */ @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()); } } } /** * Adapter to expose a unified view on {@link EvaluationContextExtension} based on some reflective inspection of the * extension (see {@link EvaluationContextExtensionInformation}) as well as the values exposed by the extension * itself. * * @author Oliver Gierke * @since 1.9 */ private static class EvaluationContextExtensionAdapter { private final EvaluationContextExtension extension; private final Map<String, Function> functions; private final Map<String, Object> properties; /** * Creates a new {@link EvaluationContextExtensionAdapter} for the given {@link EvaluationContextExtension} and * {@link EvaluationContextExtensionInformation}. * * @param extension must not be {@literal null}. * @param information must not be {@literal null}. */ public EvaluationContextExtensionAdapter(EvaluationContextExtension extension, EvaluationContextExtensionInformation information) { Assert.notNull(extension, "Extenstion 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); this.functions = new HashMap<>(); this.functions.putAll(extensionTypeInformation.getFunctions()); this.functions.putAll(rootObjectInformation.getFunctions(target)); this.functions.putAll(extension.getFunctions()); this.properties = new HashMap<>(); this.properties.putAll(extensionTypeInformation.getProperties()); this.properties.putAll(rootObjectInformation.getProperties(target)); this.properties.putAll(extension.getProperties()); this.extension = extension; } /** * Returns the extension identifier. * * @return */ public String getExtensionId() { return extension.getExtensionId(); } /** * Returns all functions exposed. * * @return */ public Map<String, Function> getFunctions() { return this.functions; } /** * Returns all properties exposed. Note, the value of a property can be a {@link Function} in turn * * @return */ public Map<String, Object> getProperties() { return this.properties; } } }