001 //
002 // Copyright 2006, 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
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.ComponentResources;
018 import org.apache.tapestry5.EventContext;
019 import org.apache.tapestry5.SymbolConstants;
020 import org.apache.tapestry5.ValueEncoder;
021 import org.apache.tapestry5.annotations.OnEvent;
022 import org.apache.tapestry5.annotations.RequestParameter;
023 import org.apache.tapestry5.func.F;
024 import org.apache.tapestry5.func.Flow;
025 import org.apache.tapestry5.func.Mapper;
026 import org.apache.tapestry5.func.Predicate;
027 import org.apache.tapestry5.internal.services.ComponentClassCache;
028 import org.apache.tapestry5.ioc.OperationTracker;
029 import org.apache.tapestry5.ioc.annotations.Symbol;
030 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
031 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
032 import org.apache.tapestry5.ioc.internal.util.TapestryException;
033 import org.apache.tapestry5.ioc.util.UnknownValueException;
034 import org.apache.tapestry5.model.MutableComponentModel;
035 import org.apache.tapestry5.plastic.*;
036 import org.apache.tapestry5.runtime.ComponentEvent;
037 import org.apache.tapestry5.runtime.Event;
038 import org.apache.tapestry5.runtime.PageLifecycleListener;
039 import org.apache.tapestry5.services.Request;
040 import org.apache.tapestry5.services.TransformConstants;
041 import org.apache.tapestry5.services.ValueEncoderSource;
042 import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
043 import org.apache.tapestry5.services.transform.TransformationSupport;
044
045 import java.util.Arrays;
046 import java.util.List;
047 import java.util.Map;
048
049 /**
050 * Provides implementations of the
051 * {@link org.apache.tapestry5.runtime.Component#dispatchComponentEvent(org.apache.tapestry5.runtime.ComponentEvent)}
052 * method, based on {@link org.apache.tapestry5.annotations.OnEvent} annotations and naming conventions.
053 */
054 public class OnEventWorker implements ComponentClassTransformWorker2
055 {
056 private final Request request;
057
058 private final ValueEncoderSource valueEncoderSource;
059
060 private final ComponentClassCache classCache;
061
062 private final OperationTracker operationTracker;
063
064 private final boolean componentIdCheck;
065
066 private final InstructionBuilderCallback RETURN_TRUE = new InstructionBuilderCallback()
067 {
068 public void doBuild(InstructionBuilder builder)
069 {
070 builder.loadConstant(true).returnResult();
071 }
072 };
073
074 class ComponentIdValidator
075 {
076 final String componentId;
077
078 final String methodIdentifier;
079
080 ComponentIdValidator(String componentId, String methodIdentifier)
081 {
082 this.componentId = componentId;
083 this.methodIdentifier = methodIdentifier;
084 }
085
086 void validate(ComponentResources resources)
087 {
088 try
089 {
090 resources.getEmbeddedComponent(componentId);
091 } catch (UnknownValueException ex)
092 {
093 throw new TapestryException(String.format("Method %s references component id '%s' which does not exist.",
094 methodIdentifier, componentId), resources.getLocation(), ex);
095 }
096 }
097 }
098
099 class ValidateComponentIds implements MethodAdvice
100 {
101 final ComponentIdValidator[] validators;
102
103 ValidateComponentIds(ComponentIdValidator[] validators)
104 {
105 this.validators = validators;
106 }
107
108 public void advise(MethodInvocation invocation)
109 {
110 ComponentResources resources = invocation.getInstanceContext().get(ComponentResources.class);
111
112 for (ComponentIdValidator validator : validators)
113 {
114 validator.validate(resources);
115 }
116
117 invocation.proceed();
118 }
119 }
120
121 /**
122 * Encapsulates information needed to invoke a method as an event handler method, including the logic
123 * to construct parameter values, and match the method against the {@link ComponentEvent}.
124 */
125 class EventHandlerMethod
126 {
127 final PlasticMethod method;
128
129 final MethodDescription description;
130
131 final String eventType, componentId;
132
133 final EventHandlerMethodParameterSource parameterSource;
134
135 int minContextValues = 0;
136
137 EventHandlerMethod(PlasticMethod method)
138 {
139 this.method = method;
140 description = method.getDescription();
141
142 parameterSource = buildSource();
143
144 String methodName = method.getDescription().methodName;
145
146 OnEvent onEvent = method.getAnnotation(OnEvent.class);
147
148 eventType = extractEventType(methodName, onEvent);
149 componentId = extractComponentId(methodName, onEvent);
150 }
151
152 void buildMatchAndInvocation(InstructionBuilder builder, final LocalVariable resultVariable)
153 {
154 final PlasticField sourceField =
155 parameterSource == null ? null
156 : method.getPlasticClass().introduceField(EventHandlerMethodParameterSource.class, description.methodName + "$parameterSource").inject(parameterSource);
157
158 builder.loadArgument(0).loadConstant(eventType).loadConstant(componentId).loadConstant(minContextValues);
159 builder.invoke(ComponentEvent.class, boolean.class, "matches", String.class, String.class, int.class);
160
161 builder.when(Condition.NON_ZERO, new InstructionBuilderCallback()
162 {
163 public void doBuild(InstructionBuilder builder)
164 {
165 builder.loadArgument(0).loadConstant(method.getMethodIdentifier()).invoke(Event.class, void.class, "setMethodDescription", String.class);
166
167 builder.loadThis();
168
169 int count = description.argumentTypes.length;
170
171 for (int i = 0; i < count; i++)
172 {
173 builder.loadThis().getField(sourceField).loadArgument(0).loadConstant(i);
174
175 builder.invoke(EventHandlerMethodParameterSource.class, Object.class, "get",
176 ComponentEvent.class, int.class);
177
178 builder.castOrUnbox(description.argumentTypes[i]);
179 }
180
181 builder.invokeVirtual(method);
182
183 if (!method.isVoid())
184 {
185 builder.boxPrimitive(description.returnType);
186 builder.loadArgument(0).swap();
187
188 builder.invoke(Event.class, boolean.class, "storeResult", Object.class);
189
190 // storeResult() returns true if the method is aborted. Return true since, certainly,
191 // a method was invoked.
192 builder.when(Condition.NON_ZERO, RETURN_TRUE);
193 }
194
195 // Set the result to true, to indicate that some method was invoked.
196
197 builder.loadConstant(true).storeVariable(resultVariable);
198 }
199 });
200 }
201
202
203 private EventHandlerMethodParameterSource buildSource()
204 {
205 final String[] parameterTypes = method.getDescription().argumentTypes;
206
207 if (parameterTypes.length == 0)
208 {
209 return null;
210 }
211
212 final List<EventHandlerMethodParameterProvider> providers = CollectionFactory.newList();
213
214 int contextIndex = 0;
215
216 for (int i = 0; i < parameterTypes.length; i++)
217 {
218 String type = parameterTypes[i];
219
220 EventHandlerMethodParameterProvider provider = parameterTypeToProvider.get(type);
221
222 if (provider != null)
223 {
224 providers.add(provider);
225 continue;
226 }
227
228 RequestParameter parameterAnnotation = method.getParameters().get(i).getAnnotation(RequestParameter.class);
229
230 if (parameterAnnotation != null)
231 {
232 String parameterName = parameterAnnotation.value();
233
234 providers.add(createQueryParameterProvider(method, i, parameterName, type,
235 parameterAnnotation.allowBlank()));
236 continue;
237 }
238
239 // Note: probably safe to do the conversion to Class early (class load time)
240 // as parameters are rarely (if ever) component classes.
241
242 providers.add(createEventContextProvider(type, contextIndex++));
243 }
244
245
246 minContextValues = contextIndex;
247
248 EventHandlerMethodParameterProvider[] providerArray = providers.toArray(new EventHandlerMethodParameterProvider[providers.size()]);
249
250 return new EventHandlerMethodParameterSource(method.getMethodIdentifier(), operationTracker, providerArray);
251 }
252 }
253
254
255 /**
256 * Stores a couple of special parameter type mappings that are used when matching the entire event context
257 * (either as Object[] or EventContext).
258 */
259 private final Map<String, EventHandlerMethodParameterProvider> parameterTypeToProvider = CollectionFactory.newMap();
260
261 {
262 // Object[] and List are out-dated and may be deprecated some day
263
264 parameterTypeToProvider.put("java.lang.Object[]", new EventHandlerMethodParameterProvider()
265 {
266
267 public Object valueForEventHandlerMethodParameter(ComponentEvent event)
268 {
269 return event.getContext();
270 }
271 });
272
273 parameterTypeToProvider.put(List.class.getName(), new EventHandlerMethodParameterProvider()
274 {
275
276 public Object valueForEventHandlerMethodParameter(ComponentEvent event)
277 {
278 return Arrays.asList(event.getContext());
279 }
280 });
281
282 // This is better, as the EventContext maintains the original objects (or strings)
283 // and gives the event handler method access with coercion
284 parameterTypeToProvider.put(EventContext.class.getName(), new EventHandlerMethodParameterProvider()
285 {
286 public Object valueForEventHandlerMethodParameter(ComponentEvent event)
287 {
288 return event.getEventContext();
289 }
290 });
291 }
292
293 public OnEventWorker(Request request, ValueEncoderSource valueEncoderSource, ComponentClassCache classCache, OperationTracker operationTracker,
294
295 @Symbol(SymbolConstants.UNKNOWN_COMPONENT_ID_CHECK_ENABLED)
296 boolean componentIdCheck)
297 {
298 this.request = request;
299 this.valueEncoderSource = valueEncoderSource;
300 this.classCache = classCache;
301 this.operationTracker = operationTracker;
302 this.componentIdCheck = componentIdCheck;
303 }
304
305 public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model)
306 {
307 Flow<PlasticMethod> methods = matchEventHandlerMethods(plasticClass);
308
309 if (methods.isEmpty())
310 {
311 return;
312 }
313
314 addEventHandlingLogic(plasticClass, support.isRootTransformation(), methods, model);
315 }
316
317
318 private void addEventHandlingLogic(final PlasticClass plasticClass, final boolean isRoot, final Flow<PlasticMethod> plasticMethods, final MutableComponentModel model)
319 {
320 Flow<EventHandlerMethod> eventHandlerMethods = plasticMethods.map(new Mapper<PlasticMethod, EventHandlerMethod>()
321 {
322 public EventHandlerMethod map(PlasticMethod element)
323 {
324 return new EventHandlerMethod(element);
325 }
326 });
327
328 implementDispatchMethod(plasticClass, isRoot, model, eventHandlerMethods);
329
330 addComponentIdValidationLogicOnPageLoad(plasticClass, eventHandlerMethods);
331 }
332
333 private void addComponentIdValidationLogicOnPageLoad(PlasticClass plasticClass, Flow<EventHandlerMethod> eventHandlerMethods)
334 {
335 if (componentIdCheck)
336 {
337 ComponentIdValidator[] validators = extractComponentIdValidators(eventHandlerMethods);
338
339 if (validators.length > 0)
340 {
341 plasticClass.introduceInterface(PageLifecycleListener.class);
342 plasticClass.introduceMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_DESCRIPTION).addAdvice(new ValidateComponentIds(validators));
343 }
344 }
345 }
346
347 private ComponentIdValidator[] extractComponentIdValidators(Flow<EventHandlerMethod> eventHandlerMethods)
348 {
349 return eventHandlerMethods.map(new Mapper<EventHandlerMethod, ComponentIdValidator>()
350 {
351 public ComponentIdValidator map(EventHandlerMethod element)
352 {
353 if (element.componentId.equals(""))
354 {
355 return null;
356 }
357
358 return new ComponentIdValidator(element.componentId, element.method.getMethodIdentifier());
359 }
360 }).removeNulls().toArray(ComponentIdValidator.class);
361 }
362
363 private void implementDispatchMethod(final PlasticClass plasticClass, final boolean isRoot, final MutableComponentModel model, final Flow<EventHandlerMethod> eventHandlerMethods)
364 {
365 plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION).changeImplementation(new InstructionBuilderCallback()
366 {
367 public void doBuild(InstructionBuilder builder)
368 {
369 builder.startVariable("boolean", new LocalVariableCallback()
370 {
371 public void doBuild(LocalVariable resultVariable, InstructionBuilder builder)
372 {
373 if (!isRoot)
374 {
375 // As a subclass, there will be a base class implementation (possibly empty).
376
377 builder.loadThis().loadArguments().invokeSpecial(plasticClass.getSuperClassName(), TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION);
378
379 // First store the result of the super() call into the variable.
380 builder.storeVariable(resultVariable);
381 builder.loadArgument(0).invoke(Event.class, boolean.class, "isAborted");
382 builder.when(Condition.NON_ZERO, RETURN_TRUE);
383 } else
384 {
385 // No event handler method has yet been invoked.
386 builder.loadConstant(false).storeVariable(resultVariable);
387 }
388
389 for (EventHandlerMethod method : eventHandlerMethods)
390 {
391 method.buildMatchAndInvocation(builder, resultVariable);
392
393 model.addEventHandler(method.eventType);
394 }
395
396 builder.loadVariable(resultVariable).returnResult();
397 }
398 });
399 }
400 });
401 }
402
403 private Flow<PlasticMethod> matchEventHandlerMethods(PlasticClass plasticClass)
404 {
405 return F.flow(plasticClass.getMethods()).filter(new Predicate<PlasticMethod>()
406 {
407 public boolean accept(PlasticMethod method)
408 {
409 return (hasCorrectPrefix(method) || hasAnnotation(method)) && !method.isOverride();
410 }
411
412 private boolean hasCorrectPrefix(PlasticMethod method)
413 {
414 return method.getDescription().methodName.startsWith("on");
415 }
416
417 private boolean hasAnnotation(PlasticMethod method)
418 {
419 return method.hasAnnotation(OnEvent.class);
420 }
421 });
422 }
423
424
425 private EventHandlerMethodParameterProvider createQueryParameterProvider(PlasticMethod method, final int parameterIndex, final String parameterName,
426 final String parameterTypeName, final boolean allowBlank)
427 {
428 final String methodIdentifier = method.getMethodIdentifier();
429
430 return new EventHandlerMethodParameterProvider()
431 {
432 @SuppressWarnings("unchecked")
433 public Object valueForEventHandlerMethodParameter(ComponentEvent event)
434 {
435 try
436 {
437 String parameterValue = request.getParameter(parameterName);
438
439 if (!allowBlank && InternalUtils.isBlank(parameterValue))
440 throw new RuntimeException(String.format(
441 "The value for query parameter '%s' was blank, but a non-blank value is needed.",
442 parameterName));
443
444 Class parameterType = classCache.forName(parameterTypeName);
445
446 ValueEncoder valueEncoder = valueEncoderSource.getValueEncoder(parameterType);
447
448 Object value = valueEncoder.toValue(parameterValue);
449
450 if (parameterType.isPrimitive() && value == null)
451 throw new RuntimeException(
452 String.format(
453 "Query parameter '%s' evaluates to null, but the event method parameter is type %s, a primitive.",
454 parameterName, parameterType.getName()));
455
456 return value;
457 } catch (Exception ex)
458 {
459 throw new RuntimeException(
460 String.format(
461 "Unable process query parameter '%s' as parameter #%d of event handler method %s: %s",
462 parameterName, parameterIndex + 1, methodIdentifier,
463 InternalUtils.toMessage(ex)), ex);
464 }
465 }
466 };
467 }
468
469 private EventHandlerMethodParameterProvider createEventContextProvider(final String type, final int parameterIndex)
470 {
471 return new EventHandlerMethodParameterProvider()
472 {
473 public Object valueForEventHandlerMethodParameter(ComponentEvent event)
474 {
475 return event.coerceContext(parameterIndex, type);
476 }
477 };
478 }
479
480 /**
481 * Returns the component id to match against, or the empty
482 * string if the component id is not specified. The component id
483 * is provided by the OnEvent annotation or (if that is not present)
484 * by the part of the method name following "From" ("onActionFromFoo").
485 */
486 private String extractComponentId(String methodName, OnEvent annotation)
487 {
488 if (annotation != null)
489 return annotation.component();
490
491 // Method name started with "on". Extract the component id, if present.
492
493 int fromx = methodName.indexOf("From");
494
495 if (fromx < 0)
496 return "";
497
498 return methodName.substring(fromx + 4);
499 }
500
501 /**
502 * Returns the event name to match against, as specified in the annotation
503 * or (if the annotation is not present) extracted from the name of the method.
504 * "onActionFromFoo" or just "onAction".
505 */
506 private String extractEventType(String methodName, OnEvent annotation)
507 {
508 if (annotation != null)
509 return annotation.value();
510
511 int fromx = methodName.indexOf("From");
512
513 // The first two characters are always "on" as in "onActionFromFoo".
514 return fromx == -1 ? methodName.substring(2) : methodName.substring(2, fromx);
515 }
516 }