001// Copyright 2007-2013 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.beanmodel.internal.services; 016 017import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.DECIMAL; 018import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.DEREF; 019import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.FALSE; 020import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.IDENTIFIER; 021import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.INTEGER; 022import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.INVOKE; 023import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.LIST; 024import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.MAP; 025import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.NOT; 026import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.NULL; 027import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.RANGEOP; 028import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.SAFEDEREF; 029import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.STRING; 030import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.THIS; 031import static org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser.TRUE; 032 033import org.apache.tapestry5.ioc.annotations.ComponentLayer; 034import java.io.ByteArrayInputStream; 035import java.io.IOException; 036import java.io.InputStream; 037import java.lang.annotation.Annotation; 038import java.lang.reflect.Field; 039import java.lang.reflect.Member; 040import java.lang.reflect.Method; 041import java.lang.reflect.Modifier; 042import java.lang.reflect.Type; 043import java.util.ArrayList; 044import java.util.HashMap; 045import java.util.List; 046import java.util.Map; 047 048import org.antlr.runtime.ANTLRInputStream; 049import org.antlr.runtime.CommonTokenStream; 050import org.antlr.runtime.tree.Tree; 051import org.apache.tapestry5.beanmodel.PropertyConduit; 052import org.apache.tapestry5.beanmodel.PropertyConduit2; 053import org.apache.tapestry5.beanmodel.internal.InternalPropertyConduit; 054import org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionLexer; 055import org.apache.tapestry5.beanmodel.internal.antlr.PropertyExpressionParser; 056import org.apache.tapestry5.beanmodel.services.PropertyConduitSource; 057import org.apache.tapestry5.commons.AnnotationProvider; 058import org.apache.tapestry5.commons.internal.NullAnnotationProvider; 059import org.apache.tapestry5.commons.internal.services.StringInterner; 060import org.apache.tapestry5.commons.internal.util.GenericsUtils; 061import org.apache.tapestry5.commons.internal.util.InternalCommonsUtils; 062import org.apache.tapestry5.commons.services.ClassPropertyAdapter; 063import org.apache.tapestry5.commons.services.InvalidationEventHub; 064import org.apache.tapestry5.commons.services.PlasticProxyFactory; 065import org.apache.tapestry5.commons.services.PropertyAccess; 066import org.apache.tapestry5.commons.services.PropertyAdapter; 067import org.apache.tapestry5.commons.services.TypeCoercer; 068import org.apache.tapestry5.commons.util.AvailableValues; 069import org.apache.tapestry5.commons.util.CollectionFactory; 070import org.apache.tapestry5.commons.util.ExceptionUtils; 071import org.apache.tapestry5.commons.util.IntegerRange; 072import org.apache.tapestry5.commons.util.MultiKey; 073import org.apache.tapestry5.commons.util.UnknownValueException; 074import org.apache.tapestry5.ioc.annotations.ComponentClasses; 075import org.apache.tapestry5.ioc.annotations.PostInjection; 076import org.apache.tapestry5.plastic.Condition; 077import org.apache.tapestry5.plastic.InstructionBuilder; 078import org.apache.tapestry5.plastic.InstructionBuilderCallback; 079import org.apache.tapestry5.plastic.MethodDescription; 080import org.apache.tapestry5.plastic.PlasticClass; 081import org.apache.tapestry5.plastic.PlasticClassTransformer; 082import org.apache.tapestry5.plastic.PlasticField; 083import org.apache.tapestry5.plastic.PlasticMethod; 084import org.apache.tapestry5.plastic.PlasticUtils; 085 086public class PropertyConduitSourceImpl implements PropertyConduitSource 087{ 088 static class ConduitMethods 089 { 090 private static final MethodDescription GET = getMethodDescription(PropertyConduit.class, "get", Object.class); 091 092 private static final MethodDescription SET = getMethodDescription(PropertyConduit.class, "set", Object.class, 093 Object.class); 094 095 private static final MethodDescription GET_PROPERTY_TYPE = getMethodDescription(PropertyConduit.class, 096 "getPropertyType"); 097 098 private static final MethodDescription GET_PROPERTY_GENERIC_TYPE = getMethodDescription(PropertyConduit2.class, 099 "getPropertyGenericType"); 100 101 private static final MethodDescription GET_PROPERTY_NAME = getMethodDescription(InternalPropertyConduit.class, 102 "getPropertyName"); 103 104 private static final MethodDescription GET_ANNOTATION = getMethodDescription(AnnotationProvider.class, 105 "getAnnotation", Class.class); 106 107 } 108 109 static class DelegateMethods 110 { 111 static final Method INVERT = getMethod(PropertyConduitDelegate.class, "invert", Object.class); 112 113 static final Method RANGE = getMethod(PropertyConduitDelegate.class, "range", int.class, int.class); 114 115 static final Method COERCE = getMethod(PropertyConduitDelegate.class, "coerce", Object.class, Class.class); 116 } 117 118 static class ArrayListMethods 119 { 120 static final Method ADD = getMethod(ArrayList.class, "add", Object.class); 121 } 122 123 static class HashMapMethods 124 { 125 static final Method PUT = getMethod(HashMap.class, "put", Object.class, Object.class); 126 } 127 128 private static InstructionBuilderCallback RETURN_NULL = new InstructionBuilderCallback() 129 { 130 public void doBuild(InstructionBuilder builder) 131 { 132 builder.loadNull().returnResult(); 133 } 134 }; 135 136 private static final String[] SINGLE_OBJECT_ARGUMENT = new String[] 137 {Object.class.getName()}; 138 139 @SuppressWarnings("unchecked") 140 private static Method getMethod(Class containingClass, String name, Class... parameterTypes) 141 { 142 try 143 { 144 return containingClass.getMethod(name, parameterTypes); 145 } catch (NoSuchMethodException ex) 146 { 147 throw new IllegalArgumentException(ex); 148 } 149 } 150 151 private static MethodDescription getMethodDescription(Class containingClass, String name, Class... parameterTypes) 152 { 153 return new MethodDescription(getMethod(containingClass, name, parameterTypes)); 154 } 155 156 private final AnnotationProvider nullAnnotationProvider = new NullAnnotationProvider(); 157 158 /** 159 * How are null values in intermdiate terms to be handled? 160 */ 161 private enum NullHandling 162 { 163 /** 164 * Add code to check for null and throw exception if null. 165 */ 166 FORBID, 167 168 /** 169 * Add code to check for null and short-circuit (i.e., the "?." 170 * safe-dereference operator) 171 */ 172 ALLOW 173 } 174 175 /** 176 * One term in an expression. Expressions start with some root type and each term advances 177 * to a new type. 178 */ 179 private class Term 180 { 181 /** 182 * The generic type of the term. 183 */ 184 final Type type; 185 186 final Class genericType; 187 188 /** 189 * Describes the term, for use in error messages. 190 */ 191 final String description; 192 193 final AnnotationProvider annotationProvider; 194 195 /** 196 * Callback that will implement the term. 197 */ 198 final InstructionBuilderCallback callback; 199 200 Term(Type type, Class genericType, String description, AnnotationProvider annotationProvider, 201 InstructionBuilderCallback callback) 202 { 203 this.type = type; 204 this.genericType = genericType; 205 this.description = description; 206 this.annotationProvider = annotationProvider; 207 this.callback = callback; 208 } 209 210 Term(Type type, String description, AnnotationProvider annotationProvider, InstructionBuilderCallback callback) 211 { 212 this(type, GenericsUtils.asClass(type), description, annotationProvider, callback); 213 } 214 215 Term(Type type, String description, InstructionBuilderCallback callback) 216 { 217 this(type, description, null, callback); 218 } 219 220 /** 221 * Returns a clone of this Term with a new callback. 222 */ 223 Term withCallback(InstructionBuilderCallback newCallback) 224 { 225 return new Term(type, genericType, description, annotationProvider, newCallback); 226 } 227 } 228 229 private final PropertyAccess access; 230 231 private final PlasticProxyFactory proxyFactory; 232 233 private final TypeCoercer typeCoercer; 234 235 private final StringInterner interner; 236 237 /** 238 * Keyed on combination of root class and expression. 239 */ 240 private final Map<MultiKey, PropertyConduit> cache = CollectionFactory.newConcurrentMap(); 241 242 private final Invariant invariantAnnotation = new Invariant() 243 { 244 public Class<? extends Annotation> annotationType() 245 { 246 return Invariant.class; 247 } 248 }; 249 250 private final AnnotationProvider invariantAnnotationProvider = new AnnotationProvider() 251 { 252 public <T extends Annotation> T getAnnotation(Class<T> annotationClass) 253 { 254 if (annotationClass == Invariant.class) 255 return annotationClass.cast(invariantAnnotation); 256 257 return null; 258 } 259 }; 260 261 private final PropertyConduit literalTrue; 262 263 private final PropertyConduit literalFalse; 264 265 private final PropertyConduit literalNull; 266 267 private final PropertyConduitDelegate sharedDelegate; 268 269 /** 270 * Encapsulates the process of building a PropertyConduit instance from an 271 * expression, as an {@link PlasticClassTransformer}. 272 */ 273 class PropertyConduitBuilder implements PlasticClassTransformer 274 { 275 private final Class rootType; 276 277 private final String expression; 278 279 private final Tree tree; 280 281 private Class conduitPropertyType; 282 283 private Type conduitPropertyGenericType; 284 285 private String conduitPropertyName; 286 287 private AnnotationProvider annotationProvider = nullAnnotationProvider; 288 289 private PlasticField delegateField; 290 291 private PlasticClass plasticClass; 292 293 private PlasticMethod getRootMethod, navMethod; 294 295 PropertyConduitBuilder(Class rootType, String expression, Tree tree) 296 { 297 this.rootType = rootType; 298 this.expression = expression; 299 this.tree = tree; 300 } 301 302 public void transform(PlasticClass plasticClass) 303 { 304 this.plasticClass = plasticClass; 305 306 // Create the various methods; also determine the conduit's property type, property name and identify 307 // the annotation provider. 308 309 implementNavMethodAndAccessors(); 310 311 implementOtherMethods(); 312 313 plasticClass.addToString(String.format("PropertyConduit[%s %s]", rootType.getName(), expression)); 314 } 315 316 private void implementOtherMethods() 317 { 318 PlasticField annotationProviderField = plasticClass.introduceField(AnnotationProvider.class, 319 "annotationProvider").inject(annotationProvider); 320 321 plasticClass.introduceMethod(ConduitMethods.GET_ANNOTATION).delegateTo(annotationProviderField); 322 323 plasticClass.introduceMethod(ConduitMethods.GET_PROPERTY_NAME, new InstructionBuilderCallback() 324 { 325 public void doBuild(InstructionBuilder builder) 326 { 327 builder.loadConstant(conduitPropertyName).returnResult(); 328 } 329 }); 330 331 final PlasticField propertyTypeField = plasticClass.introduceField(Class.class, "propertyType").inject( 332 conduitPropertyType); 333 334 plasticClass.introduceMethod(ConduitMethods.GET_PROPERTY_TYPE, new InstructionBuilderCallback() 335 { 336 public void doBuild(InstructionBuilder builder) 337 { 338 builder.loadThis().getField(propertyTypeField).returnResult(); 339 } 340 }); 341 342 final PlasticField propertyGenericTypeField = plasticClass.introduceField(Type.class, "propertyGenericType").inject( 343 conduitPropertyGenericType); 344 345 plasticClass.introduceMethod(ConduitMethods.GET_PROPERTY_GENERIC_TYPE, new InstructionBuilderCallback() 346 { 347 public void doBuild(InstructionBuilder builder) 348 { 349 builder.loadThis().getField(propertyGenericTypeField).returnResult(); 350 } 351 }); 352 } 353 354 /** 355 * Creates a method that does a conversion from Object to the expected root type, with 356 * a null check. 357 */ 358 private void implementGetRoot() 359 { 360 getRootMethod = plasticClass.introducePrivateMethod(PlasticUtils.toTypeName(rootType), "getRoot", 361 SINGLE_OBJECT_ARGUMENT, null); 362 363 getRootMethod.changeImplementation(new InstructionBuilderCallback() 364 { 365 public void doBuild(InstructionBuilder builder) 366 { 367 builder.loadArgument(0).dupe().when(Condition.NULL, new InstructionBuilderCallback() 368 { 369 public void doBuild(InstructionBuilder builder) 370 { 371 builder.throwException(NullPointerException.class, 372 String.format("Root object of property expression '%s' is null.", expression)); 373 } 374 }); 375 376 builder.checkcast(rootType).returnResult(); 377 } 378 }); 379 } 380 381 private boolean isLeaf(Tree node) 382 { 383 int type = node.getType(); 384 385 return type != DEREF && type != SAFEDEREF; 386 } 387 388 private void implementNavMethodAndAccessors() 389 { 390 implementGetRoot(); 391 392 // First, create the navigate method. 393 394 final List<InstructionBuilderCallback> callbacks = CollectionFactory.newList(); 395 396 Type activeType = rootType; 397 398 Tree node = tree; 399 400 while (!isLeaf(node)) 401 { 402 Term term = analyzeDerefNode(activeType, node); 403 404 callbacks.add(term.callback); 405 406 activeType = term.type; 407 408 // Second term is the continuation, possibly another chained 409 // DEREF, etc. 410 node = node.getChild(1); 411 } 412 413 Class activeClass = GenericsUtils.asClass(activeType); 414 415 if (callbacks.isEmpty()) 416 { 417 navMethod = getRootMethod; 418 } else 419 { 420 navMethod = plasticClass.introducePrivateMethod(PlasticUtils.toTypeName(activeClass), "navigate", 421 SINGLE_OBJECT_ARGUMENT, null); 422 423 navMethod.changeImplementation(new InstructionBuilderCallback() 424 { 425 public void doBuild(InstructionBuilder builder) 426 { 427 builder.loadThis().loadArgument(0).invokeVirtual(getRootMethod); 428 429 for (InstructionBuilderCallback callback : callbacks) 430 { 431 callback.doBuild(builder); 432 } 433 434 builder.returnResult(); 435 } 436 }); 437 } 438 439 implementAccessors(activeType, node); 440 } 441 442 private void implementAccessors(Type activeType, Tree node) 443 { 444 switch (node.getType()) 445 { 446 case IDENTIFIER: 447 448 implementPropertyAccessors(activeType, node); 449 450 return; 451 452 case INVOKE: 453 454 // So, at this point, we have the navigation method written 455 // and it covers all but the terminal 456 // de-reference. node is an IDENTIFIER or INVOKE. We're 457 // ready to use the navigation 458 // method to implement get() and set(). 459 460 implementMethodAccessors(activeType, node); 461 462 return; 463 464 case RANGEOP: 465 466 // As currently implemented, RANGEOP can only appear as the 467 // top level, which 468 // means we didn't need the navigate method after all. 469 470 implementRangeOpGetter(node); 471 implementNoOpSetter(); 472 473 conduitPropertyType = IntegerRange.class; 474 conduitPropertyGenericType = IntegerRange.class; 475 476 return; 477 478 case LIST: 479 480 implementListGetter(node); 481 implementNoOpSetter(); 482 483 conduitPropertyType = List.class; 484 conduitPropertyGenericType = List.class; 485 486 return; 487 488 case MAP: 489 implementMapGetter(node); 490 implementNoOpSetter(); 491 492 conduitPropertyType = Map.class; 493 conduitPropertyGenericType = Map.class; 494 495 return; 496 497 498 case NOT: 499 implementNotOpGetter(node); 500 implementNoOpSetter(); 501 502 conduitPropertyType = boolean.class; 503 conduitPropertyGenericType = boolean.class; 504 505 return; 506 507 default: 508 throw unexpectedNodeType(node, IDENTIFIER, INVOKE, RANGEOP, LIST, NOT); 509 } 510 } 511 512 public void implementMethodAccessors(final Type activeType, final Tree invokeNode) 513 { 514 final Term term = buildInvokeTerm(activeType, invokeNode); 515 516 implementNoOpSetter(); 517 518 conduitPropertyName = term.description; 519 conduitPropertyType = term.genericType; 520 conduitPropertyGenericType = term.genericType; 521 annotationProvider = term.annotationProvider; 522 523 plasticClass.introduceMethod(ConduitMethods.GET, new InstructionBuilderCallback() 524 { 525 public void doBuild(InstructionBuilder builder) 526 { 527 invokeNavigateMethod(builder); 528 529 term.callback.doBuild(builder); 530 531 boxIfPrimitive(builder, conduitPropertyType); 532 533 builder.returnResult(); 534 } 535 }); 536 537 implementNoOpSetter(); 538 } 539 540 public void implementPropertyAccessors(Type activeType, Tree identifierNode) 541 { 542 String propertyName = identifierNode.getText(); 543 544 PropertyAdapter adapter = findPropertyAdapter(activeType, propertyName); 545 546 conduitPropertyName = propertyName; 547 conduitPropertyType = adapter.getType(); 548 conduitPropertyGenericType = getGenericType(adapter); 549 annotationProvider = adapter; 550 551 implementGetter(adapter); 552 implementSetter(adapter); 553 } 554 555 private Type getGenericType(PropertyAdapter adapter) 556 { 557 Type genericType = null; 558 if (adapter.getField() != null) 559 { 560 genericType = adapter.getField().getGenericType(); 561 } 562 else if (adapter.getReadMethod() != null) 563 { 564 genericType = adapter.getReadMethod().getGenericReturnType(); 565 } 566 else if (adapter.getWriteMethod() != null) 567 { 568 genericType = adapter.getWriteMethod().getGenericParameterTypes()[0]; 569 } 570 else 571 { 572 throw new RuntimeException("Could not find accessor for property " + adapter.getName()); 573 } 574 575 return genericType == null ? adapter.getType() : genericType; 576 } 577 578 private void implementSetter(PropertyAdapter adapter) 579 { 580 if (adapter.getWriteMethod() != null) 581 { 582 implementSetter(adapter.getWriteMethod()); 583 return; 584 } 585 586 if (adapter.getField() != null && adapter.isUpdate()) 587 { 588 implementSetter(adapter.getField()); 589 return; 590 } 591 592 implementNoOpMethod(ConduitMethods.SET, "Expression '%s' for class %s is read-only.", expression, 593 rootType.getName()); 594 } 595 596 private boolean isStatic(Member member) 597 { 598 return Modifier.isStatic(member.getModifiers()); 599 } 600 601 private void implementSetter(final Field field) 602 { 603 if (isStatic(field)) 604 { 605 plasticClass.introduceMethod(ConduitMethods.SET, new InstructionBuilderCallback() 606 { 607 public void doBuild(InstructionBuilder builder) 608 { 609 builder.loadArgument(1).castOrUnbox(PlasticUtils.toTypeName(field.getType())); 610 611 builder.putStaticField(field.getDeclaringClass().getName(), field.getName(), field.getType()); 612 613 builder.returnResult(); 614 } 615 }); 616 617 return; 618 } 619 620 plasticClass.introduceMethod(ConduitMethods.SET, new InstructionBuilderCallback() 621 { 622 public void doBuild(InstructionBuilder builder) 623 { 624 invokeNavigateMethod(builder); 625 626 builder.loadArgument(1).castOrUnbox(PlasticUtils.toTypeName(field.getType())); 627 628 builder.putField(field.getDeclaringClass().getName(), field.getName(), field.getType()); 629 630 builder.returnResult(); 631 } 632 }); 633 } 634 635 private void implementSetter(final Method writeMethod) 636 { 637 plasticClass.introduceMethod(ConduitMethods.SET, new InstructionBuilderCallback() 638 { 639 public void doBuild(InstructionBuilder builder) 640 { 641 invokeNavigateMethod(builder); 642 643 Class propertyType = writeMethod.getParameterTypes()[0]; 644 String propertyTypeName = PlasticUtils.toTypeName(propertyType); 645 646 builder.loadArgument(1).castOrUnbox(propertyTypeName); 647 648 builder.invoke(writeMethod); 649 650 builder.returnResult(); 651 } 652 }); 653 } 654 655 private void implementGetter(PropertyAdapter adapter) 656 { 657 if (adapter.getReadMethod() != null) 658 { 659 implementGetter(adapter.getReadMethod()); 660 return; 661 } 662 663 if (adapter.getField() != null) 664 { 665 implementGetter(adapter.getField()); 666 return; 667 } 668 669 implementNoOpMethod(ConduitMethods.GET, "Expression '%s' for class %s is write-only.", expression, 670 rootType.getName()); 671 } 672 673 private void implementGetter(final Field field) 674 { 675 if (isStatic(field)) 676 { 677 plasticClass.introduceMethod(ConduitMethods.GET, new InstructionBuilderCallback() 678 { 679 public void doBuild(InstructionBuilder builder) 680 { 681 builder.getStaticField(field.getDeclaringClass().getName(), field.getName(), field.getType()); 682 683 // Cast not necessary here since the return type of get() is Object 684 685 boxIfPrimitive(builder, field.getType()); 686 687 builder.returnResult(); 688 } 689 }); 690 691 return; 692 } 693 694 plasticClass.introduceMethod(ConduitMethods.GET, new InstructionBuilderCallback() 695 { 696 public void doBuild(InstructionBuilder builder) 697 { 698 invokeNavigateMethod(builder); 699 700 builder.getField(field.getDeclaringClass().getName(), field.getName(), field.getType()); 701 702 // Cast not necessary here since the return type of get() is Object 703 704 boxIfPrimitive(builder, field.getType()); 705 706 builder.returnResult(); 707 } 708 }); 709 } 710 711 private void implementGetter(final Method readMethod) 712 { 713 plasticClass.introduceMethod(ConduitMethods.GET, new InstructionBuilderCallback() 714 { 715 public void doBuild(InstructionBuilder builder) 716 { 717 invokeNavigateMethod(builder); 718 719 invokeMethod(builder, readMethod, null, 0); 720 721 boxIfPrimitive(builder, conduitPropertyType); 722 723 builder.returnResult(); 724 } 725 }); 726 } 727 728 private void implementRangeOpGetter(final Tree rangeNode) 729 { 730 plasticClass.introduceMethod(ConduitMethods.GET, new InstructionBuilderCallback() 731 { 732 public void doBuild(InstructionBuilder builder) 733 { 734 // Put the delegate on top of the stack 735 736 builder.loadThis().getField(getDelegateField()); 737 738 invokeMethod(builder, DelegateMethods.RANGE, rangeNode, 0); 739 740 builder.returnResult(); 741 } 742 }); 743 } 744 745 /** 746 * @param node 747 * subexpression to invert 748 */ 749 private void implementNotOpGetter(final Tree node) 750 { 751 // Implement get() as navigate, then do a method invocation based on node 752 // then, then pass (wrapped) result to delegate.invert() 753 754 plasticClass.introduceMethod(ConduitMethods.GET, new InstructionBuilderCallback() 755 { 756 public void doBuild(InstructionBuilder builder) 757 { 758 Type expressionType = implementNotExpression(builder, node); 759 760 // Yes, we know this will always be the case, for now. 761 762 boxIfPrimitive(builder, expressionType); 763 764 builder.returnResult(); 765 } 766 }); 767 } 768 769 /** 770 * The first part of any implementation of get() or set(): invoke the navigation method 771 * and if the result is null, return immediately. 772 */ 773 private void invokeNavigateMethod(InstructionBuilder builder) 774 { 775 builder.loadThis().loadArgument(0).invokeVirtual(navMethod); 776 777 builder.dupe().when(Condition.NULL, RETURN_NULL); 778 } 779 780 /** 781 * Uses the builder to add instructions for a subexpression. 782 * 783 * @param builder 784 * used to add instructions 785 * @param activeType 786 * type of value on top of the stack when this code will execute, or null if no value on stack 787 * @param node 788 * defines the expression 789 * @return the expression type 790 */ 791 private Type implementSubexpression(InstructionBuilder builder, Type activeType, Tree node) 792 { 793 Term term; 794 795 while (true) 796 { 797 switch (node.getType()) 798 { 799 case IDENTIFIER: 800 case INVOKE: 801 802 if (activeType == null) 803 { 804 invokeGetRootMethod(builder); 805 806 activeType = rootType; 807 } 808 809 term = buildTerm(activeType, node); 810 811 term.callback.doBuild(builder); 812 813 return term.type; 814 815 case INTEGER: 816 817 builder.loadConstant(new Long(node.getText())); 818 819 return long.class; 820 821 case DECIMAL: 822 823 builder.loadConstant(new Double(node.getText())); 824 825 return double.class; 826 827 case STRING: 828 829 builder.loadConstant(node.getText()); 830 831 return String.class; 832 833 case DEREF: 834 case SAFEDEREF: 835 836 if (activeType == null) 837 { 838 invokeGetRootMethod(builder); 839 840 activeType = rootType; 841 } 842 843 term = analyzeDerefNode(activeType, node); 844 845 term.callback.doBuild(builder); 846 847 activeType = GenericsUtils.asClass(term.type); 848 849 node = node.getChild(1); 850 851 break; 852 853 case TRUE: 854 case FALSE: 855 856 builder.loadConstant(node.getType() == TRUE ? 1 : 0); 857 858 return boolean.class; 859 860 case LIST: 861 862 return implementListConstructor(builder, node); 863 864 case MAP: 865 return implementMapConstructor(builder, node); 866 867 case NOT: 868 869 return implementNotExpression(builder, node); 870 871 case THIS: 872 873 invokeGetRootMethod(builder); 874 875 return rootType; 876 877 case NULL: 878 879 builder.loadNull(); 880 881 return Void.class; 882 883 default: 884 throw unexpectedNodeType(node, TRUE, FALSE, INTEGER, DECIMAL, STRING, DEREF, SAFEDEREF, 885 IDENTIFIER, INVOKE, LIST, NOT, THIS, NULL); 886 } 887 } 888 } 889 890 public void invokeGetRootMethod(InstructionBuilder builder) 891 { 892 builder.loadThis().loadArgument(0).invokeVirtual(getRootMethod); 893 } 894 895 private void implementListGetter(final Tree listNode) 896 { 897 plasticClass.introduceMethod(ConduitMethods.GET, new InstructionBuilderCallback() 898 { 899 public void doBuild(InstructionBuilder builder) 900 { 901 implementListConstructor(builder, listNode); 902 903 builder.returnResult(); 904 } 905 }); 906 } 907 908 private Type implementListConstructor(InstructionBuilder builder, Tree listNode) 909 { 910 // First, create an empty instance of ArrayList 911 912 int count = listNode.getChildCount(); 913 914 builder.newInstance(ArrayList.class); 915 builder.dupe().loadConstant(count).invokeConstructor(ArrayList.class, int.class); 916 917 for (int i = 0; i < count; i++) 918 { 919 builder.dupe(); // the ArrayList 920 921 Type expressionType = implementSubexpression(builder, null, listNode.getChild(i)); 922 923 boxIfPrimitive(builder, GenericsUtils.asClass(expressionType)); 924 925 // Add the value to the array, then pop off the returned boolean 926 builder.invoke(ArrayListMethods.ADD).pop(); 927 } 928 929 return ArrayList.class; 930 } 931 932 private void implementMapGetter(final Tree mapNode) 933 { 934 plasticClass.introduceMethod(ConduitMethods.GET, new InstructionBuilderCallback() 935 { 936 public void doBuild(InstructionBuilder builder) 937 { 938 implementMapConstructor(builder, mapNode); 939 940 builder.returnResult(); 941 } 942 }); 943 } 944 945 private Type implementMapConstructor(InstructionBuilder builder, Tree mapNode) 946 { 947 int count = mapNode.getChildCount(); 948 builder.newInstance(HashMap.class); 949 builder.dupe().loadConstant(count).invokeConstructor(HashMap.class, int.class); 950 951 for (int i = 0; i < count; i += 2) 952 { 953 builder.dupe(); 954 955 //build the key: 956 Type keyType = implementSubexpression(builder, null, mapNode.getChild(i)); 957 boxIfPrimitive(builder, GenericsUtils.asClass(keyType)); 958 959 //and the value: 960 Type valueType = implementSubexpression(builder, null, mapNode.getChild(i + 1)); 961 boxIfPrimitive(builder, GenericsUtils.asClass(valueType)); 962 963 //put the value into the array, then pop off the returned object. 964 builder.invoke(HashMapMethods.PUT).pop(); 965 966 } 967 968 return HashMap.class; 969 } 970 971 972 private void implementNoOpSetter() 973 { 974 implementNoOpMethod(ConduitMethods.SET, "Expression '%s' for class %s is read-only.", expression, 975 rootType.getName()); 976 } 977 978 public void implementNoOpMethod(MethodDescription method, String format, Object... arguments) 979 { 980 final String message = String.format(format, arguments); 981 982 plasticClass.introduceMethod(method).changeImplementation(new InstructionBuilderCallback() 983 { 984 public void doBuild(InstructionBuilder builder) 985 { 986 builder.throwException(RuntimeException.class, message); 987 } 988 }); 989 } 990 991 /** 992 * Invokes a method that may take parameters. The children of the invokeNode are subexpressions 993 * to be evaluated, and potentially coerced, so that they may be passed to the method. 994 * 995 * @param builder 996 * constructs code 997 * @param method 998 * method to invoke 999 * @param node 1000 * INVOKE or RANGEOP node 1001 * @param childOffset 1002 * offset within the node to the first child expression (1 in an INVOKE node because the 1003 * first child is the method name, 0 in a RANGEOP node) 1004 */ 1005 private void invokeMethod(InstructionBuilder builder, Method method, Tree node, int childOffset) 1006 { 1007 // We start with the target object for the method on top of the stack. 1008 // Next, we have to push each method parameter, which may include boxing/deboxing 1009 // and coercion. Once the code is in good shape, there's a lot of room to optimize 1010 // the bytecode (a bit too much boxing/deboxing occurs, as well as some unnecessary 1011 // trips through TypeCoercer). We might also want to have a local variable to store 1012 // the root object (result of getRoot()). 1013 1014 Class[] parameterTypes = method.getParameterTypes(); 1015 1016 for (int i = 0; i < parameterTypes.length; i++) 1017 { 1018 Type expressionType = implementSubexpression(builder, null, node.getChild(i + childOffset)); 1019 1020 // The value left on the stack is not primitive, and expressionType represents 1021 // its real type. 1022 1023 Class parameterType = parameterTypes[i]; 1024 1025 if (!parameterType.isAssignableFrom(GenericsUtils.asClass(expressionType))) 1026 { 1027 boxIfPrimitive(builder, expressionType); 1028 1029 builder.loadThis().getField(getDelegateField()); 1030 builder.swap().loadTypeConstant(PlasticUtils.toWrapperType(parameterType)); 1031 builder.invoke(DelegateMethods.COERCE); 1032 1033 if (parameterType.isPrimitive()) 1034 { 1035 builder.castOrUnbox(parameterType.getName()); 1036 } else 1037 { 1038 builder.checkcast(parameterType); 1039 } 1040 } 1041 1042 // And that should leave an object of the correct type on the stack, 1043 // ready for the method invocation. 1044 } 1045 1046 // Now the target object and all parameters are in place. 1047 1048 builder.invoke(method.getDeclaringClass(), method.getReturnType(), method.getName(), 1049 method.getParameterTypes()); 1050 } 1051 1052 /** 1053 * Analyzes a DEREF or SAFEDEREF node, proving back a term that identifies its type and provides a callback to 1054 * peform the dereference. 1055 * 1056 * @return a term indicating the type of the expression to this point, and a {@link InstructionBuilderCallback} 1057 * to advance the evaluation of the expression form the previous value to the current 1058 */ 1059 private Term analyzeDerefNode(Type activeType, Tree node) 1060 { 1061 // The first child is the term. 1062 1063 Tree term = node.getChild(0); 1064 1065 boolean allowNull = node.getType() == SAFEDEREF; 1066 1067 return buildTerm(activeType, term, allowNull ? NullHandling.ALLOW : NullHandling.FORBID); 1068 } 1069 1070 private Term buildTerm(Type activeType, Tree term, final NullHandling nullHandling) 1071 { 1072 assertNodeType(term, IDENTIFIER, INVOKE); 1073 1074 final Term simpleTerm = buildTerm(activeType, term); 1075 1076 if (simpleTerm.genericType.isPrimitive()) 1077 return simpleTerm; 1078 1079 return simpleTerm.withCallback(new InstructionBuilderCallback() 1080 { 1081 public void doBuild(InstructionBuilder builder) 1082 { 1083 simpleTerm.callback.doBuild(builder); 1084 1085 builder.dupe().when(Condition.NULL, new InstructionBuilderCallback() 1086 { 1087 public void doBuild(InstructionBuilder builder) 1088 { 1089 switch (nullHandling) 1090 { 1091 // It is necessary to load a null onto the stack (even if there's already one 1092 // there) because of the verifier. It sees the return when the stack contains an 1093 // intermediate value (along the navigation chain) and thinks the method is 1094 // returning a value of the wrong type. 1095 1096 case ALLOW: 1097 builder.loadNull().returnResult(); 1098 1099 case FORBID: 1100 1101 builder.loadConstant(simpleTerm.description); 1102 builder.loadConstant(expression); 1103 builder.loadArgument(0); 1104 1105 builder.invokeStatic(PropertyConduitSourceImpl.class, NullPointerException.class, 1106 "nullTerm", String.class, String.class, Object.class); 1107 builder.throwException(); 1108 1109 break; 1110 1111 } 1112 } 1113 }); 1114 } 1115 }); 1116 } 1117 1118 private void assertNodeType(Tree node, int... expected) 1119 { 1120 int type = node.getType(); 1121 1122 for (int e : expected) 1123 { 1124 if (type == e) 1125 return; 1126 } 1127 1128 throw unexpectedNodeType(node, expected); 1129 } 1130 1131 private RuntimeException unexpectedNodeType(Tree node, int... expected) 1132 { 1133 List<String> tokenNames = CollectionFactory.newList(); 1134 1135 for (int i = 0; i < expected.length; i++) 1136 tokenNames.add(PropertyExpressionParser.tokenNames[expected[i]]); 1137 1138 String message = String.format("Node %s was type %s, but was expected to be (one of) %s.", 1139 node.toStringTree(), PropertyExpressionParser.tokenNames[node.getType()], 1140 InternalCommonsUtils.joinSorted(tokenNames)); 1141 1142 return new RuntimeException(message); 1143 } 1144 1145 private Term buildTerm(Type activeType, Tree termNode) 1146 { 1147 switch (termNode.getType()) 1148 { 1149 case INVOKE: 1150 1151 return buildInvokeTerm(activeType, termNode); 1152 1153 case IDENTIFIER: 1154 1155 return buildPropertyAccessTerm(activeType, termNode); 1156 1157 default: 1158 throw unexpectedNodeType(termNode, INVOKE, IDENTIFIER); 1159 } 1160 } 1161 1162 private Term buildPropertyAccessTerm(Type activeType, Tree termNode) 1163 { 1164 String propertyName = termNode.getText(); 1165 1166 PropertyAdapter adapter = findPropertyAdapter(activeType, propertyName); 1167 1168 // Prefer the accessor over the field 1169 1170 if (adapter.getReadMethod() != null) 1171 { 1172 return buildGetterMethodAccessTerm(activeType, propertyName, 1173 adapter.getReadMethod()); 1174 } 1175 1176 if (adapter.getField() != null) 1177 { 1178 return buildPublicFieldAccessTerm(activeType, propertyName, 1179 adapter.getField()); 1180 } 1181 1182 throw new RuntimeException(String.format( 1183 "Property '%s' of class %s is not readable (it has no read accessor method).", adapter.getName(), 1184 adapter.getBeanType().getName())); 1185 } 1186 1187 public PropertyAdapter findPropertyAdapter(Type activeType, String propertyName) 1188 { 1189 Class activeClass = GenericsUtils.asClass(activeType); 1190 1191 ClassPropertyAdapter classAdapter = access.getAdapter(activeClass); 1192 PropertyAdapter adapter = classAdapter.getPropertyAdapter(propertyName); 1193 1194 if (adapter == null) 1195 { 1196 final List<String> names = classAdapter.getPropertyNames(); 1197 final String className = activeClass.getName(); 1198 throw new UnknownValueException(String.format( 1199 "Class %s does not contain a property (or public field) named '%s'.", className, propertyName), 1200 new AvailableValues("Properties (and public fields)", names)); 1201 } 1202 return adapter; 1203 } 1204 1205 private Term buildGetterMethodAccessTerm(final Type activeType, String propertyName, final Method readMethod) 1206 { 1207 Type returnType = GenericsUtils.extractActualType(activeType, readMethod); 1208 1209 return new Term(returnType, propertyName, new InstructionBuilderCallback() 1210 { 1211 public void doBuild(InstructionBuilder builder) 1212 { 1213 invokeMethod(builder, readMethod, null, 0); 1214 1215 Type genericType = GenericsUtils.extractActualType(activeType, readMethod); 1216 1217 castToGenericType(builder, readMethod.getReturnType(), genericType); 1218 } 1219 }); 1220 } 1221 1222 private Term buildPublicFieldAccessTerm(Type activeType, String propertyName, final Field field) 1223 { 1224 final Type fieldType = GenericsUtils.extractActualType(activeType, field); 1225 1226 return new Term(fieldType, propertyName, new InstructionBuilderCallback() 1227 { 1228 public void doBuild(InstructionBuilder builder) 1229 { 1230 Class rawFieldType = field.getType(); 1231 1232 String rawTypeName = PlasticUtils.toTypeName(rawFieldType); 1233 String containingClassName = field.getDeclaringClass().getName(); 1234 String fieldName = field.getName(); 1235 1236 if (isStatic(field)) 1237 { 1238 // We've gone to the trouble of loading the root object, or navigated to some other object, 1239 // but we don't need or want the instance, since it's a static field we're accessing. 1240 // Ideally, we would optimize this, and only generate and invoke the getRoot() and nav() methods as needed, but 1241 // access to public fields is relatively rare, and the cost is just the unused bytecode. 1242 1243 builder.pop(); 1244 1245 builder.getStaticField(containingClassName, fieldName, rawTypeName); 1246 1247 } else 1248 { 1249 builder.getField(containingClassName, fieldName, rawTypeName); 1250 } 1251 1252 castToGenericType(builder, rawFieldType, fieldType); 1253 } 1254 1255 }); 1256 } 1257 1258 /** 1259 * Casts the results of a field read or method invocation based on generic information. 1260 * 1261 * @param builder 1262 * used to add instructions 1263 * @param rawType 1264 * the simple type (often Object) of the field (or method return type) 1265 * @param genericType 1266 * the generic Type, from which parameterizations can be determined 1267 */ 1268 private void castToGenericType(InstructionBuilder builder, Class rawType, final Type genericType) 1269 { 1270 if (!genericType.equals(rawType)) 1271 { 1272 Class castType = GenericsUtils.asClass(genericType); 1273 builder.checkcast(castType); 1274 } 1275 } 1276 1277 private Term buildInvokeTerm(final Type activeType, final Tree invokeNode) 1278 { 1279 String methodName = invokeNode.getChild(0).getText(); 1280 1281 int parameterCount = invokeNode.getChildCount() - 1; 1282 1283 Class activeClass = GenericsUtils.asClass(activeType); 1284 1285 final Method method = findMethod(activeClass, methodName, parameterCount); 1286 1287 if (method.getReturnType().equals(void.class)) 1288 throw new RuntimeException(String.format("Method %s.%s() returns void.", activeClass.getName(), 1289 methodName)); 1290 1291 Type returnType = GenericsUtils.extractActualType(activeType, method); 1292 1293 return new Term(returnType, toUniqueId(method), InternalCommonsUtils.toAnnotationProvider(method), new InstructionBuilderCallback() 1294 { 1295 public void doBuild(InstructionBuilder builder) 1296 { 1297 invokeMethod(builder, method, invokeNode, 1); 1298 1299 Type genericType = GenericsUtils.extractActualType(activeType, method); 1300 1301 castToGenericType(builder, method.getReturnType(), genericType); 1302 } 1303 } 1304 ); 1305 } 1306 1307 private Method findMethod(Class activeType, String methodName, int parameterCount) 1308 { 1309 Class searchType = activeType; 1310 1311 while (true) 1312 { 1313 1314 for (Method method : searchType.getMethods()) 1315 { 1316 if (method.getParameterTypes().length == parameterCount 1317 && method.getName().equalsIgnoreCase(methodName)) 1318 return method; 1319 } 1320 1321 // TAP5-330 1322 if (searchType != Object.class) 1323 { 1324 searchType = Object.class; 1325 } else 1326 { 1327 throw new RuntimeException(String.format("Class %s does not contain a public method named '%s()'.", 1328 activeType.getName(), methodName)); 1329 } 1330 } 1331 } 1332 1333 public void boxIfPrimitive(InstructionBuilder builder, Type termType) 1334 { 1335 boxIfPrimitive(builder, GenericsUtils.asClass(termType)); 1336 } 1337 1338 public void boxIfPrimitive(InstructionBuilder builder, Class termType) 1339 { 1340 if (termType.isPrimitive()) 1341 builder.boxPrimitive(termType.getName()); 1342 } 1343 1344 public Class implementNotExpression(InstructionBuilder builder, final Tree notNode) 1345 { 1346 Type expressionType = implementSubexpression(builder, null, notNode.getChild(0)); 1347 1348 boxIfPrimitive(builder, expressionType); 1349 1350 // Now invoke the delegate invert() method 1351 1352 builder.loadThis().getField(getDelegateField()); 1353 1354 builder.swap().invoke(DelegateMethods.INVERT); 1355 1356 return boolean.class; 1357 } 1358 1359 /** 1360 * Defer creation of the delegate field unless actually needed. 1361 */ 1362 private PlasticField getDelegateField() 1363 { 1364 if (delegateField == null) 1365 delegateField = plasticClass.introduceField(PropertyConduitDelegate.class, "delegate").inject( 1366 sharedDelegate); 1367 1368 return delegateField; 1369 } 1370 } 1371 1372 public PropertyConduitSourceImpl(PropertyAccess access, @ComponentLayer 1373 PlasticProxyFactory proxyFactory, TypeCoercer typeCoercer, StringInterner interner) 1374 { 1375 this.access = access; 1376 this.proxyFactory = proxyFactory; 1377 this.typeCoercer = typeCoercer; 1378 this.interner = interner; 1379 1380 literalTrue = createLiteralConduit(Boolean.class, true); 1381 literalFalse = createLiteralConduit(Boolean.class, false); 1382 literalNull = createLiteralConduit(Void.class, null); 1383 1384 sharedDelegate = new PropertyConduitDelegate(typeCoercer); 1385 } 1386 1387 @PostInjection 1388 public void listenForInvalidations(@ComponentClasses InvalidationEventHub hub) 1389 { 1390 hub.clearOnInvalidation(cache); 1391 } 1392 1393 1394 public PropertyConduit create(Class rootClass, String expression) 1395 { 1396 assert rootClass != null; 1397 assert InternalCommonsUtils.isNonBlank(expression); 1398 1399 MultiKey key = new MultiKey(rootClass, expression); 1400 1401 PropertyConduit result = cache.get(key); 1402 1403 if (result == null) 1404 { 1405 result = build(rootClass, expression); 1406 cache.put(key, result); 1407 } 1408 1409 return result; 1410 } 1411 1412 /** 1413 * Builds a subclass of {@link PropertyConduitDelegate} that implements the 1414 * get() and set() methods and overrides the 1415 * constructor. In a worst-case race condition, we may build two (or more) 1416 * conduits for the same 1417 * rootClass/expression, and it will get sorted out when the conduit is 1418 * stored into the cache. 1419 * 1420 * @param rootClass 1421 * class of root object for expression evaluation 1422 * @param expression 1423 * expression to be evaluated 1424 * @return the conduit 1425 */ 1426 private PropertyConduit build(final Class rootClass, String expression) 1427 { 1428 Tree tree = parse(expression); 1429 1430 try 1431 { 1432 switch (tree.getType()) 1433 { 1434 case TRUE: 1435 1436 return literalTrue; 1437 1438 case FALSE: 1439 1440 return literalFalse; 1441 1442 case NULL: 1443 1444 return literalNull; 1445 1446 case INTEGER: 1447 1448 // Leading '+' may screw this up. 1449 // TODO: Singleton instance for "0", maybe "1"? 1450 1451 return createLiteralConduit(Long.class, new Long(tree.getText())); 1452 1453 case DECIMAL: 1454 1455 // Leading '+' may screw this up. 1456 // TODO: Singleton instance for "0.0"? 1457 1458 return createLiteralConduit(Double.class, new Double(tree.getText())); 1459 1460 case STRING: 1461 1462 return createLiteralConduit(String.class, tree.getText()); 1463 1464 case RANGEOP: 1465 1466 Tree fromNode = tree.getChild(0); 1467 Tree toNode = tree.getChild(1); 1468 1469 // If the range is defined as integers (not properties, etc.) 1470 // then it is possible to calculate the value here, once, and not 1471 // build a new class. 1472 1473 if (fromNode.getType() != INTEGER || toNode.getType() != INTEGER) 1474 break; 1475 1476 int from = Integer.parseInt(fromNode.getText()); 1477 int to = Integer.parseInt(toNode.getText()); 1478 1479 IntegerRange ir = new IntegerRange(from, to); 1480 1481 return createLiteralConduit(IntegerRange.class, ir); 1482 1483 case THIS: 1484 1485 return createLiteralThisPropertyConduit(rootClass); 1486 1487 default: 1488 break; 1489 } 1490 1491 return proxyFactory.createProxy(InternalPropertyConduit.class, 1492 new PropertyConduitBuilder(rootClass, expression, tree)).newInstance(); 1493 } catch (Exception ex) 1494 { 1495 throw new PropertyExpressionException(String.format("Exception generating conduit for expression '%s': %s", 1496 expression, ExceptionUtils.toMessage(ex)), expression, ex); 1497 } 1498 } 1499 1500 private PropertyConduit createLiteralThisPropertyConduit(final Class rootClass) 1501 { 1502 return new PropertyConduit() 1503 { 1504 public Object get(Object instance) 1505 { 1506 return instance; 1507 } 1508 1509 public void set(Object instance, Object value) 1510 { 1511 throw new RuntimeException("Literal values are not updateable."); 1512 } 1513 1514 public Class getPropertyType() 1515 { 1516 return rootClass; 1517 } 1518 1519 public Type getPropertyGenericType() 1520 { 1521 return rootClass; 1522 } 1523 1524 public <T extends Annotation> T getAnnotation(Class<T> annotationClass) 1525 { 1526 return invariantAnnotationProvider.getAnnotation(annotationClass); 1527 } 1528 }; 1529 } 1530 1531 private <T> PropertyConduit createLiteralConduit(Class<T> type, T value) 1532 { 1533 return new LiteralPropertyConduit(typeCoercer, type, invariantAnnotationProvider, interner.format( 1534 "LiteralPropertyConduit[%s]", value), value); 1535 } 1536 1537 private Tree parse(String expression) 1538 { 1539 InputStream is = new ByteArrayInputStream(expression.getBytes()); 1540 1541 ANTLRInputStream ais; 1542 1543 try 1544 { 1545 ais = new ANTLRInputStream(is); 1546 } catch (IOException ex) 1547 { 1548 throw new RuntimeException(ex); 1549 } 1550 1551 PropertyExpressionLexer lexer = new PropertyExpressionLexer(ais); 1552 1553 CommonTokenStream tokens = new CommonTokenStream(lexer); 1554 1555 PropertyExpressionParser parser = new PropertyExpressionParser(tokens); 1556 1557 try 1558 { 1559 return (Tree) parser.start().getTree(); 1560 } catch (Exception ex) 1561 { 1562 throw new RuntimeException(String.format("Error parsing property expression '%s': %s.", expression, 1563 ex.getMessage()), ex); 1564 } 1565 } 1566 1567 /** 1568 * May be invoked from fabricated PropertyConduit instances. 1569 */ 1570 @SuppressWarnings("unused") 1571 public static NullPointerException nullTerm(String term, String expression, Object root) 1572 { 1573 String message = String.format("Property '%s' (within property expression '%s', of %s) is null.", term, 1574 expression, root); 1575 1576 return new NullPointerException(message); 1577 } 1578 1579 private static String toUniqueId(Method method) 1580 { 1581 StringBuilder builder = new StringBuilder(method.getName()).append('('); 1582 String sep = ""; 1583 1584 for (Class parameterType : method.getParameterTypes()) 1585 { 1586 builder.append(sep); 1587 builder.append(PlasticUtils.toTypeName(parameterType)); 1588 1589 sep = ","; 1590 } 1591 1592 return builder.append(')').toString(); 1593 } 1594 1595}