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