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