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