1 /*
2  * Copyright 2014-2020 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.data.spel;
17
18 import lombok.NonNull;
19 import lombok.RequiredArgsConstructor;
20
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.stream.Collectors;
29
30 import org.springframework.beans.factory.BeanFactory;
31 import org.springframework.beans.factory.ListableBeanFactory;
32 import org.springframework.context.expression.BeanFactoryResolver;
33 import org.springframework.core.annotation.AnnotationAwareOrderComparator;
34 import org.springframework.core.convert.TypeDescriptor;
35 import org.springframework.data.spel.EvaluationContextExtensionInformation.ExtensionTypeInformation;
36 import org.springframework.data.spel.EvaluationContextExtensionInformation.RootObjectInformation;
37 import org.springframework.data.spel.spi.EvaluationContextExtension;
38 import org.springframework.data.spel.spi.Function;
39 import org.springframework.data.util.Lazy;
40 import org.springframework.data.util.Optionals;
41 import org.springframework.expression.AccessException;
42 import org.springframework.expression.EvaluationContext;
43 import org.springframework.expression.MethodExecutor;
44 import org.springframework.expression.MethodResolver;
45 import org.springframework.expression.PropertyAccessor;
46 import org.springframework.expression.TypedValue;
47 import org.springframework.expression.spel.SpelEvaluationException;
48 import org.springframework.expression.spel.SpelMessage;
49 import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
50 import org.springframework.expression.spel.support.StandardEvaluationContext;
51 import org.springframework.lang.Nullable;
52 import org.springframework.util.Assert;
53
54 /**
55  * An {@link EvaluationContextProvider} that assembles an {@link EvaluationContext} from a list of
56  * {@link EvaluationContextExtension} instances.
57  *
58  * @author Thomas Darimont
59  * @author Oliver Gierke
60  * @author Christoph Strobl
61  * @author Jens Schauder
62  * @since 2.1
63  */

