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