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}