View Javadoc
1   /*
2    * Copyright 2002-2012 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.aop.aspectj;
18  
19  import java.lang.annotation.Annotation;
20  import java.lang.reflect.Constructor;
21  import java.lang.reflect.Method;
22  import java.util.ArrayList;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Set;
26  
27  import org.aspectj.lang.JoinPoint;
28  import org.aspectj.lang.ProceedingJoinPoint;
29  import org.aspectj.weaver.tools.PointcutParser;
30  import org.aspectj.weaver.tools.PointcutPrimitive;
31  
32  import org.springframework.core.ParameterNameDiscoverer;
33  import org.springframework.util.StringUtils;
34  
35  /**
36   * {@link ParameterNameDiscoverer} implementation that tries to deduce parameter names
37   * for an advice method from the pointcut expression, returning, and throwing clauses.
38   * If an unambiguous interpretation is not available, it returns {@code null}.
39   *
40   * <p>This class interprets arguments in the following way:
41   * <ol>
42   * <li>If the first parameter of the method is of type {@link JoinPoint}
43   * or {@link ProceedingJoinPoint}, it is assumed to be for passing
44   * {@code thisJoinPoint} to the advice, and the parameter name will
45   * be assigned the value {@code "thisJoinPoint"}.</li>
46   * <li>If the first parameter of the method is of type
47   * {@code JoinPoint.StaticPart}, it is assumed to be for passing
48   * {@code "thisJoinPointStaticPart"} to the advice, and the parameter name
49   * will be assigned the value {@code "thisJoinPointStaticPart"}.</li>
50   * <li>If a {@link #setThrowingName(String) throwingName} has been set, and
51   * there are no unbound arguments of type {@code Throwable+}, then an
52   * {@link IllegalArgumentException} is raised. If there is more than one
53   * unbound argument of type {@code Throwable+}, then an
54   * {@link AmbiguousBindingException} is raised. If there is exactly one
55   * unbound argument of type {@code Throwable+}, then the corresponding
56   * parameter name is assigned the value &lt;throwingName&gt;.</li>
57   * <li>If there remain unbound arguments, then the pointcut expression is
58   * examined. Let {@code a} be the number of annotation-based pointcut
59   * expressions (&#64;annotation, &#64;this, &#64;target, &#64;args,
60   * &#64;within, &#64;withincode) that are used in binding form. Usage in
61   * binding form has itself to be deduced: if the expression inside the
62   * pointcut is a single string literal that meets Java variable name
63   * conventions it is assumed to be a variable name. If {@code a} is
64   * zero we proceed to the next stage. If {@code a} &gt; 1 then an
65   * {@code AmbiguousBindingException} is raised. If {@code a} == 1,
66   * and there are no unbound arguments of type {@code Annotation+},
67   * then an {@code IllegalArgumentException} is raised. if there is
68   * exactly one such argument, then the corresponding parameter name is
69   * assigned the value from the pointcut expression.</li>
70   * <li>If a returningName has been set, and there are no unbound arguments
71   * then an {@code IllegalArgumentException} is raised. If there is
72   * more than one unbound argument then an
73   * {@code AmbiguousBindingException} is raised. If there is exactly
74   * one unbound argument then the corresponding parameter name is assigned
75   * the value &lt;returningName&gt;.</li>
76   * <li>If there remain unbound arguments, then the pointcut expression is
77   * examined once more for {@code this}, {@code target}, and
78   * {@code args} pointcut expressions used in the binding form (binding
79   * forms are deduced as described for the annotation based pointcuts). If
80   * there remains more than one unbound argument of a primitive type (which
81   * can only be bound in {@code args}) then an
82   * {@code AmbiguousBindingException} is raised. If there is exactly
83   * one argument of a primitive type, then if exactly one {@code args}
84   * bound variable was found, we assign the corresponding parameter name
85   * the variable name. If there were no {@code args} bound variables
86   * found an {@code IllegalStateException} is raised. If there are
87   * multiple {@code args} bound variables, an
88   * {@code AmbiguousBindingException} is raised. At this point, if
89   * there remains more than one unbound argument we raise an
90   * {@code AmbiguousBindingException}. If there are no unbound arguments
91   * remaining, we are done. If there is exactly one unbound argument
92   * remaining, and only one candidate variable name unbound from
93   * {@code this}, {@code target}, or {@code args}, it is
94   * assigned as the corresponding parameter name. If there are multiple
95   * possibilities, an {@code AmbiguousBindingException} is raised.</li>
96   * </ol>
97   *
98   * <p>The behavior on raising an {@code IllegalArgumentException} or
99   * {@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  * @since 2.0
117  */
118 public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscoverer {
119 
120 	private static final String THIS_JOIN_POINT = "thisJoinPoint";
121 	private static final String THIS_JOIN_POINT_STATIC_PART = "thisJoinPointStaticPart";
122 
123 	// Steps in the binding algorithm...
124 	private static final int STEP_JOIN_POINT_BINDING = 1;
125 	private static final int STEP_THROWING_BINDING = 2;
126 	private static final int STEP_ANNOTATION_BINDING = 3;
127 	private static final int STEP_RETURNING_BINDING = 4;
128 	private static final int STEP_PRIMITIVE_ARGS_BINDING = 5;
129 	private static final int STEP_THIS_TARGET_ARGS_BINDING = 6;
130 	private static final int STEP_REFERENCE_PCUT_BINDING = 7;
131 	private static final int STEP_FINISHED = 8;
132 
133 	private static final Set<String> singleValuedAnnotationPcds = new HashSet<String>();
134 	private static final Set<String> nonReferencePointcutTokens = new HashSet<String>();
135 
136 
137 	static {
138 		singleValuedAnnotationPcds.add("@this");
139 		singleValuedAnnotationPcds.add("@target");
140 		singleValuedAnnotationPcds.add("@within");
141 		singleValuedAnnotationPcds.add("@withincode");
142 		singleValuedAnnotationPcds.add("@annotation");
143 
144 		Set<PointcutPrimitive> pointcutPrimitives = PointcutParser.getAllSupportedPointcutPrimitives();
145 		for (PointcutPrimitive primitive : pointcutPrimitives) {
146 			nonReferencePointcutTokens.add(primitive.getName());
147 		}
148 		nonReferencePointcutTokens.add("&&");
149 		nonReferencePointcutTokens.add("!");
150 		nonReferencePointcutTokens.add("||");
151 		nonReferencePointcutTokens.add("and");
152 		nonReferencePointcutTokens.add("or");
153 		nonReferencePointcutTokens.add("not");
154 	}
155 
156 
157 	private boolean raiseExceptions;
158 
159 	/**
160 	 * If the advice is afterReturning, and binds the return value, this is the parameter name used.
161 	 */
162 	private String returningName;
163 
164 	/**
165 	 * If the advice is afterThrowing, and binds the thrown value, this is the parameter name used.
166 	 */
167 	private String throwingName;
168 
169 	/**
170 	 * The pointcut expression associated with the advice, as a simple String.
171 	 */
172 	private String pointcutExpression;
173 
174 	private Class<?>[] argumentTypes;
175 
176 	private String[] parameterNameBindings;
177 
178 	private int numberOfRemainingUnboundArguments;
179 
180 
181 	/**
182 	 * Create a new discoverer that attempts to discover parameter names
183 	 * from the given pointcut expression.
184 	 */
185 	public AspectJAdviceParameterNameDiscoverer(String pointcutExpression) {
186 		this.pointcutExpression = pointcutExpression;
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(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(String throwingName) {
213 		this.throwingName = throwingName;
214 	}
215 
216 	/**
217 	 * Deduce the parameter names for an advice method.
218 	 * <p>See the {@link AspectJAdviceParameterNameDiscoverer class level javadoc}
219 	 * for this class for details of the algorithm used.
220 	 * @param method the target {@link Method}
221 	 * @return the parameter names
222 	 */
223 	@Override
224 	public String[] getParameterNames(Method method) {
225 		this.argumentTypes = method.getParameterTypes();
226 		this.numberOfRemainingUnboundArguments = this.argumentTypes.length;
227 		this.parameterNameBindings = new String[this.numberOfRemainingUnboundArguments];
228 
229 		int minimumNumberUnboundArgs = 0;
230 		if (this.returningName != null) {
231 			minimumNumberUnboundArgs++;
232 		}
233 		if (this.throwingName != null) {
234 			minimumNumberUnboundArgs++;
235 		}
236 		if (this.numberOfRemainingUnboundArguments < minimumNumberUnboundArgs) {
237 			throw new IllegalStateException(
238 					"Not enough arguments in method to satisfy binding of returning and throwing variables");
239 		}
240 
241 		try {
242 			int algorithmicStep = STEP_JOIN_POINT_BINDING;
243 			while ((this.numberOfRemainingUnboundArguments > 0) && algorithmicStep < STEP_FINISHED) {
244 				switch (algorithmicStep++) {
245 					case STEP_JOIN_POINT_BINDING:
246 						if (!maybeBindThisJoinPoint()) {
247 							maybeBindThisJoinPointStaticPart();
248 						}
249 						break;
250 					case STEP_THROWING_BINDING:
251 						maybeBindThrowingVariable();
252 						break;
253 					case STEP_ANNOTATION_BINDING:
254 						maybeBindAnnotationsFromPointcutExpression();
255 						break;
256 					case STEP_RETURNING_BINDING:
257 						maybeBindReturningVariable();
258 						break;
259 					case STEP_PRIMITIVE_ARGS_BINDING:
260 						maybeBindPrimitiveArgsFromPointcutExpression();
261 						break;
262 					case STEP_THIS_TARGET_ARGS_BINDING:
263 						maybeBindThisOrTargetOrArgsFromPointcutExpression();
264 						break;
265 					case STEP_REFERENCE_PCUT_BINDING:
266 						maybeBindReferencePointcutParameter();
267 						break;
268 					default:
269 						throw new IllegalStateException("Unknown algorithmic step: " + (algorithmicStep - 1));
270 				}
271 			}
272 		}
273 		catch (AmbiguousBindingException ambigEx) {
274 			if (this.raiseExceptions) {
275 				throw ambigEx;
276 			}
277 			else {
278 				return null;
279 			}
280 		}
281 		catch (IllegalArgumentException ex) {
282 			if (this.raiseExceptions) {
283 				throw ex;
284 			}
285 			else {
286 				return null;
287 			}
288 		}
289 
290 		if (this.numberOfRemainingUnboundArguments == 0) {
291 			return this.parameterNameBindings;
292 		}
293 		else {
294 			if (this.raiseExceptions) {
295 				throw new IllegalStateException("Failed to bind all argument names: " +
296 						this.numberOfRemainingUnboundArguments + " argument(s) could not be bound");
297 			}
298 			else {
299 				// convention for failing is to return null, allowing participation in a chain of responsibility
300 				return null;
301 			}
302 		}
303 	}
304 
305 	/**
306 	 * An advice method can never be a constructor in Spring.
307 	 * @return {@code null}
308 	 * @throws UnsupportedOperationException if
309 	 * {@link #setRaiseExceptions(boolean) raiseExceptions} has been set to {@code true}
310 	 */
311 	@Override
312 	public String[] getParameterNames(Constructor<?> ctor) {
313 		if (this.raiseExceptions) {
314 			throw new UnsupportedOperationException("An advice method can never be a constructor");
315 		}
316 		else {
317 			// we return null rather than throw an exception so that we behave well
318 			// in a chain-of-responsibility.
319 			return null;
320 		}
321 	}
322 
323 
324 	private void bindParameterName(int index, String name) {
325 		this.parameterNameBindings[index] = name;
326 		this.numberOfRemainingUnboundArguments--;
327 	}
328 
329 	/**
330 	 * If the first parameter is of type JoinPoint or ProceedingJoinPoint,bind "thisJoinPoint" as
331 	 * parameter name and return true, else return false.
332 	 */
333 	private boolean maybeBindThisJoinPoint() {
334 		if ((this.argumentTypes[0] == JoinPoint.class) || (this.argumentTypes[0] == ProceedingJoinPoint.class)) {
335 			bindParameterName(0, THIS_JOIN_POINT);
336 			return true;
337 		}
338 		else {
339 			return false;
340 		}
341 	}
342 
343 	private void maybeBindThisJoinPointStaticPart() {
344 		if (this.argumentTypes[0] == JoinPoint.StaticPart.class) {
345 			bindParameterName(0, THIS_JOIN_POINT_STATIC_PART);
346 		}
347 	}
348 
349 	/**
350 	 * If a throwing name was specified and there is exactly one choice remaining
351 	 * (argument that is a subtype of Throwable) then bind it.
352 	 */
353 	private void maybeBindThrowingVariable() {
354 		if (this.throwingName == null) {
355 			return;
356 		}
357 
358 		// So there is binding work to do...
359 		int throwableIndex = -1;
360 		for (int i = 0; i < this.argumentTypes.length; i++) {
361 			if (isUnbound(i) && isSubtypeOf(Throwable.class, i)) {
362 				if (throwableIndex == -1) {
363 					throwableIndex = i;
364 				}
365 				else {
366 					// Second candidate we've found - ambiguous binding
367 					throw new AmbiguousBindingException("Binding of throwing parameter '" +
368 							this.throwingName + "' is ambiguous: could be bound to argument " +
369 							throwableIndex + " or argument " + i);
370 				}
371 			}
372 		}
373 
374 		if (throwableIndex == -1) {
375 			throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName
376 					+ "' could not be completed as no available arguments are a subtype of Throwable");
377 		}
378 		else {
379 			bindParameterName(throwableIndex, this.throwingName);
380 		}
381 	}
382 
383 	/**
384 	 * If a returning variable was specified and there is only one choice remaining, bind it.
385 	 */
386 	private void maybeBindReturningVariable() {
387 		if (this.numberOfRemainingUnboundArguments == 0) {
388 			throw new IllegalStateException(
389 					"Algorithm assumes that there must be at least one unbound parameter on entry to this method");
390 		}
391 
392 		if (this.returningName != null) {
393 			if (this.numberOfRemainingUnboundArguments > 1) {
394 				throw new AmbiguousBindingException("Binding of returning parameter '" + this.returningName +
395 						"' is ambiguous, there are " + this.numberOfRemainingUnboundArguments + " candidates.");
396 			}
397 
398 			// We're all set... find the unbound parameter, and bind it.
399 			for (int i = 0; i < this.parameterNameBindings.length; i++) {
400 				if (this.parameterNameBindings[i] == null) {
401 					bindParameterName(i, this.returningName);
402 					break;
403 				}
404 			}
405 		}
406 	}
407 
408 
409 	/**
410 	 * Parse the string pointcut expression looking for:
411 	 * &#64;this, &#64;target, &#64;args, &#64;within, &#64;withincode, &#64;annotation.
412 	 * If we find one of these pointcut expressions, try and extract a candidate variable
413 	 * name (or variable names, in the case of args).
414 	 * <p>Some more support from AspectJ in doing this exercise would be nice... :)
415 	 */
416 	private void maybeBindAnnotationsFromPointcutExpression() {
417 		List<String> varNames = new ArrayList<String>();
418 		String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " ");
419 		for (int i = 0; i < tokens.length; i++) {
420 			String toMatch = tokens[i];
421 			int firstParenIndex = toMatch.indexOf("(");
422 			if (firstParenIndex != -1) {
423 				toMatch = toMatch.substring(0, firstParenIndex);
424 			}
425 			if (singleValuedAnnotationPcds.contains(toMatch)) {
426 				PointcutBody body = getPointcutBody(tokens, i);
427 				i += body.numTokensConsumed;
428 				String varName = maybeExtractVariableName(body.text);
429 				if (varName != null) {
430 					varNames.add(varName);
431 				}
432 			}
433 			else if (tokens[i].startsWith("@args(") || tokens[i].equals("@args")) {
434 				PointcutBody body = getPointcutBody(tokens, i);
435 				i += body.numTokensConsumed;
436 				maybeExtractVariableNamesFromArgs(body.text, varNames);
437 			}
438 		}
439 
440 		bindAnnotationsFromVarNames(varNames);
441 	}
442 
443 	/**
444 	 * Match the given list of extracted variable names to argument slots.
445 	 */
446 	private void bindAnnotationsFromVarNames(List<String> varNames) {
447 		if (!varNames.isEmpty()) {
448 			// we have work to do...
449 			int numAnnotationSlots = countNumberOfUnboundAnnotationArguments();
450 			if (numAnnotationSlots > 1) {
451 				throw new AmbiguousBindingException("Found " + varNames.size() +
452 						" potential annotation variable(s), and " +
453 						numAnnotationSlots + " potential argument slots");
454 			}
455 			else if (numAnnotationSlots == 1) {
456 				if (varNames.size() == 1) {
457 					// it's a match
458 					findAndBind(Annotation.class, varNames.get(0));
459 				}
460 				else {
461 					// multiple candidate vars, but only one slot
462 					throw new IllegalArgumentException("Found " + varNames.size() +
463 							" candidate annotation binding variables" +
464 							" but only one potential argument binding slot");
465 				}
466 			}
467 			else {
468 				// no slots so presume those candidate vars were actually type names
469 			}
470 		}
471 	}
472 
473 	/*
474 	 * If the token starts meets Java identifier conventions, it's in.
475 	 */
476 	private String maybeExtractVariableName(String candidateToken) {
477 		if (candidateToken == null || candidateToken.equals("")) {
478 			return null;
479 		}
480 		if (Character.isJavaIdentifierStart(candidateToken.charAt(0)) &&
481 				Character.isLowerCase(candidateToken.charAt(0))) {
482 			char[] tokenChars = candidateToken.toCharArray();
483 			for (char tokenChar : tokenChars) {
484 				if (!Character.isJavaIdentifierPart(tokenChar)) {
485 					return null;
486 				}
487 			}
488 			return candidateToken;
489 		}
490 		else {
491 			return null;
492 		}
493 	}
494 
495 	/**
496 	 * Given an args pointcut body (could be {@code args} or {@code at_args}),
497 	 * add any candidate variable names to the given list.
498 	 */
499 	private void maybeExtractVariableNamesFromArgs(String argsSpec, List<String> varNames) {
500 		if (argsSpec == null) {
501 			return;
502 		}
503 		String[] tokens = StringUtils.tokenizeToStringArray(argsSpec, ",");
504 		for (int i = 0; i < tokens.length; i++) {
505 			tokens[i] = StringUtils.trimWhitespace(tokens[i]);
506 			String varName = maybeExtractVariableName(tokens[i]);
507 			if (varName != null) {
508 				varNames.add(varName);
509 			}
510 		}
511 	}
512 
513 	/**
514 	 * Parse the string pointcut expression looking for this(), target() and args() expressions.
515 	 * If we find one, try and extract a candidate variable name and bind it.
516 	 */
517 	private void maybeBindThisOrTargetOrArgsFromPointcutExpression() {
518 		if (this.numberOfRemainingUnboundArguments > 1) {
519 			throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments
520 					+ " unbound args at this(),target(),args() binding stage, with no way to determine between them");
521 		}
522 
523 		List<String> varNames = new ArrayList<String>();
524 		String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " ");
525 		for (int i = 0; i < tokens.length; i++) {
526 			if (tokens[i].equals("this") ||
527 					tokens[i].startsWith("this(") ||
528 					tokens[i].equals("target") ||
529 					tokens[i].startsWith("target(")) {
530 				PointcutBody body = getPointcutBody(tokens, i);
531 				i += body.numTokensConsumed;
532 				String varName = maybeExtractVariableName(body.text);
533 				if (varName != null) {
534 					varNames.add(varName);
535 				}
536 			}
537 			else if (tokens[i].equals("args") || tokens[i].startsWith("args(")) {
538 				PointcutBody body = getPointcutBody(tokens, i);
539 				i += body.numTokensConsumed;
540 				List<String> candidateVarNames = new ArrayList<String>();
541 				maybeExtractVariableNamesFromArgs(body.text, candidateVarNames);
542 				// we may have found some var names that were bound in previous primitive args binding step,
543 				// filter them out...
544 				for (String varName : candidateVarNames) {
545 					if (!alreadyBound(varName)) {
546 						varNames.add(varName);
547 					}
548 				}
549 			}
550 		}
551 
552 
553 		if (varNames.size() > 1) {
554 			throw new AmbiguousBindingException("Found " + varNames.size() +
555 					" candidate this(), target() or args() variables but only one unbound argument slot");
556 		}
557 		else if (varNames.size() == 1) {
558 			for (int j = 0; j < this.parameterNameBindings.length; j++) {
559 				if (isUnbound(j)) {
560 					bindParameterName(j, varNames.get(0));
561 					break;
562 				}
563 			}
564 		}
565 		// else varNames.size must be 0 and we have nothing to bind.
566 	}
567 
568 	private void maybeBindReferencePointcutParameter() {
569 		if (this.numberOfRemainingUnboundArguments > 1) {
570 			throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments
571 					+ " unbound args at reference pointcut binding stage, with no way to determine between them");
572 		}
573 
574 		List<String> varNames = new ArrayList<String>();
575 		String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " ");
576 		for (int i = 0; i < tokens.length; i++) {
577 			String toMatch = tokens[i];
578 			if (toMatch.startsWith("!")) {
579 				toMatch = toMatch.substring(1);
580 			}
581 			int firstParenIndex = toMatch.indexOf("(");
582 			if (firstParenIndex != -1) {
583 				toMatch = toMatch.substring(0, firstParenIndex);
584 			}
585 			else {
586 				if (tokens.length < i + 2) {
587 					// no "(" and nothing following
588 					continue;
589 				}
590 				else {
591 					String nextToken = tokens[i + 1];
592 					if (nextToken.charAt(0) != '(') {
593 						// next token is not "(" either, can't be a pc...
594 						continue;
595 					}
596 				}
597 
598 			}
599 
600 			// eat the body
601 			PointcutBody body = getPointcutBody(tokens, i);
602 			i += body.numTokensConsumed;
603 
604 			if (!nonReferencePointcutTokens.contains(toMatch)) {
605 				// then it could be a reference pointcut
606 				String varName = maybeExtractVariableName(body.text);
607 				if (varName != null) {
608 					varNames.add(varName);
609 				}
610 			}
611 		}
612 
613 		if (varNames.size() > 1) {
614 			throw new AmbiguousBindingException("Found " + varNames.size() +
615 					" candidate reference pointcut variables but only one unbound argument slot");
616 		}
617 		else if (varNames.size() == 1) {
618 			for (int j = 0; j < this.parameterNameBindings.length; j++) {
619 				if (isUnbound(j)) {
620 					bindParameterName(j, varNames.get(0));
621 					break;
622 				}
623 			}
624 		}
625 		// else varNames.size must be 0 and we have nothing to bind.
626 	}
627 
628 	/*
629 	 * We've found the start of a binding pointcut at the given index into the
630 	 * token array. Now we need to extract the pointcut body and return it.
631 	 */
632 	private PointcutBody getPointcutBody(String[] tokens, int startIndex) {
633 		int numTokensConsumed = 0;
634 		String currentToken = tokens[startIndex];
635 		int bodyStart = currentToken.indexOf('(');
636 		if (currentToken.charAt(currentToken.length() - 1) == ')') {
637 			// It's an all in one... get the text between the first (and the last)
638 			return new PointcutBody(0, currentToken.substring(bodyStart + 1, currentToken.length() - 1));
639 		}
640 		else {
641 			StringBuilder sb = new StringBuilder();
642 			if (bodyStart >= 0 && bodyStart != (currentToken.length() - 1)) {
643 				sb.append(currentToken.substring(bodyStart + 1));
644 				sb.append(" ");
645 			}
646 			numTokensConsumed++;
647 			int currentIndex = startIndex + numTokensConsumed;
648 			while (currentIndex < tokens.length) {
649 				if (tokens[currentIndex].equals("(")) {
650 					currentIndex++;
651 					continue;
652 				}
653 
654 				if (tokens[currentIndex].endsWith(")")) {
655 					sb.append(tokens[currentIndex].substring(0, tokens[currentIndex].length() - 1));
656 					return new PointcutBody(numTokensConsumed, sb.toString().trim());
657 				}
658 
659 				String toAppend = tokens[currentIndex];
660 				if (toAppend.startsWith("(")) {
661 					toAppend = toAppend.substring(1);
662 				}
663 				sb.append(toAppend);
664 				sb.append(" ");
665 				currentIndex++;
666 				numTokensConsumed++;
667 			}
668 
669 		}
670 
671 		// We looked and failed...
672 		return new PointcutBody(numTokensConsumed, null);
673 	}
674 
675 	/**
676 	 * Match up args against unbound arguments of primitive types
677 	 */
678 	private void maybeBindPrimitiveArgsFromPointcutExpression() {
679 		int numUnboundPrimitives = countNumberOfUnboundPrimitiveArguments();
680 		if (numUnboundPrimitives > 1) {
681 			throw new AmbiguousBindingException("Found '" + numUnboundPrimitives +
682 					"' unbound primitive arguments with no way to distinguish between them.");
683 		}
684 		if (numUnboundPrimitives == 1) {
685 			// Look for arg variable and bind it if we find exactly one...
686 			List<String> varNames = new ArrayList<String>();
687 			String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " ");
688 			for (int i = 0; i < tokens.length; i++) {
689 				if (tokens[i].equals("args") || tokens[i].startsWith("args(")) {
690 					PointcutBody body = getPointcutBody(tokens, i);
691 					i += body.numTokensConsumed;
692 					maybeExtractVariableNamesFromArgs(body.text, varNames);
693 				}
694 			}
695 			if (varNames.size() > 1) {
696 				throw new AmbiguousBindingException("Found " + varNames.size() +
697 						" candidate variable names but only one candidate binding slot when matching primitive args");
698 			}
699 			else if (varNames.size() == 1) {
700 				// 1 primitive arg, and one candidate...
701 				for (int i = 0; i < this.argumentTypes.length; i++) {
702 					if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) {
703 						bindParameterName(i, varNames.get(0));
704 						break;
705 					}
706 				}
707 			}
708 		}
709 	}
710 
711 	/*
712 	 * Return true if the parameter name binding for the given parameter
713 	 * index has not yet been assigned.
714 	 */
715 	private boolean isUnbound(int i) {
716 		return this.parameterNameBindings[i] == null;
717 	}
718 
719 	private boolean alreadyBound(String varName) {
720 		for (int i = 0; i < this.parameterNameBindings.length; i++) {
721 			if (!isUnbound(i) && varName.equals(this.parameterNameBindings[i])) {
722 				return true;
723 			}
724 		}
725 		return false;
726 	}
727 
728 	/*
729 	 * Return {@code true} if the given argument type is a subclass
730 	 * of the given supertype.
731 	 */
732 	private boolean isSubtypeOf(Class<?> supertype, int argumentNumber) {
733 		return supertype.isAssignableFrom(this.argumentTypes[argumentNumber]);
734 	}
735 
736 	private int countNumberOfUnboundAnnotationArguments() {
737 		int count = 0;
738 		for (int i = 0; i < this.argumentTypes.length; i++) {
739 			if (isUnbound(i) && isSubtypeOf(Annotation.class, i)) {
740 				count++;
741 			}
742 		}
743 		return count;
744 	}
745 
746 	private int countNumberOfUnboundPrimitiveArguments() {
747 		int count = 0;
748 		for (int i = 0; i < this.argumentTypes.length; i++) {
749 			if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) {
750 				count++;
751 			}
752 		}
753 		return count;
754 	}
755 
756 	/*
757 	 * Find the argument index with the given type, and bind the given
758 	 * {@code varName} in that position.
759 	 */
760 	private void findAndBind(Class<?> argumentType, String varName) {
761 		for (int i = 0; i < this.argumentTypes.length; i++) {
762 			if (isUnbound(i) && isSubtypeOf(argumentType, i)) {
763 				bindParameterName(i, varName);
764 				return;
765 			}
766 		}
767 		throw new IllegalStateException("Expected to find an unbound argument of type '" +
768 				argumentType.getName() + "'");
769 	}
770 
771 
772 	/**
773 	 * Simple struct to hold the extracted text from a pointcut body, together
774 	 * with the number of tokens consumed in extracting it.
775 	 */
776 	private static class PointcutBody {
777 
778 		private int numTokensConsumed;
779 
780 		private String text;
781 
782 		public PointcutBody(int tokens, String text) {
783 			this.numTokensConsumed = tokens;
784 			this.text = text;
785 		}
786 	}
787 
788 
789 	/**
790 	 * Thrown in response to an ambiguous binding being detected when
791 	 * trying to resolve a method's parameter names.
792 	 */
793 	@SuppressWarnings("serial")
794 	public static class AmbiguousBindingException extends RuntimeException {
795 
796 		/**
797 		 * Construct a new AmbiguousBindingException with the specified message.
798 		 * @param msg the detail message
799 		 */
800 		public AmbiguousBindingException(String msg) {
801 			super(msg);
802 		}
803 	}
804 
805 }