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