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}