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}