    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
    015package org.apache.tapestry5.internal.transform;
    016
    017import org.apache.tapestry5.MarkupWriter;
    018import org.apache.tapestry5.annotations.*;
    019import org.apache.tapestry5.func.F;
    020import org.apache.tapestry5.func.Flow;
    021import org.apache.tapestry5.func.Predicate;
    022import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    023import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    024import org.apache.tapestry5.model.MutableComponentModel;
    025import org.apache.tapestry5.plastic.*;
    026import org.apache.tapestry5.runtime.Event;
    027import org.apache.tapestry5.services.TransformConstants;
    028import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
    029import org.apache.tapestry5.services.transform.TransformationSupport;
    030
    031import java.lang.annotation.Annotation;
    032import java.util.List;
    033import java.util.Map;
    034import java.util.Map.Entry;
    035import 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")
    044public 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}