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 class ComponentIdValidator 073 { 074 final String componentId; 075 076 final String methodIdentifier; 077 078 ComponentIdValidator(String componentId, String methodIdentifier) 079 { 080 this.componentId = componentId; 081 this.methodIdentifier = methodIdentifier; 082 } 083 084 void validate(ComponentResources resources) 085 { 086 try 087 { 088 resources.getEmbeddedComponent(componentId); 089 } catch (UnknownValueException ex) 090 { 091 throw new TapestryException(String.format("Method %s references component id '%s' which does not exist.", 092 methodIdentifier, componentId), resources.getLocation(), ex); 093 } 094 } 095 } 096 097 class ValidateComponentIds implements MethodAdvice 098 { 099 final ComponentIdValidator[] validators; 100 101 ValidateComponentIds(ComponentIdValidator[] validators) 102 { 103 this.validators = validators; 104 } 105 106 public void advise(MethodInvocation invocation) 107 { 108 ComponentResources resources = invocation.getInstanceContext().get(ComponentResources.class); 109 110 for (ComponentIdValidator validator : validators) 111 { 112 validator.validate(resources); 113 } 114 115 invocation.proceed(); 116 } 117 } 118 119 /** 120 * Encapsulates information needed to invoke a method as an event handler method, including the logic 121 * to construct parameter values, and match the method against the {@link ComponentEvent}. 122 */ 123 class EventHandlerMethod 124 { 125 final PlasticMethod method; 126 127 final MethodDescription description; 128 129 final String eventType, componentId; 130 131 final EventHandlerMethodParameterSource parameterSource; 132 133 int minContextValues = 0; 134 135 boolean handleActivationEventContext = false; 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 this.handleActivationEventContext = true; 226 continue; 227 } 228 229 RequestParameter parameterAnnotation = method.getParameters().get(i).getAnnotation(RequestParameter.class); 230 231 if (parameterAnnotation != null) 232 { 233 String parameterName = parameterAnnotation.value(); 234 235 providers.add(createQueryParameterProvider(method, i, parameterName, type, 236 parameterAnnotation.allowBlank())); 237 continue; 238 } 239 240 // Note: probably safe to do the conversion to Class early (class load time) 241 // as parameters are rarely (if ever) component classes. 242 243 providers.add(createEventContextProvider(type, contextIndex++)); 244 } 245 246 247 minContextValues = contextIndex; 248 249 EventHandlerMethodParameterProvider[] providerArray = providers.toArray(new EventHandlerMethodParameterProvider[providers.size()]); 250 251 return new EventHandlerMethodParameterSource(method.getMethodIdentifier(), operationTracker, providerArray); 252 } 253 } 254 255 256 /** 257 * Stores a couple of special parameter type mappings that are used when matching the entire event context 258 * (either as Object[] or EventContext). 259 */ 260 private final Map<String, EventHandlerMethodParameterProvider> parameterTypeToProvider = CollectionFactory.newMap(); 261 262 { 263 // Object[] and List are out-dated and may be deprecated some day 264 265 parameterTypeToProvider.put("java.lang.Object[]", new EventHandlerMethodParameterProvider() 266 { 267 268 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 269 { 270 return event.getContext(); 271 } 272 }); 273 274 parameterTypeToProvider.put(List.class.getName(), new EventHandlerMethodParameterProvider() 275 { 276 277 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 278 { 279 return Arrays.asList(event.getContext()); 280 } 281 }); 282 283 // This is better, as the EventContext maintains the original objects (or strings) 284 // and gives the event handler method access with coercion 285 parameterTypeToProvider.put(EventContext.class.getName(), new EventHandlerMethodParameterProvider() 286 { 287 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 288 { 289 return event.getEventContext(); 290 } 291 }); 292 } 293 294 public OnEventWorker(Request request, ValueEncoderSource valueEncoderSource, ComponentClassCache classCache, OperationTracker operationTracker) 295 { 296 this.request = request; 297 this.valueEncoderSource = valueEncoderSource; 298 this.classCache = classCache; 299 this.operationTracker = operationTracker; 300 } 301 302 public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) 303 { 304 Flow<PlasticMethod> methods = matchEventHandlerMethods(plasticClass); 305 306 if (methods.isEmpty()) 307 { 308 return; 309 } 310 311 addEventHandlingLogic(plasticClass, support.isRootTransformation(), methods, model); 312 } 313 314 315 private void addEventHandlingLogic(final PlasticClass plasticClass, final boolean isRoot, final Flow<PlasticMethod> plasticMethods, final MutableComponentModel model) 316 { 317 Flow<EventHandlerMethod> eventHandlerMethods = plasticMethods.map(new Mapper<PlasticMethod, EventHandlerMethod>() 318 { 319 public EventHandlerMethod map(PlasticMethod element) 320 { 321 return new EventHandlerMethod(element); 322 } 323 }); 324 325 implementDispatchMethod(plasticClass, isRoot, model, eventHandlerMethods); 326 327 addComponentIdValidationLogicOnPageLoad(plasticClass, eventHandlerMethods); 328 } 329 330 private void addComponentIdValidationLogicOnPageLoad(PlasticClass plasticClass, Flow<EventHandlerMethod> eventHandlerMethods) 331 { 332 ComponentIdValidator[] validators = extractComponentIdValidators(eventHandlerMethods); 333 334 if (validators.length > 0) 335 { 336 plasticClass.introduceInterface(PageLifecycleListener.class); 337 plasticClass.introduceMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_DESCRIPTION).addAdvice(new ValidateComponentIds(validators)); 338 } 339 } 340 341 private ComponentIdValidator[] extractComponentIdValidators(Flow<EventHandlerMethod> eventHandlerMethods) 342 { 343 return eventHandlerMethods.map(new Mapper<EventHandlerMethod, ComponentIdValidator>() 344 { 345 public ComponentIdValidator map(EventHandlerMethod element) 346 { 347 if (element.componentId.equals("")) 348 { 349 return null; 350 } 351 352 return new ComponentIdValidator(element.componentId, element.method.getMethodIdentifier()); 353 } 354 }).removeNulls().toArray(ComponentIdValidator.class); 355 } 356 357 private void implementDispatchMethod(final PlasticClass plasticClass, final boolean isRoot, final MutableComponentModel model, final Flow<EventHandlerMethod> eventHandlerMethods) 358 { 359 plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION).changeImplementation(new InstructionBuilderCallback() 360 { 361 public void doBuild(InstructionBuilder builder) 362 { 363 builder.startVariable("boolean", new LocalVariableCallback() 364 { 365 public void doBuild(LocalVariable resultVariable, InstructionBuilder builder) 366 { 367 if (!isRoot) 368 { 369 // As a subclass, there will be a base class implementation (possibly empty). 370 371 builder.loadThis().loadArguments().invokeSpecial(plasticClass.getSuperClassName(), TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION); 372 373 // First store the result of the super() call into the variable. 374 builder.storeVariable(resultVariable); 375 builder.loadArgument(0).invoke(Event.class, boolean.class, "isAborted"); 376 builder.when(Condition.NON_ZERO, RETURN_TRUE); 377 } else 378 { 379 // No event handler method has yet been invoked. 380 builder.loadConstant(false).storeVariable(resultVariable); 381 } 382 383 for (EventHandlerMethod method : eventHandlerMethods) 384 { 385 method.buildMatchAndInvocation(builder, resultVariable); 386 387 model.addEventHandler(method.eventType); 388 389 if (method.handleActivationEventContext) 390 model.doHandleActivationEventContext(); 391 } 392 393 builder.loadVariable(resultVariable).returnResult(); 394 } 395 }); 396 } 397 }); 398 } 399 400 private Flow<PlasticMethod> matchEventHandlerMethods(PlasticClass plasticClass) 401 { 402 return F.flow(plasticClass.getMethods()).filter(new Predicate<PlasticMethod>() 403 { 404 public boolean accept(PlasticMethod method) 405 { 406 return (hasCorrectPrefix(method) || hasAnnotation(method)) && !method.isOverride(); 407 } 408 409 private boolean hasCorrectPrefix(PlasticMethod method) 410 { 411 return method.getDescription().methodName.startsWith("on"); 412 } 413 414 private boolean hasAnnotation(PlasticMethod method) 415 { 416 return method.hasAnnotation(OnEvent.class); 417 } 418 }); 419 } 420 421 422 private EventHandlerMethodParameterProvider createQueryParameterProvider(PlasticMethod method, final int parameterIndex, final String parameterName, 423 final String parameterTypeName, final boolean allowBlank) 424 { 425 final String methodIdentifier = method.getMethodIdentifier(); 426 427 return new EventHandlerMethodParameterProvider() 428 { 429 @SuppressWarnings("unchecked") 430 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 431 { 432 try 433 { 434 435 Class parameterType = classCache.forName(parameterTypeName); 436 boolean isArray = parameterType.isArray(); 437 438 if (isArray) 439 { 440 parameterType = parameterType.getComponentType(); 441 } 442 443 ValueEncoder valueEncoder = valueEncoderSource.getValueEncoder(parameterType); 444 445 String parameterValue = request.getParameter(parameterName); 446 447 if (!allowBlank && InternalUtils.isBlank(parameterValue)) 448 throw new RuntimeException(String.format( 449 "The value for query parameter '%s' was blank, but a non-blank value is needed.", 450 parameterName)); 451 452 Object value; 453 454 if (!isArray) 455 { 456 value = coerce(parameterName, parameterType, parameterValue, valueEncoder, allowBlank); 457 } else 458 { 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, allowBlank); 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, boolean allowBlank) 481 { 482 483 if (!allowBlank && InternalUtils.isBlank(parameterValue)) 484 { 485 throw new RuntimeException(String.format( 486 "The value for query parameter '%s' was blank, but a non-blank value is needed.", 487 parameterName)); 488 } 489 490 Object value = valueEncoder.toValue(parameterValue); 491 492 if (parameterType.isPrimitive() && value == null) 493 throw new RuntimeException( 494 String.format( 495 "Query parameter '%s' evaluates to null, but the event method parameter is type %s, a primitive.", 496 parameterName, parameterType.getName())); 497 return value; 498 } 499 }; 500 } 501 502 private EventHandlerMethodParameterProvider createEventContextProvider(final String type, final int parameterIndex) 503 { 504 return new EventHandlerMethodParameterProvider() 505 { 506 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 507 { 508 return event.coerceContext(parameterIndex, type); 509 } 510 }; 511 } 512 513 /** 514 * Returns the component id to match against, or the empty 515 * string if the component id is not specified. The component id 516 * is provided by the OnEvent annotation or (if that is not present) 517 * by the part of the method name following "From" ("onActionFromFoo"). 518 */ 519 private String extractComponentId(String methodName, OnEvent annotation) 520 { 521 if (annotation != null) 522 return annotation.component(); 523 524 // Method name started with "on". Extract the component id, if present. 525 526 int fromx = methodName.indexOf("From"); 527 528 if (fromx < 0) 529 return ""; 530 531 return methodName.substring(fromx + 4); 532 } 533 534 /** 535 * Returns the event name to match against, as specified in the annotation 536 * or (if the annotation is not present) extracted from the name of the method. 537 * "onActionFromFoo" or just "onAction". 538 */ 539 private String extractEventType(String methodName, OnEvent annotation) 540 { 541 if (annotation != null) 542 return annotation.value(); 543 544 int fromx = methodName.indexOf("From"); 545 546 // The first two characters are always "on" as in "onActionFromFoo". 547 return fromx == -1 ? methodName.substring(2) : methodName.substring(2, fromx); 548 } 549}