64 @RequiredArgsConstructor
65 public class ExtensionAwareEvaluationContextProvider implements EvaluationContextProvider {
66
67     private final Map<Class<?>, EvaluationContextExtensionInformation> extensionInformationCache = new ConcurrentHashMap<>();
68
69     private final Lazy<? extends Collection<? extends EvaluationContextExtension>> extensions;
70     private ListableBeanFactory beanFactory;
71
72     ExtensionAwareEvaluationContextProvider() {
73         this(Collections.emptyList());
74     }
75
76     /**
77      * Creates a new {@link ExtensionAwareEvaluationContextProvider} with extensions looked up lazily from the given
78      * {@link BeanFactory}.
79      *
80      * @param beanFactory the {@link ListableBeanFactory} to lookup extensions from.
81      */

82     public ExtensionAwareEvaluationContextProvider(ListableBeanFactory beanFactory) {
83
84         this(Lazy.of(() -> getExtensionsFrom(beanFactory)));
85
86         this.beanFactory = beanFactory;
87     }
88
89     /**
90      * Creates a new {@link ExtensionAwareEvaluationContextProvider} for the given {@link EvaluationContextExtension}s.
91      *
92      * @param extensions must not be {@literal null}.
93      */

94     public ExtensionAwareEvaluationContextProvider(Collection<? extends EvaluationContextExtension> extensions) {
95         this(Lazy.of(extensions));
96     }
97
98     /* (non-Javadoc)
99      * @see org.springframework.data.jpa.repository.support.EvaluationContextProvider#getEvaluationContext()
100      */

101     @Override
102     public StandardEvaluationContext getEvaluationContext(Object rootObject) {
103
104         StandardEvaluationContext context = new StandardEvaluationContext();
105
106         if (beanFactory != null) {
107             context.setBeanResolver(new BeanFactoryResolver(beanFactory));
108         }
109
110         ExtensionAwarePropertyAccessor accessor = new ExtensionAwarePropertyAccessor(extensions.get());
111
112         context.addPropertyAccessor(accessor);
113         context.addPropertyAccessor(new ReflectivePropertyAccessor());
114         context.addMethodResolver(accessor);
115
116         if (rootObject != null) {
117             context.setRootObject(rootObject);
118         }
119
120         return context;
121     }
122
123     /**
124      * Looks up all {@link EvaluationContextExtension} instances from the given {@link ListableBeanFactory}.
125      *
126      * @param beanFactory must not be {@literal null}.
127      * @return
128      */

129     private static Collection<? extends EvaluationContextExtension> getExtensionsFrom(ListableBeanFactory beanFactory) {
130         return beanFactory.getBeansOfType(EvaluationContextExtension.classtruefalse).values();
131     }
132
133     /**
134      * Looks up the {@link EvaluationContextExtensionInformation} for the given {@link EvaluationContextExtension} from
135      * the cache or creates a new one and caches that for later lookup.
136      *
137      * @param extension must not be {@literal null}.
138      * @return
139      */

140     private EvaluationContextExtensionInformation getOrCreateInformation(EvaluationContextExtension extension) {
141
142         Class<? extends EvaluationContextExtension> extensionType = extension.getClass();
143
144         return extensionInformationCache.computeIfAbsent(extensionType,
145                 type -> new EvaluationContextExtensionInformation(extensionType));
146     }
147
148     /**
149      * Creates {@link EvaluationContextExtensionAdapter}s for the given {@link EvaluationContextExtension}s.
150      *
151      * @param extensions
152      * @return
153      */

154     private List<EvaluationContextExtensionAdapter> toAdapters(
155             Collection<? extends EvaluationContextExtension> extensions) {
156
157         return extensions.stream()//
158                 .sorted(AnnotationAwareOrderComparator.INSTANCE)//
159                 .map(it -> new EvaluationContextExtensionAdapter(it, getOrCreateInformation(it)))//
160                 .collect(Collectors.toList());
161     }
162
163     /**
164      * @author Thomas Darimont
165      * @author Oliver Gierke
166      * @see 1.9
167      */

168     private class ExtensionAwarePropertyAccessor implements PropertyAccessor, MethodResolver {
169
170         private final List<EvaluationContextExtensionAdapter> adapters;
171         private final Map<String, EvaluationContextExtensionAdapter> adapterMap;
172
173         /**
174          * Creates a new {@link ExtensionAwarePropertyAccessor} for the given {@link EvaluationContextExtension}s.
175          *
176          * @param extensions must not be {@literal null}.
177          */

178         public ExtensionAwarePropertyAccessor(Collection<? extends EvaluationContextExtension> extensions) {
179
180             Assert.notNull(extensions, "Extensions must not be null!");
181
182             this.adapters = toAdapters(extensions);
183             this.adapterMap = adapters.stream()//
184                     .collect(Collectors.toMap(EvaluationContextExtensionAdapter::getExtensionId, it -> it));
185
186             Collections.reverse(this.adapters);
187         }
188
189         /*
190          * (non-Javadoc)
191          * @see org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider.ReadOnlyPropertyAccessor#canRead(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
192          */

193         @Override
194         public boolean canRead(EvaluationContext context, @Nullable Object target, String name) {
195
196             if (target instanceof EvaluationContextExtension) {
197                 return true;
198             }
199
200             if (adapterMap.containsKey(name)) {
201                 return true;
202             }
203
204             return adapters.stream().anyMatch(it -> it.getProperties().containsKey(name));
205         }
206
207         /*
208          * (non-Javadoc)
209          * @see org.springframework.expression.PropertyAccessor#read(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
210          */

211         @Override
212         public TypedValue read(EvaluationContext context, @Nullable Object target, String name) {
213
214             if (target instanceof EvaluationContextExtensionAdapter) {
215                 return lookupPropertyFrom((EvaluationContextExtensionAdapter) target, name);
216             }
217
218             if (adapterMap.containsKey(name)) {
219                 return new TypedValue(adapterMap.get(name));
220             }
221
222             return adapters.stream()//
223                     .filter(it -> it.getProperties().containsKey(name))//
224                     .map(it -> lookupPropertyFrom(it, name))//
225                     .findFirst().orElse(TypedValue.NULL);
226         }
227
228         /*
229          * (non-Javadoc)
230          * @see org.springframework.expression.MethodResolver#resolve(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.util.List)
231          */

232         @Nullable
233         @Override
234         public MethodExecutor resolve(EvaluationContext context, @Nullable Object target, final String name,
235                 List<TypeDescriptor> argumentTypes) {
236
237             if (target instanceof EvaluationContextExtensionAdapter) {
238                 return getMethodExecutor((EvaluationContextExtensionAdapter) target, name, argumentTypes).orElse(null);
239             }
240
241             return adapters.stream()//
242                     .flatMap(it -> Optionals.toStream(getMethodExecutor(it, name, argumentTypes)))//
243                     .findFirst().orElse(null);
244         }
245
246         /*
247          * (non-Javadoc)
248          * @see org.springframework.expression.PropertyAccessor#canWrite(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
249          */

250         @Override
251         public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) {
252             return false;
253         }
254
255         /*
256          * (non-Javadoc)
257          * @see org.springframework.expression.PropertyAccessor#write(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String, java.lang.Object)
258          */

259         @Override
260         public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) {
261             // noop
262         }
263
264         /*
265          * (non-Javadoc)
266          * @see org.springframework.expression.PropertyAccessor#getSpecificTargetClasses()
267          */

