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    
015    package org.apache.tapestry5.internal.transform;
016    
017    import org.apache.tapestry5.ComponentResources;
018    import org.apache.tapestry5.EventContext;
019    import org.apache.tapestry5.SymbolConstants;
020    import org.apache.tapestry5.ValueEncoder;
021    import org.apache.tapestry5.annotations.OnEvent;
022    import org.apache.tapestry5.annotations.RequestParameter;
023    import org.apache.tapestry5.func.F;
024    import org.apache.tapestry5.func.Flow;
025    import org.apache.tapestry5.func.Mapper;
026    import org.apache.tapestry5.func.Predicate;
027    import org.apache.tapestry5.internal.services.ComponentClassCache;
028    import org.apache.tapestry5.ioc.OperationTracker;
029    import org.apache.tapestry5.ioc.annotations.Symbol;
030    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
031    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
032    import org.apache.tapestry5.ioc.internal.util.TapestryException;
033    import org.apache.tapestry5.ioc.util.UnknownValueException;
034    import org.apache.tapestry5.model.MutableComponentModel;
035    import org.apache.tapestry5.plastic.*;
036    import org.apache.tapestry5.runtime.ComponentEvent;
037    import org.apache.tapestry5.runtime.Event;
038    import org.apache.tapestry5.runtime.PageLifecycleListener;
039    import org.apache.tapestry5.services.Request;
040    import org.apache.tapestry5.services.TransformConstants;
041    import org.apache.tapestry5.services.ValueEncoderSource;
042    import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
043    import org.apache.tapestry5.services.transform.TransformationSupport;
044    
045    import java.util.Arrays;
046    import java.util.List;
047    import 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     */
054    public 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    }