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 <throwingName>.</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 (@annotation, @this, @target, @args, 060 * @within, @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} > 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 <returningName>.</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 * @this, @target, @args, @within, @withincode, @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}