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