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