    001    // Copyright 2006, 2007, 2008, 2010 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
    015    package org.apache.tapestry5.internal.transform;
    016
    017    import org.apache.tapestry5.MarkupWriter;
    018    import org.apache.tapestry5.annotations.*;
    019    import org.apache.tapestry5.func.F;
    020    import org.apache.tapestry5.func.Flow;
    021    import org.apache.tapestry5.func.Predicate;
    022    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    023    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    024    import org.apache.tapestry5.model.MutableComponentModel;
    025    import org.apache.tapestry5.plastic.*;
    026    import org.apache.tapestry5.runtime.Event;
    027    import org.apache.tapestry5.services.TransformConstants;
    028    import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
    029    import org.apache.tapestry5.services.transform.TransformationSupport;
    030
    031    import java.lang.annotation.Annotation;
    032    import java.util.List;
    033    import java.util.Map;
    034    import java.util.Map.Entry;
    035    import java.util.Set;
    036
    037    /**
    038     * Converts one of the methods of {@link org.apache.tapestry5.runtime.Component} into a chain of
    039     * command that, itself,
    040     * invokes certain methods (render phase methods) marked with an annotation, or named in a specific
    041     * way.
    042     */
    043    @SuppressWarnings("all")
    044    public class RenderPhaseMethodWorker implements ComponentClassTransformWorker2
    045    {
    046        private final Map<Class<? extends Annotation>, MethodDescription> annotationToDescription = CollectionFactory.newMap();
    047
    048        private final Map<String, Class<? extends Annotation>> nameToAnnotation = CollectionFactory.newCaseInsensitiveMap();
    049
    050        private final Set<Class<? extends Annotation>> reverseAnnotations = CollectionFactory.newSet(AfterRenderBody.class,
    051                AfterRenderTemplate.class, AfterRender.class, CleanupRender.class);
    052
    053        private final Set<MethodDescription> lifecycleMethods = CollectionFactory.newSet();
    054
    055        {
    056
    057            annotationToDescription.put(SetupRender.class, TransformConstants.SETUP_RENDER_DESCRIPTION);
    058            annotationToDescription.put(BeginRender.class, TransformConstants.BEGIN_RENDER_DESCRIPTION);
    059            annotationToDescription.put(BeforeRenderTemplate.class, TransformConstants.BEFORE_RENDER_TEMPLATE_DESCRIPTION);
    060            annotationToDescription.put(BeforeRenderBody.class, TransformConstants.BEFORE_RENDER_BODY_DESCRIPTION);
    061            annotationToDescription.put(AfterRenderBody.class, TransformConstants.AFTER_RENDER_BODY_DESCRIPTION);
    062            annotationToDescription.put(AfterRenderTemplate.class, TransformConstants.AFTER_RENDER_TEMPLATE_DESCRIPTION);
    063            annotationToDescription.put(AfterRender.class, TransformConstants.AFTER_RENDER_DESCRIPTION);
    064            annotationToDescription.put(CleanupRender.class, TransformConstants.CLEANUP_RENDER_DESCRIPTION);
    065
    066            for (Entry<Class<? extends Annotation>, MethodDescription> me : annotationToDescription.entrySet())
    067            {
    068                nameToAnnotation.put(me.getValue().methodName, me.getKey());
    069                lifecycleMethods.add(me.getValue());
    070            }
    071
    072        }
    073
    074
    075        private InstructionBuilderCallback JUST_RETURN = new InstructionBuilderCallback()
    076        {
    077            public void doBuild(InstructionBuilder builder)
    078            {
    079                builder.returnDefaultValue();
    080            }
    081        };
    082
    083        public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model)
    084        {
    085            Map<Class, List<PlasticMethod>> methods = mapRenderPhaseAnnotationToMethods(plasticClass);
    086
    087            for (Class renderPhaseAnnotation : methods.keySet())
    088            {
    089                mapMethodsToRenderPhase(plasticClass, support.isRootTransformation(), renderPhaseAnnotation, methods.get(renderPhaseAnnotation));
    090
    091                model.addRenderPhase(renderPhaseAnnotation);
    092            }
    093        }
    094
    095        private void mapMethodsToRenderPhase(final PlasticClass plasticClass, final boolean isRoot, Class annotationType, List<PlasticMethod> methods)
    096        {
    097
    098            // The method, defined by Component, that will in turn invoke the other methods.
    099
    100            final MethodDescription interfaceMethodDescription = annotationToDescription.get(annotationType);
    101            PlasticMethod interfaceMethod = plasticClass.introduceMethod(interfaceMethodDescription);
    102
    103            final boolean reverse = reverseAnnotations.contains(annotationType);
    104
    105            final Flow<PlasticMethod> orderedMethods =
    106                    reverse ? F.flow(methods).reverse()
    107                            : F.flow(methods);
    108
    109            // You'd think we'd need to catch non-RuntimeExceptions thrown by invoked methods ... turns out
    110            // that the distinction between checked and non-checked exception is a concern only of the Java compiler,
    111            // not the runtime or JVM. This did require a small change to ComponentPageElementImpl, to catch Exception (previously
    112            // it caught RuntimeException).
    113            interfaceMethod.changeImplementation(new InstructionBuilderCallback()
    114            {
    115                private void addSuperCall(InstructionBuilder builder)
    116                {
    117                    builder.loadThis().loadArguments().invokeSpecial(plasticClass.getSuperClassName(), interfaceMethodDescription);
    118                }
    119
    120                private void invokeMethod(InstructionBuilder builder, PlasticMethod method)
    121                {
    122                    // First, tell the Event object what method is being invoked.
    123
    124                    builder.loadArgument(1);
    125                    builder.loadConstant( method.getMethodIdentifier());
    126                    builder.invoke(Event.class, void.class, "setMethodDescription", String.class);
    127
    128                    builder.loadThis();
    129
    130                    // Methods either take no parameters, or take a MarkupWriter parameter.
    131
    132                    if (method.getParameters().size() > 0)
    133                    {
    134                        builder.loadArgument(0);
    135                    }
    136
    137                    builder.invokeVirtual(method);
    138
    139                    // Non-void methods will pass a value to the event.
    140
    141                    if (!method.isVoid())
    142                    {
    143                        builder.boxPrimitive(method.getDescription().returnType);
    144                        builder.loadArgument(1).swap();
    145
    146                        builder.invoke(Event.class, boolean.class, "storeResult", Object.class);
    147
    148                        builder.when(Condition.NON_ZERO, JUST_RETURN);
    149                    }
    150                }
    151
    152                public void doBuild(InstructionBuilder builder)
    153                {
    154                    if (!reverse && !isRoot)
    155                    {
    156                        addSuperCall(builder);
    157
    158                        builder.loadArgument(1).invoke(Event.class, boolean.class, "isAborted");
    159
    160                        builder.when(Condition.NON_ZERO, JUST_RETURN);
    161                    }
    162
    163                    for (PlasticMethod invokedMethod : orderedMethods)
    164                    {
    165                        invokeMethod(builder, invokedMethod);
    166                    }
    167
    168                    if (reverse && !isRoot)
    169                    {
    170                        addSuperCall(builder);
    171                    }
    172
    173                    builder.returnDefaultValue();
    174                }
    175            });
    176        }
    177
    178
    179        private Map<Class, List<PlasticMethod>> mapRenderPhaseAnnotationToMethods(PlasticClass plasticClass)
    180        {
    181            Map<Class, List<PlasticMethod>> map = CollectionFactory.newMap();
    182
    183            Flow<PlasticMethod> matches = matchAllMethodsNotOverriddenFromBaseClass(plasticClass);
    184
    185            for (PlasticMethod method : matches)
    186            {
    187                addMethodToRenderPhaseCategoryMap(map, method);
    188            }
    189
    190            return map;
    191        }
    192
    193
    194        private void addMethodToRenderPhaseCategoryMap(Map<Class, List<PlasticMethod>> map, PlasticMethod method)
    195        {
    196            Class categorized = categorizeMethod(method);
    197
    198            if (categorized != null)
    199            {
    200                validateAsRenderPhaseMethod(method);
    201
    202                InternalUtils.addToMapList(map, categorized, method);
    203            }
    204        }
    205
    206
    207        private Class categorizeMethod(PlasticMethod method)
    208        {
    209            for (Class annotationClass : annotationToDescription.keySet())
    210            {
    211                if (method.hasAnnotation(annotationClass))
    212                    return annotationClass;
    213            }
    214
    215            return nameToAnnotation.get(method.getDescription().methodName);
    216        }
    217
    218        private void validateAsRenderPhaseMethod(PlasticMethod method)
    219        {
    220            final String[] argumentTypes = method.getDescription().argumentTypes;
    221
    222            switch (argumentTypes.length)
    223            {
    224                case 0:
    225                    break;
    226
    227                case 1:
    228                    if (argumentTypes[0].equals(MarkupWriter.class.getName()))
    229                        break;
    230                default:
    231                    throw new RuntimeException(
    232                            String.format(
    233                                    "Method %s is not a valid render phase method: it should take no parameters, or take a single parameter of type MarkupWriter.",
    234                                    method.toString()));
    235            }
    236        }
    237
    238
    239        private Flow<PlasticMethod> matchAllMethodsNotOverriddenFromBaseClass(final PlasticClass plasticClass)
    240        {
    241            return F.flow(plasticClass.getMethods()).filter(new Predicate<PlasticMethod>()
    242            {
    243                public boolean accept(PlasticMethod method)
    244                {
    245                    return !method.isOverride() && !lifecycleMethods.contains(method.getDescription());
    246                }
    247            });
    248        }
    249    }