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}