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