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}