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