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