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