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    }