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; 019import java.util.Optional; 020import java.util.Set; 021import java.util.regex.Pattern; 022 023import org.apache.tapestry5.ComponentResources; 024import org.apache.tapestry5.EventContext; 025import org.apache.tapestry5.ValueEncoder; 026import org.apache.tapestry5.annotations.DisableStrictChecks; 027import org.apache.tapestry5.annotations.OnEvent; 028import org.apache.tapestry5.annotations.PublishEvent; 029import org.apache.tapestry5.annotations.RequestBody; 030import org.apache.tapestry5.annotations.RequestParameter; 031import org.apache.tapestry5.annotations.StaticActivationContextValue; 032import org.apache.tapestry5.commons.internal.util.TapestryException; 033import org.apache.tapestry5.commons.util.CollectionFactory; 034import org.apache.tapestry5.commons.util.ExceptionUtils; 035import org.apache.tapestry5.commons.util.UnknownValueException; 036import org.apache.tapestry5.corelib.mixins.PublishServerSideEvents; 037import org.apache.tapestry5.func.F; 038import org.apache.tapestry5.func.Flow; 039import org.apache.tapestry5.func.Mapper; 040import org.apache.tapestry5.func.Predicate; 041import org.apache.tapestry5.http.services.Request; 042import org.apache.tapestry5.http.services.RestSupport; 043import org.apache.tapestry5.internal.InternalConstants; 044import org.apache.tapestry5.internal.services.ComponentClassCache; 045import org.apache.tapestry5.ioc.Invokable; 046import org.apache.tapestry5.ioc.OperationTracker; 047import org.apache.tapestry5.ioc.internal.util.InternalUtils; 048import org.apache.tapestry5.json.JSONArray; 049import org.apache.tapestry5.json.JSONObject; 050import org.apache.tapestry5.model.MutableComponentModel; 051import org.apache.tapestry5.plastic.Condition; 052import org.apache.tapestry5.plastic.InstructionBuilder; 053import org.apache.tapestry5.plastic.InstructionBuilderCallback; 054import org.apache.tapestry5.plastic.LocalVariable; 055import org.apache.tapestry5.plastic.LocalVariableCallback; 056import org.apache.tapestry5.plastic.MethodAdvice; 057import org.apache.tapestry5.plastic.MethodDescription; 058import org.apache.tapestry5.plastic.MethodInvocation; 059import org.apache.tapestry5.plastic.MethodParameter; 060import org.apache.tapestry5.plastic.PlasticClass; 061import org.apache.tapestry5.plastic.PlasticField; 062import org.apache.tapestry5.plastic.PlasticMethod; 063import org.apache.tapestry5.runtime.ComponentEvent; 064import org.apache.tapestry5.runtime.Event; 065import org.apache.tapestry5.runtime.PageLifecycleListener; 066import org.apache.tapestry5.services.TransformConstants; 067import org.apache.tapestry5.services.ValueEncoderSource; 068import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; 069import org.apache.tapestry5.services.transform.TransformationSupport; 070 071/** 072 * Provides implementations of the 073 * {@link org.apache.tapestry5.runtime.Component#dispatchComponentEvent(org.apache.tapestry5.runtime.ComponentEvent)} 074 * method, based on {@link org.apache.tapestry5.annotations.OnEvent} annotations and naming conventions. 075 */ 076public class OnEventWorker implements ComponentClassTransformWorker2 077{ 078 private final Request request; 079 080 private final ValueEncoderSource valueEncoderSource; 081 082 private final RestSupport restSupport; 083 084 private final ComponentClassCache classCache; 085 086 private final OperationTracker operationTracker; 087 088 private final InstructionBuilderCallback RETURN_TRUE = new InstructionBuilderCallback() 089 { 090 public void doBuild(InstructionBuilder builder) 091 { 092 builder.loadConstant(true).returnResult(); 093 } 094 }; 095 096 private final static Predicate<PlasticMethod> IS_EVENT_HANDLER = new Predicate<PlasticMethod>() 097 { 098 public boolean accept(PlasticMethod method) 099 { 100 return (hasCorrectPrefix(method) || hasAnnotation(method)) && !method.isOverride(); 101 } 102 103 private boolean hasCorrectPrefix(PlasticMethod method) 104 { 105 return method.getDescription().methodName.startsWith("on"); 106 } 107 108 private boolean hasAnnotation(PlasticMethod method) 109 { 110 return method.hasAnnotation(OnEvent.class); 111 } 112 }; 113 114 class ComponentIdValidator 115 { 116 final String componentId; 117 118 final String methodIdentifier; 119 120 ComponentIdValidator(String componentId, String methodIdentifier) 121 { 122 this.componentId = componentId; 123 this.methodIdentifier = methodIdentifier; 124 } 125 126 void validate(ComponentResources resources) 127 { 128 try 129 { 130 resources.getEmbeddedComponent(componentId); 131 } catch (UnknownValueException ex) 132 { 133 throw new TapestryException(String.format("Method %s references component id '%s' which does not exist.", 134 methodIdentifier, componentId), resources.getLocation(), ex); 135 } 136 } 137 } 138 139 class ValidateComponentIds implements MethodAdvice 140 { 141 final ComponentIdValidator[] validators; 142 143 ValidateComponentIds(ComponentIdValidator[] validators) 144 { 145 this.validators = validators; 146 } 147 148 public void advise(MethodInvocation invocation) 149 { 150 ComponentResources resources = invocation.getInstanceContext().get(ComponentResources.class); 151 152 for (ComponentIdValidator validator : validators) 153 { 154 validator.validate(resources); 155 } 156 157 invocation.proceed(); 158 } 159 } 160 161 /** 162 * Encapsulates information needed to invoke a method as an event handler method, including the logic 163 * to construct parameter values, and match the method against the {@link ComponentEvent}. 164 */ 165 class EventHandlerMethod 166 { 167 final PlasticMethod method; 168 169 final MethodDescription description; 170 171 final String eventType, componentId; 172 173 final EventHandlerMethodParameterSource parameterSource; 174 175 int minContextValues = 0; 176 177 boolean handleActivationEventContext = false; 178 179 final String[] staticActivationContextValues; 180 181 final PublishEvent publishEvent; 182 183 EventHandlerMethod(PlasticMethod method) 184 { 185 this.method = method; 186 description = method.getDescription(); 187 188 parameterSource = buildSource(); 189 190 String methodName = method.getDescription().methodName; 191 192 OnEvent onEvent = method.getAnnotation(OnEvent.class); 193 194 eventType = extractEventType(methodName, onEvent); 195 componentId = extractComponentId(methodName, onEvent); 196 197 publishEvent = method.getAnnotation(PublishEvent.class); 198 staticActivationContextValues = extractStaticActivationContextValues(method); 199 } 200 201 final private Pattern WHITESPACE = Pattern.compile(".*\\s.*"); 202 203 private String[] extractStaticActivationContextValues(PlasticMethod method) 204 { 205 String[] values = null; 206 for (int i = 0; i < method.getParameters().size(); i++) 207 { 208 MethodParameter parameter = method.getParameters().get(i); 209 final StaticActivationContextValue staticValue = parameter.getAnnotation(StaticActivationContextValue.class); 210 if (staticValue != null) 211 { 212 if (values == null) 213 { 214 values = new String[method.getParameters().size()]; 215 } 216 String value = staticValue.value(); 217 if (value != null && !value.isEmpty() && !WHITESPACE.matcher(value).matches()) 218 { 219 values[i] = value; 220 } 221 else 222 { 223 throw new RuntimeException(String.format("%s has at least one parameter " 224 + "with a @%s annotation with an invalid value (empty string or " 225 + "value containing whitespace)", 226 method.getMethodIdentifier(), 227 StaticActivationContextValue.class.getSimpleName())); 228 } 229 } 230 } 231 return values; 232 } 233 234 void buildMatchAndInvocation(InstructionBuilder builder, final LocalVariable resultVariable) 235 { 236 final PlasticField sourceField = 237 parameterSource == null ? null 238 : method.getPlasticClass().introduceField(EventHandlerMethodParameterSource.class, description.methodName + "$parameterSource").inject(parameterSource); 239 240 final PlasticField staticActivationContextValueField = 241 staticActivationContextValues == null ? null 242 : method.getPlasticClass().introduceField(String[].class, description.methodName + "$staticActivationContextValues").inject(staticActivationContextValues); 243 244 builder.loadArgument(0).loadConstant(eventType).loadConstant(componentId).loadConstant(minContextValues); 245 if (staticActivationContextValueField != null) 246 { 247 builder.loadThis().getField(staticActivationContextValueField); 248 } 249 else 250 { 251 builder.loadNull(); 252 } 253 254 builder.invoke(ComponentEvent.class, boolean.class, "matches", String.class, String.class, int.class, String[].class); 255 256 builder.when(Condition.NON_ZERO, new InstructionBuilderCallback() 257 { 258 public void doBuild(InstructionBuilder builder) 259 { 260 builder.loadArgument(0).loadConstant(method.getMethodIdentifier()).invoke(Event.class, void.class, "setMethodDescription", String.class); 261 262 builder.loadThis(); 263 264 int count = description.argumentTypes.length; 265 266 for (int i = 0; i < count; i++) 267 { 268 builder.loadThis().getField(sourceField).loadArgument(0).loadConstant(i); 269 270 builder.invoke(EventHandlerMethodParameterSource.class, Object.class, "get", 271 ComponentEvent.class, int.class); 272 273 builder.castOrUnbox(description.argumentTypes[i]); 274 } 275 276 builder.invokeVirtual(method); 277 278 if (!method.isVoid()) 279 { 280 builder.boxPrimitive(description.returnType); 281 builder.loadArgument(0).swap(); 282 283 builder.invoke(Event.class, boolean.class, "storeResult", Object.class); 284 285 // storeResult() returns true if the method is aborted. Return true since, certainly, 286 // a method was invoked. 287 builder.when(Condition.NON_ZERO, RETURN_TRUE); 288 } 289 290 // Set the result to true, to indicate that some method was invoked. 291 292 builder.loadConstant(true).storeVariable(resultVariable); 293 } 294 }); 295 } 296 297 298 private EventHandlerMethodParameterSource buildSource() 299 { 300 final String[] parameterTypes = method.getDescription().argumentTypes; 301 302 if (parameterTypes.length == 0) 303 { 304 return null; 305 } 306 307 final List<EventHandlerMethodParameterProvider> providers = CollectionFactory.newList(); 308 309 int contextIndex = 0; 310 boolean hasBodyRequestParameters = false; 311 312 for (int i = 0; i < parameterTypes.length; i++) 313 { 314 String type = parameterTypes[i]; 315 316 EventHandlerMethodParameterProvider provider = parameterTypeToProvider.get(type); 317 318 if (provider != null) 319 { 320 providers.add(provider); 321 this.handleActivationEventContext = true; 322 continue; 323 } 324 325 RequestParameter parameterAnnotation = method.getParameters().get(i).getAnnotation(RequestParameter.class); 326 327 if (parameterAnnotation != null) 328 { 329 String parameterName = parameterAnnotation.value(); 330 331 providers.add(createQueryParameterProvider(method, i, parameterName, type, 332 parameterAnnotation.allowBlank())); 333 continue; 334 } 335 336 RequestBody bodyAnnotation = method.getParameters().get(i).getAnnotation(RequestBody.class); 337 338 if (bodyAnnotation != null) 339 { 340 if (!hasBodyRequestParameters) 341 { 342 providers.add(createRequestBodyProvider(method, i, type, 343 bodyAnnotation.allowEmpty())); 344 hasBodyRequestParameters = true; 345 } 346 else 347 { 348 throw new RuntimeException( 349 String.format("Method %s has more than one @RequestBody parameter", method.getDescription())); 350 } 351 continue; 352 } 353 354 // Note: probably safe to do the conversion to Class early (class load time) 355 // as parameters are rarely (if ever) component classes. 356 357 providers.add(createEventContextProvider(type, contextIndex++)); 358 } 359 360 361 minContextValues = contextIndex; 362 363 EventHandlerMethodParameterProvider[] providerArray = providers.toArray(new EventHandlerMethodParameterProvider[providers.size()]); 364 365 return new EventHandlerMethodParameterSource(method.getMethodIdentifier(), operationTracker, providerArray); 366 } 367 } 368 369 370 /** 371 * Stores a couple of special parameter type mappings that are used when matching the entire event context 372 * (either as Object[] or EventContext). 373 */ 374 private final Map<String, EventHandlerMethodParameterProvider> parameterTypeToProvider = CollectionFactory.newMap(); 375 376 { 377 // Object[] and List are out-dated and may be deprecated some day 378 379 parameterTypeToProvider.put("java.lang.Object[]", new EventHandlerMethodParameterProvider() 380 { 381 382 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 383 { 384 return event.getContext(); 385 } 386 }); 387 388 parameterTypeToProvider.put(List.class.getName(), new EventHandlerMethodParameterProvider() 389 { 390 391 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 392 { 393 return Arrays.asList(event.getContext()); 394 } 395 }); 396 397 // This is better, as the EventContext maintains the original objects (or strings) 398 // and gives the event handler method access with coercion 399 parameterTypeToProvider.put(EventContext.class.getName(), new EventHandlerMethodParameterProvider() 400 { 401 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 402 { 403 return event.getEventContext(); 404 } 405 }); 406 } 407 408 public OnEventWorker(Request request, ValueEncoderSource valueEncoderSource, ComponentClassCache classCache, OperationTracker operationTracker, RestSupport restSupport) 409 { 410 this.request = request; 411 this.valueEncoderSource = valueEncoderSource; 412 this.classCache = classCache; 413 this.operationTracker = operationTracker; 414 this.restSupport = restSupport; 415 } 416 417 public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) 418 { 419 Flow<PlasticMethod> methods = matchEventHandlerMethods(plasticClass); 420 421 if (methods.isEmpty()) 422 { 423 return; 424 } 425 426 addEventHandlingLogic(plasticClass, support.isRootTransformation(), methods, model); 427 } 428 429 private static final Set<String> HTTP_EVENT_HANDLER_NAMES = InternalConstants.SUPPORTED_HTTP_METHOD_EVENT_HANDLER_METHOD_NAMES; 430 431 private static final Set<String> HTTP_METHOD_EVENTS = InternalConstants.SUPPORTED_HTTP_METHOD_EVENTS; 432 433 private void addEventHandlingLogic(final PlasticClass plasticClass, final boolean isRoot, final Flow<PlasticMethod> plasticMethods, final MutableComponentModel model) 434 { 435 Flow<EventHandlerMethod> eventHandlerMethods = plasticMethods.map(new Mapper<PlasticMethod, EventHandlerMethod>() 436 { 437 public EventHandlerMethod map(PlasticMethod element) 438 { 439 return new EventHandlerMethod(element); 440 } 441 }); 442 443 implementDispatchMethod(plasticClass, isRoot, model, eventHandlerMethods); 444 445 addComponentIdValidationLogicOnPageLoad(plasticClass, eventHandlerMethods); 446 447 addPublishEventInfo(eventHandlerMethods, model); 448 } 449 450 private void addPublishEventInfo(Flow<EventHandlerMethod> eventHandlerMethods, 451 MutableComponentModel model) 452 { 453 JSONArray publishEvents = new JSONArray(); 454 for (EventHandlerMethod eventHandlerMethod : eventHandlerMethods) 455 { 456 if (eventHandlerMethod.publishEvent != null) 457 { 458 publishEvents.add(eventHandlerMethod.eventType.toLowerCase()); 459 } 460 } 461 462 // If we do have events to publish, we apply the mixin and pass 463 // event information to it. 464 if (publishEvents.size() > 0) { 465 model.addMixinClassName(PublishServerSideEvents.class.getName(), "after:*"); 466 model.setMeta(InternalConstants.PUBLISH_COMPONENT_EVENTS_META, publishEvents.toString()); 467 } 468 } 469 470 private void addComponentIdValidationLogicOnPageLoad(PlasticClass plasticClass, Flow<EventHandlerMethod> eventHandlerMethods) 471 { 472 ComponentIdValidator[] validators = extractComponentIdValidators(eventHandlerMethods); 473 474 if (validators.length > 0) 475 { 476 plasticClass.introduceInterface(PageLifecycleListener.class); 477 plasticClass.introduceMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_DESCRIPTION).addAdvice(new ValidateComponentIds(validators)); 478 } 479 } 480 481 private ComponentIdValidator[] extractComponentIdValidators(Flow<EventHandlerMethod> eventHandlerMethods) 482 { 483 return eventHandlerMethods.map(new Mapper<EventHandlerMethod, ComponentIdValidator>() 484 { 485 public ComponentIdValidator map(EventHandlerMethod element) 486 { 487 if (element.componentId.equals("")) 488 { 489 return null; 490 } 491 if (element.method.getAnnotation(DisableStrictChecks.class) != null) 492 { 493 return null; 494 } 495 496 return new ComponentIdValidator(element.componentId, element.method.getMethodIdentifier()); 497 } 498 }).removeNulls().toArray(ComponentIdValidator.class); 499 } 500 501 private void implementDispatchMethod(final PlasticClass plasticClass, final boolean isRoot, final MutableComponentModel model, final Flow<EventHandlerMethod> eventHandlerMethods) 502 { 503 plasticClass.introduceMethod(TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION).changeImplementation(new InstructionBuilderCallback() 504 { 505 public void doBuild(InstructionBuilder builder) 506 { 507 builder.startVariable("boolean", new LocalVariableCallback() 508 { 509 public void doBuild(LocalVariable resultVariable, InstructionBuilder builder) 510 { 511 if (!isRoot) 512 { 513 // As a subclass, there will be a base class implementation (possibly empty). 514 515 builder.loadThis().loadArguments().invokeSpecial(plasticClass.getSuperClassName(), TransformConstants.DISPATCH_COMPONENT_EVENT_DESCRIPTION); 516 517 // First store the result of the super() call into the variable. 518 builder.storeVariable(resultVariable); 519 builder.loadArgument(0).invoke(Event.class, boolean.class, "isAborted"); 520 builder.when(Condition.NON_ZERO, RETURN_TRUE); 521 } else 522 { 523 // No event handler method has yet been invoked. 524 builder.loadConstant(false).storeVariable(resultVariable); 525 } 526 527 boolean hasRestEndpointEventHandlerMethod = false; 528 JSONArray restEndpointEventHandlerMethods = null; 529 for (EventHandlerMethod method : eventHandlerMethods) 530 { 531 method.buildMatchAndInvocation(builder, resultVariable); 532 533 model.addEventHandler(method.eventType); 534 535 if (method.handleActivationEventContext) 536 { 537 model.doHandleActivationEventContext(); 538 } 539 540 // We're collecting this info for all components, even considering REST 541 // events are only triggered in pages, because we can have REST event 542 // handler methods in base classes too, and we need this info 543 // for generating complete, correct OpenAPI documentation. 544 final OnEvent onEvent = method.method.getAnnotation(OnEvent.class); 545 final String methodName = method.method.getDescription().methodName; 546 if (isRestEndpointEventHandlerMethod(onEvent, methodName)) 547 { 548 hasRestEndpointEventHandlerMethod = true; 549 if (restEndpointEventHandlerMethods == null) 550 { 551 restEndpointEventHandlerMethods = new JSONArray(); 552 } 553 JSONObject methodMeta = new JSONObject(); 554 methodMeta.put("name", methodName); 555 JSONArray parameters = new JSONArray(); 556 for (MethodParameter parameter : method.method.getParameters()) 557 { 558 parameters.add(parameter.getType()); 559 } 560 methodMeta.put("parameters", parameters); 561 restEndpointEventHandlerMethods.add(methodMeta); 562 } 563 } 564 565 // This meta property is only ever checked in pages, so we avoid using more 566 // memory by not setting it to all component models. 567 if (model.isPage()) 568 { 569 model.setMeta(InternalConstants.REST_ENDPOINT_EVENT_HANDLER_METHOD_PRESENT, 570 hasRestEndpointEventHandlerMethod ? InternalConstants.TRUE : InternalConstants.FALSE); 571 } 572 573 // See comment on the top of isRestEndpointEventHandlerMethod() above. 574 // This shouldn't waste memory unless there are REST event handler 575 // methods in components, something that would be ignored anyway. 576 if (restEndpointEventHandlerMethods != null) 577 { 578 model.setMeta(InternalConstants.REST_ENDPOINT_EVENT_HANDLER_METHODS, 579 restEndpointEventHandlerMethods.toCompactString()); 580 } 581 582 builder.loadVariable(resultVariable).returnResult(); 583 } 584 585 }); 586 } 587 }); 588 } 589 590 private Flow<PlasticMethod> matchEventHandlerMethods(PlasticClass plasticClass) 591 { 592 return F.flow(plasticClass.getMethods()).filter(IS_EVENT_HANDLER); 593 } 594 595 @SuppressWarnings({ "unchecked", "rawtypes" }) 596 private EventHandlerMethodParameterProvider createRequestBodyProvider(PlasticMethod method, final int parameterIndex, 597 final String parameterTypeName, final boolean allowEmpty) 598 { 599 final String methodIdentifier = method.getMethodIdentifier(); 600 return (event) -> { 601 Invokable<Object> operation = () -> { 602 Class parameterType = classCache.forName(parameterTypeName); 603 Optional result = restSupport.getRequestBodyAs(parameterType); 604 if (!allowEmpty && !result.isPresent()) 605 { 606 throw new RuntimeException( 607 String.format("The request has an empty body and %s has one parameter with @RequestBody(allowEmpty=false)", methodIdentifier)); 608 } 609 return result.orElse(null); 610 }; 611 return operationTracker.invoke( 612 "Converting HTTP request body for @RequestBody parameter", 613 operation); 614 }; 615 } 616 617 private EventHandlerMethodParameterProvider createQueryParameterProvider(PlasticMethod method, final int parameterIndex, final String parameterName, 618 final String parameterTypeName, final boolean allowBlank) 619 { 620 final String methodIdentifier = method.getMethodIdentifier(); 621 622 return new EventHandlerMethodParameterProvider() 623 { 624 @SuppressWarnings("unchecked") 625 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 626 { 627 try 628 { 629 630 Class parameterType = classCache.forName(parameterTypeName); 631 boolean isArray = parameterType.isArray(); 632 633 if (isArray) 634 { 635 parameterType = parameterType.getComponentType(); 636 } 637 638 ValueEncoder valueEncoder = valueEncoderSource.getValueEncoder(parameterType); 639 640 String parameterValue = request.getParameter(parameterName); 641 642 if (!allowBlank && InternalUtils.isBlank(parameterValue)) 643 throw new RuntimeException(String.format( 644 "The value for query parameter '%s' was blank, but a non-blank value is needed.", 645 parameterName)); 646 647 Object value; 648 649 if (!isArray) 650 { 651 value = coerce(parameterName, parameterType, parameterValue, valueEncoder, allowBlank); 652 } else 653 { 654 String[] parameterValues = request.getParameters(parameterName); 655 Object[] array = (Object[]) Array.newInstance(parameterType, parameterValues.length); 656 for (int i = 0; i < parameterValues.length; i++) 657 { 658 array[i] = coerce(parameterName, parameterType, parameterValues[i], valueEncoder, allowBlank); 659 } 660 value = array; 661 } 662 663 return value; 664 } catch (Exception ex) 665 { 666 throw new RuntimeException( 667 String.format( 668 "Unable process query parameter '%s' as parameter #%d of event handler method %s: %s", 669 parameterName, parameterIndex + 1, methodIdentifier, 670 ExceptionUtils.toMessage(ex)), ex); 671 } 672 } 673 674 private Object coerce(final String parameterName, Class parameterType, 675 String parameterValue, ValueEncoder valueEncoder, boolean allowBlank) 676 { 677 678 if (!allowBlank && InternalUtils.isBlank(parameterValue)) 679 { 680 throw new RuntimeException(String.format( 681 "The value for query parameter '%s' was blank, but a non-blank value is needed.", 682 parameterName)); 683 } 684 685 Object value = valueEncoder.toValue(parameterValue); 686 687 if (parameterType.isPrimitive() && value == null) 688 throw new RuntimeException( 689 String.format( 690 "Query parameter '%s' evaluates to null, but the event method parameter is type %s, a primitive.", 691 parameterName, parameterType.getName())); 692 return value; 693 } 694 }; 695 } 696 697 private EventHandlerMethodParameterProvider createEventContextProvider(final String type, final int parameterIndex) 698 { 699 return new EventHandlerMethodParameterProvider() 700 { 701 public Object valueForEventHandlerMethodParameter(ComponentEvent event) 702 { 703 return event.coerceContext(parameterIndex, type); 704 } 705 }; 706 } 707 708 /** 709 * Returns the component id to match against, or the empty 710 * string if the component id is not specified. The component id 711 * is provided by the OnEvent annotation or (if that is not present) 712 * by the part of the method name following "From" ("onActionFromFoo"). 713 */ 714 private String extractComponentId(String methodName, OnEvent annotation) 715 { 716 if (annotation != null) 717 return annotation.component(); 718 719 // Method name started with "on". Extract the component id, if present. 720 721 int fromx = methodName.indexOf("From"); 722 723 if (fromx < 0) 724 return ""; 725 726 return methodName.substring(fromx + 4); 727 } 728 729 /** 730 * Returns the event name to match against, as specified in the annotation 731 * or (if the annotation is not present) extracted from the name of the method. 732 * "onActionFromFoo" or just "onAction". 733 */ 734 private String extractEventType(String methodName, OnEvent annotation) 735 { 736 if (annotation != null) 737 return annotation.value(); 738 739 int fromx = methodName.indexOf("From"); 740 741 // The first two characters are always "on" as in "onActionFromFoo". 742 return fromx == -1 ? methodName.substring(2) : methodName.substring(2, fromx); 743 } 744 745 /** 746 * Tells whether a method with a given name and possibly {@link OnEvent} annotation 747 * is a REST endpoint event handler method or not. 748 */ 749 public static boolean isRestEndpointEventHandlerMethod(final OnEvent onEvent, final String methodName) { 750 return onEvent != null && HTTP_METHOD_EVENTS.contains(onEvent.value().toLowerCase()) 751 || HTTP_EVENT_HANDLER_NAMES.contains(methodName.toLowerCase()); 752 } 753 754}