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 }