268         @Nullable
269         @Override
270         public Class<?>[] getSpecificTargetClasses() {
271             return null;
272         }
273
274         /**
275          * Returns a {@link MethodExecutor} wrapping a function from the adapter passed in as an argument.
276          *
277          * @param adapter the source of functions to consider.
278          * @param name the name of the function
279          * @param argumentTypes the types of the arguments that the function must accept.
280          * @return a matching {@link MethodExecutor}
281          */

282         private Optional<MethodExecutor> getMethodExecutor(EvaluationContextExtensionAdapter adapter, String name,
283                 List<TypeDescriptor> argumentTypes) {
284             return adapter.getFunctions().get(name, argumentTypes).map(FunctionMethodExecutor::new);
285         }
286
287         /**
288          * Looks up the property value for the property of the given name from the given extension. Takes care of resolving
289          * {@link Function} values transitively.
290          *
291          * @param extension must not be {@literal null}.
292          * @param name must not be {@literal null} or empty.
293          * @return a {@link TypedValue} matching the given parameters.
294          */

295         private TypedValue lookupPropertyFrom(EvaluationContextExtensionAdapter extension, String name) {
296
297             Object value = extension.getProperties().get(name);
298
299             if (!(value instanceof Function)) {
300                 return new TypedValue(value);
301             }
302
303             Function function = (Function) value;
304
305             try {
306                 return new TypedValue(function.invoke(new Object[0]));
307             } catch (Exception e) {
308                 throw new SpelEvaluationException(e, SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, name,
309                         function.getDeclaringClass());
310             }
311         }
312     }
313
314     /**
315      * {@link MethodExecutor} to invoke {@link Function} instances.
316      *
317      * @author Oliver Gierke
318      * @since 1.9
319      */

320     @RequiredArgsConstructor
321     private static class FunctionMethodExecutor implements MethodExecutor {
322
323         private final @NonNull Function function;
324
325         /*
326          * (non-Javadoc)
327          * @see org.springframework.expression.MethodExecutor#execute(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.Object[])
328          */

329         @Override
330         public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException {
331
332             try {
333                 return new TypedValue(function.invoke(arguments));
334             } catch (Exception e) {
335                 throw new SpelEvaluationException(e, SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, function.getName(),
336                         function.getDeclaringClass());
337             }
338         }
339     }
340
341     /**
342      * Adapter to expose a unified view on {@link EvaluationContextExtension} based on some reflective inspection of the
343      * extension (see {@link EvaluationContextExtensionInformation}) as well as the values exposed by the extension
344      * itself.
345      *
346      * @author Oliver Gierke
347      * @since 1.9
348      */

349     private static class EvaluationContextExtensionAdapter {
350
351         private final EvaluationContextExtension extension;
352
353         private final Functions functions = new Functions();
354         private final Map<String, Object> properties;
355
356         /**
357          * Creates a new {@link EvaluationContextExtensionAdapter} for the given {@link EvaluationContextExtension} and
358          * {@link EvaluationContextExtensionInformation}.
359          *
360          * @param extension must not be {@literal null}.
361          * @param information must not be {@literal null}.
362          */

363         public EvaluationContextExtensionAdapter(EvaluationContextExtension extension,
364                 EvaluationContextExtensionInformation information) {
365
366             Assert.notNull(extension, "Extension must not be null!");
367             Assert.notNull(information, "Extension information must not be null!");
368
369             Optional<Object> target = Optional.ofNullable(extension.getRootObject());
370             ExtensionTypeInformation extensionTypeInformation = information.getExtensionTypeInformation();
371             RootObjectInformation rootObjectInformation = information.getRootObjectInformation(target);
372
373             functions.addAll(extension.getFunctions());
374             functions.addAll(rootObjectInformation.getFunctions(target));
375             functions.addAll(extensionTypeInformation.getFunctions());
376
377             this.properties = new HashMap<>();
378             this.properties.putAll(extensionTypeInformation.getProperties());
379             this.properties.putAll(rootObjectInformation.getProperties(target));
380             this.properties.putAll(extension.getProperties());
381
382             this.extension = extension;
383         }
384
385         /**
386          * Returns the extension identifier.
387          *
388          * @return the id of the extension
389          */

390         String getExtensionId() {
391             return extension.getExtensionId();
392         }
393
394         /**
395          * Returns all functions exposed.
396          *
397          * @return all exposed functions.
398          */

399         Functions getFunctions() {
400             return this.functions;
401         }
402
403         /**
404          * Returns all properties exposed. Note, the value of a property can be a {@link Function} in turn
405          *
406          * @return a map from property name to property value.
407          */

408         public Map<String, Object> getProperties() {
409             return this.properties;
410         }
411     }
412 }
413