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}