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