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