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.aop.aspectj;
018
019import java.lang.annotation.Annotation;
020import java.lang.reflect.Constructor;
021import java.lang.reflect.Method;
022import java.util.ArrayList;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Set;
026
027import org.aspectj.lang.JoinPoint;
028import org.aspectj.lang.ProceedingJoinPoint;
029import org.aspectj.weaver.tools.PointcutParser;
030import org.aspectj.weaver.tools.PointcutPrimitive;
031
032import org.springframework.core.ParameterNameDiscoverer;
033import org.springframework.lang.Nullable;
034import org.springframework.util.StringUtils;
035
036/**
037 * {@link ParameterNameDiscoverer} implementation that tries to deduce parameter names
038 * for an advice method from the pointcut expression, returning, and throwing clauses.
039 * If an unambiguous interpretation is not available, it returns {@code null}.
040 *
041 * <p>This class interprets arguments in the following way:
042 * <ol>
043 * <li>If the first parameter of the method is of type {@link JoinPoint}
044 * or {@link ProceedingJoinPoint}, it is assumed to be for passing
045 * {@code thisJoinPoint} to the advice, and the parameter name will
046 * be assigned the value {@code "thisJoinPoint"}.</li>
047 * <li>If the first parameter of the method is of type
048 * {@code JoinPoint.StaticPart}, it is assumed to be for passing
049 * {@code "thisJoinPointStaticPart"} to the advice, and the parameter name
050 * will be assigned the value {@code "thisJoinPointStaticPart"}.</li>
051 * <li>If a {@link #setThrowingName(String) throwingName} has been set, and
052 * there are no unbound arguments of type {@code Throwable+}, then an
053 * {@link IllegalArgumentException} is raised. If there is more than one
054 * unbound argument of type {@code Throwable+}, then an
055 * {@link AmbiguousBindingException} is raised. If there is exactly one
056 * unbound argument of type {@code Throwable+}, then the corresponding
057 * parameter name is assigned the value &lt;throwingName&gt;.</li>
058 * <li>If there remain unbound arguments, then the pointcut expression is
059 * examined. Let {@code a} be the number of annotation-based pointcut
060 * expressions (&#64;annotation, &#64;this, &#64;target, &#64;args,
061 * &#64;within, &#64;withincode) that are used in binding form. Usage in
062 * binding form has itself to be deduced: if the expression inside the
063 * pointcut is a single string literal that meets Java variable name
064 * conventions it is assumed to be a variable name. If {@code a} is
065 * zero we proceed to the next stage. If {@code a} &gt; 1 then an
066 * {@code AmbiguousBindingException} is raised. If {@code a} == 1,
067 * and there are no unbound arguments of type {@code Annotation+},
068 * then an {@code IllegalArgumentException} is raised. if there is
069 * exactly one such argument, then the corresponding parameter name is
070 * assigned the value from the pointcut expression.</li>
071 * <li>If a returningName has been set, and there are no unbound arguments
072 * then an {@code IllegalArgumentException} is raised. If there is
073 * more than one unbound argument then an
074 * {@code AmbiguousBindingException} is raised. If there is exactly
075 * one unbound argument then the corresponding parameter name is assigned
076 * the value &lt;returningName&gt;.</li>
077 * <li>If there remain unbound arguments, then the pointcut expression is
078 * examined once more for {@code this}, {@code target}, and
079 * {@code args} pointcut expressions used in the binding form (binding
080 * forms are deduced as described for the annotation based pointcuts). If
081 * there remains more than one unbound argument of a primitive type (which
082 * can only be bound in {@code args}) then an
083 * {@code AmbiguousBindingException} is raised. If there is exactly
084 * one argument of a primitive type, then if exactly one {@code args}
085 * bound variable was found, we assign the corresponding parameter name
086 * the variable name. If there were no {@code args} bound variables
087 * found an {@code IllegalStateException} is raised. If there are
088 * multiple {@code args} bound variables, an
089 * {@code AmbiguousBindingException} is raised. At this point, if
090 * there remains more than one unbound argument we raise an
091 * {@code AmbiguousBindingException}. If there are no unbound arguments
092 * remaining, we are done. If there is exactly one unbound argument
093 * remaining, and only one candidate variable name unbound from
094 * {@code this}, {@code target}, or {@code args}, it is
095 * assigned as the corresponding parameter name. If there are multiple
096 * possibilities, an {@code AmbiguousBindingException} is raised.</li>
097 * </ol>
098 *
099 * <p>The behavior on raising an {@code IllegalArgumentException} or
100 * {@code AmbiguousBindingException} is configurable to allow this discoverer
101 * to be used as part of a chain-of-responsibility. By default the condition will
102 * be logged and the {@code getParameterNames(..)} method will simply return
103 * {@code null}. If the {@link #setRaiseExceptions(boolean) raiseExceptions}
104 * property is set to {@code true}, the conditions will be thrown as
105 * {@code IllegalArgumentException} and {@code AmbiguousBindingException},
106 * respectively.
107 *
108 * <p>Was that perfectly clear? ;)
109 *
110 * <p>Short version: If an unambiguous binding can be deduced, then it is.
111 * If the advice requirements cannot possibly be satisfied, then {@code null}
112 * is returned. By setting the {@link #setRaiseExceptions(boolean) raiseExceptions}
113 * property to {@code true}, descriptive exceptions will be thrown instead of
114 * returning {@code null} in the case that the parameter names cannot be discovered.
115 *
116 * @author Adrian Colyer
117 * @author Juergen Hoeller
118 * @since 2.0
119 */
120public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscoverer {
121
122        private static final String THIS_JOIN_POINT = "thisJoinPoint";
123        private static final String THIS_JOIN_POINT_STATIC_PART = "thisJoinPointStaticPart";
124
125        // Steps in the binding algorithm...
126        private static final int STEP_JOIN_POINT_BINDING = 1;
127        private static final int STEP_THROWING_BINDING = 2;
128        private static final int STEP_ANNOTATION_BINDING = 3;
129        private static final int STEP_RETURNING_BINDING = 4;
130        private static final int STEP_PRIMITIVE_ARGS_BINDING = 5;
131        private static final int STEP_THIS_TARGET_ARGS_BINDING = 6;
132        private static final int STEP_REFERENCE_PCUT_BINDING = 7;
133        private static final int STEP_FINISHED = 8;
134
135        private static final Set<String> singleValuedAnnotationPcds = new HashSet<>();
136        private static final Set<String> nonReferencePointcutTokens = new HashSet<>();
137
138
139        static {
140                singleValuedAnnotationPcds.add("@this");
141                singleValuedAnnotationPcds.add("@target");
142                singleValuedAnnotationPcds.add("@within");
143                singleValuedAnnotationPcds.add("@withincode");
144                singleValuedAnnotationPcds.add("@annotation");
145
146                Set<PointcutPrimitive> pointcutPrimitives = PointcutParser.getAllSupportedPointcutPrimitives();
147                for (PointcutPrimitive primitive : pointcutPrimitives) {
148                        nonReferencePointcutTokens.add(primitive.getName());
149                }
150                nonReferencePointcutTokens.add("&&");
151                nonReferencePointcutTokens.add("!");
152                nonReferencePointcutTokens.add("||");
153                nonReferencePointcutTokens.add("and");
154                nonReferencePointcutTokens.add("or");
155                nonReferencePointcutTokens.add("not");
156        }
157
158
159        /** The pointcut expression associated with the advice, as a simple String. */
160        @Nullable
161        private String pointcutExpression;
162
163        private boolean raiseExceptions;
164
165        /** If the advice is afterReturning, and binds the return value, this is the parameter name used. */
166        @Nullable
167        private String returningName;
168
169        /** If the advice is afterThrowing, and binds the thrown value, this is the parameter name used. */
170        @Nullable
171        private String throwingName;
172
173        private Class<?>[] argumentTypes = new Class<?>[0];
174
175        private String[] parameterNameBindings = new String[0];
176
177        private int numberOfRemainingUnboundArguments;
178
179
180        /**
181         * Create a new discoverer that attempts to discover parameter names.
182         * from the given pointcut expression.
183         */
184        public AspectJAdviceParameterNameDiscoverer(@Nullable String pointcutExpression) {
185                this.pointcutExpression = pointcutExpression;
186        }
187
188
189        /**
190         * Indicate whether {@link IllegalArgumentException} and {@link AmbiguousBindingException}
191         * must be thrown as appropriate in the case of failing to deduce advice parameter names.
192         * @param raiseExceptions {@code true} if exceptions are to be thrown
193         */
194        public void setRaiseExceptions(boolean raiseExceptions) {
195                this.raiseExceptions = raiseExceptions;
196        }
197
198        /**
199         * If {@code afterReturning} advice binds the return value, the
200         * returning variable name must be specified.
201         * @param returningName the name of the returning variable
202         */
203        public void setReturningName(@Nullable String returningName) {
204                this.returningName = returningName;
205        }
206
207        /**
208         * If {@code afterThrowing} advice binds the thrown value, the
209         * throwing variable name must be specified.
210         * @param throwingName the name of the throwing variable
211         */
212        public void setThrowingName(@Nullable String throwingName) {
213                this.throwingName = throwingName;
214        }
215
216
217        /**
218         * Deduce the parameter names for an advice method.
219         * <p>See the {@link AspectJAdviceParameterNameDiscoverer class level javadoc}
220         * for this class for details of the algorithm used.
221         * @param method the target {@link Method}
222         * @return the parameter names
223         */
224        @Override
225        @Nullable
226        public String[] getParameterNames(Method method) {
227                this.argumentTypes = method.getParameterTypes();
228                this.numberOfRemainingUnboundArguments = this.argumentTypes.length;
229                this.parameterNameBindings = new String[this.numberOfRemainingUnboundArguments];
230
231                int minimumNumberUnboundArgs = 0;
232                if (this.returningName != null) {
233                        minimumNumberUnboundArgs++;
234                }
235                if (this.throwingName != null) {
236                        minimumNumberUnboundArgs++;
237                }
238                if (this.numberOfRemainingUnboundArguments < minimumNumberUnboundArgs) {
239                        throw new IllegalStateException(
240                                        "Not enough arguments in method to satisfy binding of returning and throwing variables");
241                }
242
243                try {
244                        int algorithmicStep = STEP_JOIN_POINT_BINDING;
245                        while ((this.numberOfRemainingUnboundArguments > 0) && algorithmicStep < STEP_FINISHED) {
246                                switch (algorithmicStep++) {
247                                        case STEP_JOIN_POINT_BINDING:
248                                                if (!maybeBindThisJoinPoint()) {
249                                                        maybeBindThisJoinPointStaticPart();
250                                                }
251                                                break;
252                                        case STEP_THROWING_BINDING:
253                                                maybeBindThrowingVariable();
254                                                break;
255                                        case STEP_ANNOTATION_BINDING:
256                                                maybeBindAnnotationsFromPointcutExpression();
257                                                break;
258                                        case STEP_RETURNING_BINDING:
259                                                maybeBindReturningVariable();
260                                                break;
261                                        case STEP_PRIMITIVE_ARGS_BINDING:
262                                                maybeBindPrimitiveArgsFromPointcutExpression();
263                                                break;
264                                        case STEP_THIS_TARGET_ARGS_BINDING:
265                                                maybeBindThisOrTargetOrArgsFromPointcutExpression();
266                                                break;
267                                        case STEP_REFERENCE_PCUT_BINDING:
268                                                maybeBindReferencePointcutParameter();
269                                                break;
270                                        default:
271                                                throw new IllegalStateException("Unknown algorithmic step: " + (algorithmicStep - 1));
272                                }
273                        }
274                }
275                catch (AmbiguousBindingException | IllegalArgumentException ex) {
276                        if (this.raiseExceptions) {
277                                throw ex;
278                        }
279                        else {
280                                return null;
281                        }
282                }
283
284                if (this.numberOfRemainingUnboundArguments == 0) {
285                        return this.parameterNameBindings;
286                }
287                else {
288                        if (this.raiseExceptions) {
289                                throw new IllegalStateException("Failed to bind all argument names: " +
290                                                this.numberOfRemainingUnboundArguments + " argument(s) could not be bound");
291                        }
292                        else {
293                                // convention for failing is to return null, allowing participation in a chain of responsibility
294                                return null;
295                        }
296                }
297        }
298
299        /**
300         * An advice method can never be a constructor in Spring.
301         * @return {@code null}
302         * @throws UnsupportedOperationException if
303         * {@link #setRaiseExceptions(boolean) raiseExceptions} has been set to {@code true}
304         */
305        @Override
306        @Nullable
307        public String[] getParameterNames(Constructor<?> ctor) {
308                if (this.raiseExceptions) {
309                        throw new UnsupportedOperationException("An advice method can never be a constructor");
310                }
311                else {
312                        // we return null rather than throw an exception so that we behave well
313                        // in a chain-of-responsibility.
314                        return null;
315                }
316        }
317
318
319        private void bindParameterName(int index, String name) {
320                this.parameterNameBindings[index] = name;
321                this.numberOfRemainingUnboundArguments--;
322        }
323
324        /**
325         * If the first parameter is of type JoinPoint or ProceedingJoinPoint,bind "thisJoinPoint" as
326         * parameter name and return true, else return false.
327         */
328        private boolean maybeBindThisJoinPoint() {
329                if ((this.argumentTypes[0] == JoinPoint.class) || (this.argumentTypes[0] == ProceedingJoinPoint.class)) {
330                        bindParameterName(0, THIS_JOIN_POINT);
331                        return true;
332                }
333                else {
334                        return false;
335                }
336        }
337
338        private void maybeBindThisJoinPointStaticPart() {
339                if (this.argumentTypes[0] == JoinPoint.StaticPart.class) {
340                        bindParameterName(0, THIS_JOIN_POINT_STATIC_PART);
341                }
342        }
343
344        /**
345         * If a throwing name was specified and there is exactly one choice remaining
346         * (argument that is a subtype of Throwable) then bind it.
347         */
348        private void maybeBindThrowingVariable() {
349                if (this.throwingName == null) {
350                        return;
351                }
352
353                // So there is binding work to do...
354                int throwableIndex = -1;
355                for (int i = 0; i < this.argumentTypes.length; i++) {
356                        if (isUnbound(i) && isSubtypeOf(Throwable.class, i)) {
357                                if (throwableIndex == -1) {
358                                        throwableIndex = i;
359                                }
360                                else {
361                                        // Second candidate we've found - ambiguous binding
362                                        throw new AmbiguousBindingException("Binding of throwing parameter '" +
363                                                        this.throwingName + "' is ambiguous: could be bound to argument " +
364                                                        throwableIndex + " or argument " + i);
365                                }
366                        }
367                }
368
369                if (throwableIndex == -1) {
370                        throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName
371                                        + "' could not be completed as no available arguments are a subtype of Throwable");
372                }
373                else {
374                        bindParameterName(throwableIndex, this.throwingName);
375                }
376        }
377
378        /**
379         * If a returning variable was specified and there is only one choice remaining, bind it.
380         */
381        private void maybeBindReturningVariable() {
382                if (this.numberOfRemainingUnboundArguments == 0) {
383                        throw new IllegalStateException(
384                                        "Algorithm assumes that there must be at least one unbound parameter on entry to this method");
385                }
386
387                if (this.returningName != null) {
388                        if (this.numberOfRemainingUnboundArguments > 1) {
389                                throw new AmbiguousBindingException("Binding of returning parameter '" + this.returningName +
390                                                "' is ambiguous, there are " + this.numberOfRemainingUnboundArguments + " candidates.");
391                        }
392
393                        // We're all set... find the unbound parameter, and bind it.
394                        for (int i = 0; i < this.parameterNameBindings.length; i++) {
395                                if (this.parameterNameBindings[i] == null) {
396                                        bindParameterName(i, this.returningName);
397                                        break;
398                                }
399                        }
400                }
401        }
402
403
404        /**
405         * Parse the string pointcut expression looking for:
406         * &#64;this, &#64;target, &#64;args, &#64;within, &#64;withincode, &#64;annotation.
407         * If we find one of these pointcut expressions, try and extract a candidate variable
408         * name (or variable names, in the case of args).
409         * <p>Some more support from AspectJ in doing this exercise would be nice... :)
410         */
411        private void maybeBindAnnotationsFromPointcutExpression() {
412                List<String> varNames = new ArrayList<>();
413                String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " ");
414                for (int i = 0; i < tokens.length; i++) {
415                        String toMatch = tokens[i];
416                        int firstParenIndex = toMatch.indexOf('(');
417                        if (firstParenIndex != -1) {
418                                toMatch = toMatch.substring(0, firstParenIndex);
419                        }
420                        if (singleValuedAnnotationPcds.contains(toMatch)) {
421                                PointcutBody body = getPointcutBody(tokens, i);
422                                i += body.numTokensConsumed;
423                                String varName = maybeExtractVariableName(body.text);
424                                if (varName != null) {
425                                        varNames.add(varName);
426                                }
427                        }
428                        else if (tokens[i].startsWith("@args(") || tokens[i].equals("@args")) {
429                                PointcutBody body = getPointcutBody(tokens, i);
430                                i += body.numTokensConsumed;
431                                maybeExtractVariableNamesFromArgs(body.text, varNames);
432                        }
433                }
434
435                bindAnnotationsFromVarNames(varNames);
436        }
437
438        /**
439         * Match the given list of extracted variable names to argument slots.
440         */
441        private void bindAnnotationsFromVarNames(List<String> varNames) {
442                if (!varNames.isEmpty()) {
443                        // we have work to do...
444                        int numAnnotationSlots = countNumberOfUnboundAnnotationArguments();
445                        if (numAnnotationSlots > 1) {
446                                throw new AmbiguousBindingException("Found " + varNames.size() +
447                                                " potential annotation variable(s), and " +
448                                                numAnnotationSlots + " potential argument slots");
449                        }
450                        else if (numAnnotationSlots == 1) {
451                                if (varNames.size() == 1) {
452                                        // it's a match
453                                        findAndBind(Annotation.class, varNames.get(0));
454                                }
455                                else {
456                                        // multiple candidate vars, but only one slot
457                                        throw new IllegalArgumentException("Found " + varNames.size() +
458                                                        " candidate annotation binding variables" +
459                                                        " but only one potential argument binding slot");
460                                }
461                        }
462                        else {
463                                // no slots so presume those candidate vars were actually type names
464                        }
465                }
466        }
467
468        /*
469         * If the token starts meets Java identifier conventions, it's in.
470         */
471        @Nullable
472        private String maybeExtractVariableName(@Nullable String candidateToken) {
473                if (!StringUtils.hasLength(candidateToken)) {
474                        return null;
475                }
476                if (Character.isJavaIdentifierStart(candidateToken.charAt(0)) &&
477                                Character.isLowerCase(candidateToken.charAt(0))) {
478                        char[] tokenChars = candidateToken.toCharArray();
479                        for (char tokenChar : tokenChars) {
480                                if (!Character.isJavaIdentifierPart(tokenChar)) {
481                                        return null;
482                                }
483                        }
484                        return candidateToken;
485                }
486                else {
487                        return null;
488                }
489        }
490
491        /**
492         * Given an args pointcut body (could be {@code args} or {@code at_args}),
493         * add any candidate variable names to the given list.
494         */
495        private void maybeExtractVariableNamesFromArgs(@Nullable String argsSpec, List<String> varNames) {
496                if (argsSpec == null) {
497                        return;
498                }
499                String[] tokens = StringUtils.tokenizeToStringArray(argsSpec, ",");
500                for (int i = 0; i < tokens.length; i++) {
501                        tokens[i] = StringUtils.trimWhitespace(tokens[i]);
502                        String varName = maybeExtractVariableName(tokens[i]);
503                        if (varName != null) {
504                                varNames.add(varName);
505                        }
506                }
507        }
508
509        /**
510         * Parse the string pointcut expression looking for this(), target() and args() expressions.
511         * If we find one, try and extract a candidate variable name and bind it.
512         */
513        private void maybeBindThisOrTargetOrArgsFromPointcutExpression() {
514                if (this.numberOfRemainingUnboundArguments > 1) {
515                        throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments
516                                        + " unbound args at this(),target(),args() binding stage, with no way to determine between them");
517                }
518
519                List<String> varNames = new ArrayList<>();
520                String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " ");
521                for (int i = 0; i < tokens.length; i++) {
522                        if (tokens[i].equals("this") ||
523                                        tokens[i].startsWith("this(") ||
524                                        tokens[i].equals("target") ||
525                                        tokens[i].startsWith("target(")) {
526                                PointcutBody body = getPointcutBody(tokens, i);
527                                i += body.numTokensConsumed;
528                                String varName = maybeExtractVariableName(body.text);
529                                if (varName != null) {
530                                        varNames.add(varName);
531                                }
532                        }
533                        else if (tokens[i].equals("args") || tokens[i].startsWith("args(")) {
534                                PointcutBody body = getPointcutBody(tokens, i);
535                                i += body.numTokensConsumed;
536                                List<String> candidateVarNames = new ArrayList<>();
537                                maybeExtractVariableNamesFromArgs(body.text, candidateVarNames);
538                                // we may have found some var names that were bound in previous primitive args binding step,
539                                // filter them out...
540                                for (String varName : candidateVarNames) {
541                                        if (!alreadyBound(varName)) {
542                                                varNames.add(varName);
543                                        }
544                                }
545                        }
546                }
547
548
549                if (varNames.size() > 1) {
550                        throw new AmbiguousBindingException("Found " + varNames.size() +
551                                        " candidate this(), target() or args() variables but only one unbound argument slot");
552                }
553                else if (varNames.size() == 1) {
554                        for (int j = 0; j < this.parameterNameBindings.length; j++) {
555                                if (isUnbound(j)) {
556                                        bindParameterName(j, varNames.get(0));
557                                        break;
558                                }
559                        }
560                }
561                // else varNames.size must be 0 and we have nothing to bind.
562        }
563
564        private void maybeBindReferencePointcutParameter() {
565                if (this.numberOfRemainingUnboundArguments > 1) {
566                        throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments
567                                        + " unbound args at reference pointcut binding stage, with no way to determine between them");
568                }
569
570                List<String> varNames = new ArrayList<>();
571                String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " ");
572                for (int i = 0; i < tokens.length; i++) {
573                        String toMatch = tokens[i];
574                        if (toMatch.startsWith("!")) {
575                                toMatch = toMatch.substring(1);
576                        }
577                        int firstParenIndex = toMatch.indexOf('(');
578                        if (firstParenIndex != -1) {
579                                toMatch = toMatch.substring(0, firstParenIndex);
580                        }
581                        else {
582                                if (tokens.length < i + 2) {
583                                        // no "(" and nothing following
584                                        continue;
585                                }
586                                else {
587                                        String nextToken = tokens[i + 1];
588                                        if (nextToken.charAt(0) != '(') {
589                                                // next token is not "(" either, can't be a pc...
590                                                continue;
591                                        }
592                                }
593
594                        }
595
596                        // eat the body
597                        PointcutBody body = getPointcutBody(tokens, i);
598                        i += body.numTokensConsumed;
599
600                        if (!nonReferencePointcutTokens.contains(toMatch)) {
601                                // then it could be a reference pointcut
602                                String varName = maybeExtractVariableName(body.text);
603                                if (varName != null) {
604                                        varNames.add(varName);
605                                }
606                        }
607                }
608
609                if (varNames.size() > 1) {
610                        throw new AmbiguousBindingException("Found " + varNames.size() +
611                                        " candidate reference pointcut variables but only one unbound argument slot");
612                }
613                else if (varNames.size() == 1) {
614                        for (int j = 0; j < this.parameterNameBindings.length; j++) {
615                                if (isUnbound(j)) {
616                                        bindParameterName(j, varNames.get(0));
617                                        break;
618                                }
619                        }
620                }
621                // else varNames.size must be 0 and we have nothing to bind.
622        }
623
624        /*
625         * We've found the start of a binding pointcut at the given index into the
626         * token array. Now we need to extract the pointcut body and return it.
627         */
628        private PointcutBody getPointcutBody(String[] tokens, int startIndex) {
629                int numTokensConsumed = 0;
630                String currentToken = tokens[startIndex];
631                int bodyStart = currentToken.indexOf('(');
632                if (currentToken.charAt(currentToken.length() - 1) == ')') {
633                        // It's an all in one... get the text between the first (and the last)
634                        return new PointcutBody(0, currentToken.substring(bodyStart + 1, currentToken.length() - 1));
635                }
636                else {
637                        StringBuilder sb = new StringBuilder();
638                        if (bodyStart >= 0 && bodyStart != (currentToken.length() - 1)) {
639                                sb.append(currentToken.substring(bodyStart + 1));
640                                sb.append(" ");
641                        }
642                        numTokensConsumed++;
643                        int currentIndex = startIndex + numTokensConsumed;
644                        while (currentIndex < tokens.length) {
645                                if (tokens[currentIndex].equals("(")) {
646                                        currentIndex++;
647                                        continue;
648                                }
649
650                                if (tokens[currentIndex].endsWith(")")) {
651                                        sb.append(tokens[currentIndex], 0, tokens[currentIndex].length() - 1);
652                                        return new PointcutBody(numTokensConsumed, sb.toString().trim());
653                                }
654
655                                String toAppend = tokens[currentIndex];
656                                if (toAppend.startsWith("(")) {
657                                        toAppend = toAppend.substring(1);
658                                }
659                                sb.append(toAppend);
660                                sb.append(" ");
661                                currentIndex++;
662                                numTokensConsumed++;
663                        }
664
665                }
666
667                // We looked and failed...
668                return new PointcutBody(numTokensConsumed, null);
669        }
670
671        /**
672         * Match up args against unbound arguments of primitive types.
673         */
674        private void maybeBindPrimitiveArgsFromPointcutExpression() {
675                int numUnboundPrimitives = countNumberOfUnboundPrimitiveArguments();
676                if (numUnboundPrimitives > 1) {
677                        throw new AmbiguousBindingException("Found '" + numUnboundPrimitives +
678                                        "' unbound primitive arguments with no way to distinguish between them.");
679                }
680                if (numUnboundPrimitives == 1) {
681                        // Look for arg variable and bind it if we find exactly one...
682                        List<String> varNames = new ArrayList<>();
683                        String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " ");
684                        for (int i = 0; i < tokens.length; i++) {
685                                if (tokens[i].equals("args") || tokens[i].startsWith("args(")) {
686                                        PointcutBody body = getPointcutBody(tokens, i);
687                                        i += body.numTokensConsumed;
688                                        maybeExtractVariableNamesFromArgs(body.text, varNames);
689                                }
690                        }
691                        if (varNames.size() > 1) {
692                                throw new AmbiguousBindingException("Found " + varNames.size() +
693                                                " candidate variable names but only one candidate binding slot when matching primitive args");
694                        }
695                        else if (varNames.size() == 1) {
696                                // 1 primitive arg, and one candidate...
697                                for (int i = 0; i < this.argumentTypes.length; i++) {
698                                        if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) {
699                                                bindParameterName(i, varNames.get(0));
700                                                break;
701                                        }
702                                }
703                        }
704                }
705        }
706
707        /*
708         * Return true if the parameter name binding for the given parameter
709         * index has not yet been assigned.
710         */
711        private boolean isUnbound(int i) {
712                return this.parameterNameBindings[i] == null;
713        }
714
715        private boolean alreadyBound(String varName) {
716                for (int i = 0; i < this.parameterNameBindings.length; i++) {
717                        if (!isUnbound(i) && varName.equals(this.parameterNameBindings[i])) {
718                                return true;
719                        }
720                }
721                return false;
722        }
723
724        /*
725         * Return {@code true} if the given argument type is a subclass
726         * of the given supertype.
727         */
728        private boolean isSubtypeOf(Class<?> supertype, int argumentNumber) {
729                return supertype.isAssignableFrom(this.argumentTypes[argumentNumber]);
730        }
731
732        private int countNumberOfUnboundAnnotationArguments() {
733                int count = 0;
734                for (int i = 0; i < this.argumentTypes.length; i++) {
735                        if (isUnbound(i) && isSubtypeOf(Annotation.class, i)) {
736                                count++;
737                        }
738                }
739                return count;
740        }
741
742        private int countNumberOfUnboundPrimitiveArguments() {
743                int count = 0;
744                for (int i = 0; i < this.argumentTypes.length; i++) {
745                        if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) {
746                                count++;
747                        }
748                }
749                return count;
750        }
751
752        /*
753         * Find the argument index with the given type, and bind the given
754         * {@code varName} in that position.
755         */
756        private void findAndBind(Class<?> argumentType, String varName) {
757                for (int i = 0; i < this.argumentTypes.length; i++) {
758                        if (isUnbound(i) && isSubtypeOf(argumentType, i)) {
759                                bindParameterName(i, varName);
760                                return;
761                        }
762                }
763                throw new IllegalStateException("Expected to find an unbound argument of type '" +
764                                argumentType.getName() + "'");
765        }
766
767
768        /**
769         * Simple struct to hold the extracted text from a pointcut body, together
770         * with the number of tokens consumed in extracting it.
771         */
772        private static class PointcutBody {
773
774                private int numTokensConsumed;
775
776                @Nullable
777                private String text;
778
779                public PointcutBody(int tokens, @Nullable String text) {
780                        this.numTokensConsumed = tokens;
781                        this.text = text;
782                }
783        }
784
785
786        /**
787         * Thrown in response to an ambiguous binding being detected when
788         * trying to resolve a method's parameter names.
789         */
790        @SuppressWarnings("serial")
791        public static class AmbiguousBindingException extends RuntimeException {
792
793                /**
794                 * Construct a new AmbiguousBindingException with the specified message.
795                 * @param msg the detail message
796                 */
797                public AmbiguousBindingException(String msg) {
798                        super(msg);
799                }
800        }
801
802}