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