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