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