    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}