001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.expression.spel.ast;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.lang.reflect.Modifier;
022import java.lang.reflect.Proxy;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.List;
026import java.util.StringJoiner;
027
028import org.springframework.asm.Label;
029import org.springframework.asm.MethodVisitor;
030import org.springframework.core.convert.TypeDescriptor;
031import org.springframework.expression.AccessException;
032import org.springframework.expression.EvaluationContext;
033import org.springframework.expression.EvaluationException;
034import org.springframework.expression.ExpressionInvocationTargetException;
035import org.springframework.expression.MethodExecutor;
036import org.springframework.expression.MethodResolver;
037import org.springframework.expression.TypedValue;
038import org.springframework.expression.spel.CodeFlow;
039import org.springframework.expression.spel.ExpressionState;
040import org.springframework.expression.spel.SpelEvaluationException;
041import org.springframework.expression.spel.SpelMessage;
042import org.springframework.expression.spel.support.ReflectiveMethodExecutor;
043import org.springframework.expression.spel.support.ReflectiveMethodResolver;
044import org.springframework.lang.Nullable;
045import org.springframework.util.Assert;
046import org.springframework.util.ObjectUtils;
047
048/**
049 * Expression language AST node that represents a method reference.
050 *
051 * @author Andy Clement
052 * @author Juergen Hoeller
053 * @since 3.0
054 */
055public class MethodReference extends SpelNodeImpl {
056
057        private final String name;
058
059        private final boolean nullSafe;
060
061        @Nullable
062        private String originalPrimitiveExitTypeDescriptor;
063
064        @Nullable
065        private volatile CachedMethodExecutor cachedExecutor;
066
067
068        public MethodReference(boolean nullSafe, String methodName, int startPos, int endPos, SpelNodeImpl... arguments) {
069                super(startPos, endPos, arguments);
070                this.name = methodName;
071                this.nullSafe = nullSafe;
072        }
073
074
075        public final String getName() {
076                return this.name;
077        }
078
079        @Override
080        protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
081                Object[] arguments = getArguments(state);
082                if (state.getActiveContextObject().getValue() == null) {
083                        throwIfNotNullSafe(getArgumentTypes(arguments));
084                        return ValueRef.NullValueRef.INSTANCE;
085                }
086                return new MethodValueRef(state, arguments);
087        }
088
089        @Override
090        public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
091                EvaluationContext evaluationContext = state.getEvaluationContext();
092                Object value = state.getActiveContextObject().getValue();
093                TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor();
094                Object[] arguments = getArguments(state);
095                TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments);
096                updateExitTypeDescriptor();
097                return result;
098        }
099
100        private TypedValue getValueInternal(EvaluationContext evaluationContext,
101                        @Nullable Object value, @Nullable TypeDescriptor targetType, Object[] arguments) {
102
103                List<TypeDescriptor> argumentTypes = getArgumentTypes(arguments);
104                if (value == null) {
105                        throwIfNotNullSafe(argumentTypes);
106                        return TypedValue.NULL;
107                }
108
109                MethodExecutor executorToUse = getCachedExecutor(evaluationContext, value, targetType, argumentTypes);
110                if (executorToUse != null) {
111                        try {
112                                return executorToUse.execute(evaluationContext, value, arguments);
113                        }
114                        catch (AccessException ex) {
115                                // Two reasons this can occur:
116                                // 1. the method invoked actually threw a real exception
117                                // 2. the method invoked was not passed the arguments it expected and
118                                //    has become 'stale'
119
120                                // In the first case we should not retry, in the second case we should see
121                                // if there is a better suited method.
122
123                                // To determine the situation, the AccessException will contain a cause.
124                                // If the cause is an InvocationTargetException, a user exception was
125                                // thrown inside the method. Otherwise the method could not be invoked.
126                                throwSimpleExceptionIfPossible(value, ex);
127
128                                // At this point we know it wasn't a user problem so worth a retry if a
129                                // better candidate can be found.
130                                this.cachedExecutor = null;
131                        }
132                }
133
134                // either there was no accessor or it no longer existed
135                executorToUse = findAccessorForMethod(argumentTypes, value, evaluationContext);
136                this.cachedExecutor = new CachedMethodExecutor(
137                                executorToUse, (value instanceof Class ? (Class<?>) value : null), targetType, argumentTypes);
138                try {
139                        return executorToUse.execute(evaluationContext, value, arguments);
140                }
141                catch (AccessException ex) {
142                        // Same unwrapping exception handling as above in above catch block
143                        throwSimpleExceptionIfPossible(value, ex);
144                        throw new SpelEvaluationException(getStartPosition(), ex,
145                                        SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, this.name,
146                                        value.getClass().getName(), ex.getMessage());
147                }
148        }
149
150        private void throwIfNotNullSafe(List<TypeDescriptor> argumentTypes) {
151                if (!this.nullSafe) {
152                        throw new SpelEvaluationException(getStartPosition(),
153                                        SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED,
154                                        FormatHelper.formatMethodForMessage(this.name, argumentTypes));
155                }
156        }
157
158        private Object[] getArguments(ExpressionState state) {
159                Object[] arguments = new Object[getChildCount()];
160                for (int i = 0; i < arguments.length; i++) {
161                        // Make the root object the active context again for evaluating the parameter expressions
162                        try {
163                                state.pushActiveContextObject(state.getScopeRootContextObject());
164                                arguments[i] = this.children[i].getValueInternal(state).getValue();
165                        }
166                        finally {
167                                state.popActiveContextObject();
168                        }
169                }
170                return arguments;
171        }
172
173        private List<TypeDescriptor> getArgumentTypes(Object... arguments) {
174                List<TypeDescriptor> descriptors = new ArrayList<>(arguments.length);
175                for (Object argument : arguments) {
176                        descriptors.add(TypeDescriptor.forObject(argument));
177                }
178                return Collections.unmodifiableList(descriptors);
179        }
180
181        @Nullable
182        private MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object value,
183                        @Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
184
185                List<MethodResolver> methodResolvers = evaluationContext.getMethodResolvers();
186                if (methodResolvers.size() != 1 || !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) {
187                        // Not a default ReflectiveMethodResolver - don't know whether caching is valid
188                        return null;
189                }
190
191                CachedMethodExecutor executorToCheck = this.cachedExecutor;
192                if (executorToCheck != null && executorToCheck.isSuitable(value, target, argumentTypes)) {
193                        return executorToCheck.get();
194                }
195                this.cachedExecutor = null;
196                return null;
197        }
198
199        private MethodExecutor findAccessorForMethod(List<TypeDescriptor> argumentTypes, Object targetObject,
200                        EvaluationContext evaluationContext) throws SpelEvaluationException {
201
202                AccessException accessException = null;
203                List<MethodResolver> methodResolvers = evaluationContext.getMethodResolvers();
204                for (MethodResolver methodResolver : methodResolvers) {
205                        try {
206                                MethodExecutor methodExecutor = methodResolver.resolve(
207                                                evaluationContext, targetObject, this.name, argumentTypes);
208                                if (methodExecutor != null) {
209                                        return methodExecutor;
210                                }
211                        }
212                        catch (AccessException ex) {
213                                accessException = ex;
214                                break;
215                        }
216                }
217
218                String method = FormatHelper.formatMethodForMessage(this.name, argumentTypes);
219                String className = FormatHelper.formatClassNameForMessage(
220                                targetObject instanceof Class ? ((Class<?>) targetObject) : targetObject.getClass());
221                if (accessException != null) {
222                        throw new SpelEvaluationException(
223                                        getStartPosition(), accessException, SpelMessage.PROBLEM_LOCATING_METHOD, method, className);
224                }
225                else {
226                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.METHOD_NOT_FOUND, method, className);
227                }
228        }
229
230        /**
231         * Decode the AccessException, throwing a lightweight evaluation exception or,
232         * if the cause was a RuntimeException, throw the RuntimeException directly.
233         */
234        private void throwSimpleExceptionIfPossible(Object value, AccessException ex) {
235                if (ex.getCause() instanceof InvocationTargetException) {
236                        Throwable rootCause = ex.getCause().getCause();
237                        if (rootCause instanceof RuntimeException) {
238                                throw (RuntimeException) rootCause;
239                        }
240                        throw new ExpressionInvocationTargetException(getStartPosition(),
241                                        "A problem occurred when trying to execute method '" + this.name +
242                                        "' on object of type [" + value.getClass().getName() + "]", rootCause);
243                }
244        }
245
246        private void updateExitTypeDescriptor() {
247                CachedMethodExecutor executorToCheck = this.cachedExecutor;
248                if (executorToCheck != null && executorToCheck.get() instanceof ReflectiveMethodExecutor) {
249                        Method method = ((ReflectiveMethodExecutor) executorToCheck.get()).getMethod();
250                        String descriptor = CodeFlow.toDescriptor(method.getReturnType());
251                        if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) {
252                                this.originalPrimitiveExitTypeDescriptor = descriptor;
253                                this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor);
254                        }
255                        else {
256                                this.exitTypeDescriptor = descriptor;
257                        }
258                }
259        }
260
261        @Override
262        public String toStringAST() {
263                StringJoiner sj = new StringJoiner(",", "(", ")");
264                for (int i = 0; i < getChildCount(); i++) {
265                        sj.add(getChild(i).toStringAST());
266                }
267                return this.name + sj.toString();
268        }
269
270        /**
271         * A method reference is compilable if it has been resolved to a reflectively accessible method
272         * and the child nodes (arguments to the method) are also compilable.
273         */
274        @Override
275        public boolean isCompilable() {
276                CachedMethodExecutor executorToCheck = this.cachedExecutor;
277                if (executorToCheck == null || executorToCheck.hasProxyTarget() ||
278                                !(executorToCheck.get() instanceof ReflectiveMethodExecutor)) {
279                        return false;
280                }
281
282                for (SpelNodeImpl child : this.children) {
283                        if (!child.isCompilable()) {
284                                return false;
285                        }
286                }
287
288                ReflectiveMethodExecutor executor = (ReflectiveMethodExecutor) executorToCheck.get();
289                if (executor.didArgumentConversionOccur()) {
290                        return false;
291                }
292                Class<?> clazz = executor.getMethod().getDeclaringClass();
293                if (!Modifier.isPublic(clazz.getModifiers()) && executor.getPublicDeclaringClass() == null) {
294                        return false;
295                }
296
297                return true;
298        }
299
300        @Override
301        public void generateCode(MethodVisitor mv, CodeFlow cf) {
302                CachedMethodExecutor executorToCheck = this.cachedExecutor;
303                if (executorToCheck == null || !(executorToCheck.get() instanceof ReflectiveMethodExecutor)) {
304                        throw new IllegalStateException("No applicable cached executor found: " + executorToCheck);
305                }
306
307                ReflectiveMethodExecutor methodExecutor = (ReflectiveMethodExecutor) executorToCheck.get();
308                Method method = methodExecutor.getMethod();
309                boolean isStaticMethod = Modifier.isStatic(method.getModifiers());
310                String descriptor = cf.lastDescriptor();
311
312                Label skipIfNull = null;
313                if (descriptor == null && !isStaticMethod) {
314                        // Nothing on the stack but something is needed
315                        cf.loadTarget(mv);
316                }
317                if ((descriptor != null || !isStaticMethod) && this.nullSafe) {
318                        mv.visitInsn(DUP);
319                        skipIfNull = new Label();
320                        Label continueLabel = new Label();
321                        mv.visitJumpInsn(IFNONNULL, continueLabel);
322                        CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor);
323                        mv.visitJumpInsn(GOTO, skipIfNull);
324                        mv.visitLabel(continueLabel);
325                }
326                if (descriptor != null && isStaticMethod) {
327                        // Something on the stack when nothing is needed
328                        mv.visitInsn(POP);
329                }
330
331                if (CodeFlow.isPrimitive(descriptor)) {
332                        CodeFlow.insertBoxIfNecessary(mv, descriptor.charAt(0));
333                }
334
335                String classDesc;
336                if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
337                        classDesc = method.getDeclaringClass().getName().replace('.', '/');
338                }
339                else {
340                        Class<?> publicDeclaringClass = methodExecutor.getPublicDeclaringClass();
341                        Assert.state(publicDeclaringClass != null, "No public declaring class");
342                        classDesc = publicDeclaringClass.getName().replace('.', '/');
343                }
344
345                if (!isStaticMethod && (descriptor == null || !descriptor.substring(1).equals(classDesc))) {
346                        CodeFlow.insertCheckCast(mv, "L" + classDesc);
347                }
348
349                generateCodeForArguments(mv, cf, method, this.children);
350                mv.visitMethodInsn((isStaticMethod ? INVOKESTATIC : (method.isDefault() ? INVOKEINTERFACE : INVOKEVIRTUAL)),
351                                classDesc, method.getName(), CodeFlow.createSignatureDescriptor(method),
352                                method.getDeclaringClass().isInterface());
353                cf.pushDescriptor(this.exitTypeDescriptor);
354
355                if (this.originalPrimitiveExitTypeDescriptor != null) {
356                        // The output of the accessor will be a primitive but from the block above it might be null,
357                        // so to have a 'common stack' element at skipIfNull target we need to box the primitive
358                        CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor);
359                }
360                if (skipIfNull != null) {
361                        mv.visitLabel(skipIfNull);
362                }
363        }
364
365
366        private class MethodValueRef implements ValueRef {
367
368                private final EvaluationContext evaluationContext;
369
370                @Nullable
371                private final Object value;
372
373                @Nullable
374                private final TypeDescriptor targetType;
375
376                private final Object[] arguments;
377
378                public MethodValueRef(ExpressionState state, Object[] arguments) {
379                        this.evaluationContext = state.getEvaluationContext();
380                        this.value = state.getActiveContextObject().getValue();
381                        this.targetType = state.getActiveContextObject().getTypeDescriptor();
382                        this.arguments = arguments;
383                }
384
385                @Override
386                public TypedValue getValue() {
387                        TypedValue result = MethodReference.this.getValueInternal(
388                                        this.evaluationContext, this.value, this.targetType, this.arguments);
389                        updateExitTypeDescriptor();
390                        return result;
391                }
392
393                @Override
394                public void setValue(@Nullable Object newValue) {
395                        throw new IllegalAccessError();
396                }
397
398                @Override
399                public boolean isWritable() {
400                        return false;
401                }
402        }
403
404
405        private static class CachedMethodExecutor {
406
407                private final MethodExecutor methodExecutor;
408
409                @Nullable
410                private final Class<?> staticClass;
411
412                @Nullable
413                private final TypeDescriptor target;
414
415                private final List<TypeDescriptor> argumentTypes;
416
417                public CachedMethodExecutor(MethodExecutor methodExecutor, @Nullable Class<?> staticClass,
418                                @Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
419
420                        this.methodExecutor = methodExecutor;
421                        this.staticClass = staticClass;
422                        this.target = target;
423                        this.argumentTypes = argumentTypes;
424                }
425
426                public boolean isSuitable(Object value, @Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
427                        return ((this.staticClass == null || this.staticClass == value) &&
428                                        ObjectUtils.nullSafeEquals(this.target, target) && this.argumentTypes.equals(argumentTypes));
429                }
430
431                public boolean hasProxyTarget() {
432                        return (this.target != null && Proxy.isProxyClass(this.target.getType()));
433                }
434
435                public MethodExecutor get() {
436                        return this.methodExecutor;
437                }
438        }
439
440}