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