View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.view.facelets.tag.jsf.core;
20  
21  import java.io.IOException;
22  import java.util.Collection;
23  import java.util.List;
24  import java.util.Map;
25  
26  import javax.el.MethodExpression;
27  import javax.faces.application.ResourceHandler;
28  import javax.faces.component.PartialStateHolder;
29  import javax.faces.component.UIComponent;
30  import javax.faces.component.UniqueIdVendor;
31  import javax.faces.component.behavior.AjaxBehavior;
32  import javax.faces.component.behavior.ClientBehavior;
33  import javax.faces.component.behavior.ClientBehaviorHolder;
34  import javax.faces.context.FacesContext;
35  import javax.faces.event.AbortProcessingException;
36  import javax.faces.event.AjaxBehaviorEvent;
37  import javax.faces.event.AjaxBehaviorListener;
38  import javax.faces.view.BehaviorHolderAttachedObjectHandler;
39  import javax.faces.view.facelets.ComponentHandler;
40  import javax.faces.view.facelets.FaceletContext;
41  import javax.faces.view.facelets.FaceletHandler;
42  import javax.faces.view.facelets.TagAttribute;
43  import javax.faces.view.facelets.TagAttributeException;
44  import javax.faces.view.facelets.TagConfig;
45  import javax.faces.view.facelets.TagException;
46  import javax.faces.view.facelets.TagHandler;
47  
48  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFFaceletAttribute;
49  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFFaceletTag;
50  import org.apache.myfaces.shared.renderkit.JSFAttr;
51  import org.apache.myfaces.view.facelets.AbstractFaceletContext;
52  import org.apache.myfaces.view.facelets.FaceletCompositionContext;
53  import org.apache.myfaces.view.facelets.tag.TagHandlerUtils;
54  import org.apache.myfaces.view.facelets.tag.composite.InsertChildrenHandler;
55  import org.apache.myfaces.view.facelets.tag.jsf.ComponentSupport;
56  import org.apache.myfaces.view.facelets.tag.ui.DecorateHandler;
57  import org.apache.myfaces.view.facelets.tag.ui.IncludeHandler;
58  import org.apache.myfaces.view.facelets.tag.ui.InsertHandler;
59  
60  /**
61   * This tag creates an instance of AjaxBehavior, and associates it with the nearest 
62   * parent UIComponent that implements ClientBehaviorHolder interface. This tag can
63   * be used on single or composite components.
64   * <p>
65   * Unless otherwise specified, all attributes accept static values or EL expressions.
66   * </p>
67   * <p>
68   * According to the documentation, the tag handler implementing this tag should meet
69   * the following conditions:  
70   * </p>
71   * <ul>
72   * <li>Since this tag attach objects to UIComponent instances, and those instances 
73   * implements Behavior interface, this component should implement 
74   * BehaviorHolderAttachedObjectHandler interface.</li>
75   * <li>f:ajax does not support binding property. In theory we should do something similar
76   * to f:convertDateTime tag does: extends from ConverterHandler and override setAttributes
77   * method, but in this case BehaviorTagHandlerDelegate has binding property defined, so
78   * if we extend from BehaviorHandler we add binding support to f:ajax.</li>
79   * <li>This tag works as a attached object handler, but note on the api there is no component
80   * to define a target for a behavior. See comment inside apply() method.</li>
81   * </ul>
82   * @author Leonardo Uribe (latest modification by $Author$)
83   * @version $Revision$ $Date$
84   */
85  @JSFFaceletTag(name = "f:ajax")
86  public class AjaxHandler extends TagHandler implements
87          BehaviorHolderAttachedObjectHandler
88  {
89  
90      public final static Class<?>[] AJAX_BEHAVIOR_LISTENER_SIG = new Class<?>[] { AjaxBehaviorEvent.class };
91      
92      /**
93       * Constant used to check if in the current build view it has been rendered the standard jsf javascript
94       * library. It is necessary to remove this key from facesContext attribute map after build, to keep
95       * working this code for next views to be built.
96       */
97      public final static String STANDARD_JSF_AJAX_LIBRARY_LOADED
98              = "org.apache.myfaces.STANDARD_JSF_AJAX_LIBRARY_LOADED";
99  
100     /**
101      * 
102      */
103     @JSFFaceletAttribute(name = "disabled", className = "javax.el.ValueExpression",
104                          deferredValueType = "java.lang.Boolean")
105     private final TagAttribute _disabled;
106 
107     /**
108      * 
109      */
110     @JSFFaceletAttribute(name = "event", className = "javax.el.ValueExpression",
111                          deferredValueType = "java.lang.String")
112     private final TagAttribute _event;
113 
114     /**
115      * 
116      */
117     @JSFFaceletAttribute(name = "execute", className = "javax.el.ValueExpression",
118                          deferredValueType = "java.lang.Object")
119     private final TagAttribute _execute;
120 
121     /**
122      * 
123      */
124     @JSFFaceletAttribute(name = "immediate", className = "javax.el.ValueExpression",
125                          deferredValueType = "java.lang.Boolean")
126     private final TagAttribute _immediate;
127 
128     /**
129      * 
130      */
131     @JSFFaceletAttribute(name = "listener", className = "javax.el.MethodExpression",
132             deferredMethodSignature = "public void m(javax.faces.event.AjaxBehaviorEvent evt) "
133                                       + "throws javax.faces.event.AbortProcessingException")
134     private final TagAttribute _listener;
135 
136     /**
137      * 
138      */
139     @JSFFaceletAttribute(name = "onevent", className = "javax.el.ValueExpression",
140                          deferredValueType = "java.lang.String")
141     private final TagAttribute _onevent;
142 
143     /**
144      * 
145      */
146     @JSFFaceletAttribute(name = "onerror", className = "javax.el.ValueExpression",
147                          deferredValueType = "java.lang.String")
148     private final TagAttribute _onerror;
149 
150     /**
151      * 
152      */
153     @JSFFaceletAttribute(name = "render", className = "javax.el.ValueExpression",
154                          deferredValueType = "java.lang.Object")
155     private final TagAttribute _render;
156     /**
157      * 
158      */
159     @JSFFaceletAttribute(name = "delay", className = "javax.el.ValueExpression",
160                          deferredValueType = "java.lang.String")
161     private final TagAttribute _delay;
162     
163     @JSFFaceletAttribute(name = "resetValues", className = "javax.el.ValueExpression",
164             deferredValueType = "java.lang.Boolean")
165     private final TagAttribute _resetValues;
166     
167     private final boolean _wrapMode;
168 
169     public AjaxHandler(TagConfig config)
170     {
171         super(config);
172         _disabled = getAttribute("disabled");
173         _event = getAttribute("event");
174         _execute = getAttribute("execute");
175         _immediate = getAttribute("immediate");
176         _listener = getAttribute("listener");
177         _onerror = getAttribute("onerror");
178         _onevent = getAttribute("onevent");
179         _render = getAttribute("render");
180         _delay = getAttribute("delay");
181         _resetValues = getAttribute("resetValues");
182         // According to the spec, this tag works in two different ways:
183         // 1. Apply an ajax behavior for a selected component in this way
184         //    <x:component><f:ajax ..../></x:component>
185         // 2. Apply an ajax behavior for a group of components inside it
186         //   <f:ajax ....><x:componentA .../><x:componentB .../></f:ajax>
187         //
188         // The first problem is how to discriminate if f:ajax tag is on a
189         // "leaf" or if contain other components.
190         //
191         // One option is use the same strategy to cache instance for 
192         // <composite:interface> handler: traverse the tree for instances of 
193         // ComponentHandler. If it is found, wrapMode is used otherwise
194         // suppose f:ajax is the one wrapped by a component.
195         Collection<FaceletHandler> compHandlerList = 
196             TagHandlerUtils.findNextByType(nextHandler, ComponentHandler.class, 
197                     InsertChildrenHandler.class, InsertHandler.class, DecorateHandler.class, IncludeHandler.class);
198         
199         _wrapMode = !compHandlerList.isEmpty();
200     }
201 
202     public void apply(FaceletContext ctx, UIComponent parent)
203             throws IOException
204     {
205         //Apply only if we are creating a new component
206         if (!ComponentHandler.isNew(parent))
207         {
208             if (_wrapMode)
209             {
210                 AbstractFaceletContext actx = (AbstractFaceletContext) ctx;
211                 // In this case it will be only applied to components inserted by 
212                 // c:if or related tags, in other cases, ComponentTagHandlerDelegate should
213                 // not reapply ajax tag.
214                 actx.pushAjaxHandlerToStack(this);
215                 nextHandler.apply(ctx, parent);
216                 actx.popAjaxHandlerToStack();
217             }
218             return;
219         }
220         if (_wrapMode)
221         {
222             AbstractFaceletContext actx = (AbstractFaceletContext) ctx;
223             // Push and pop this ajax handler to the stack, to delegate the
224             // call to applyAttachedObject to ComponentTagHandlerDelegate
225             // The default one proposed here is
226             // use a different stack on DefaultFaceletContext.applyCompositeComponent,
227             // so components inside composite:implementation tag will not be
228             // affected by f:ajax outsider handlers.
229             actx.pushAjaxHandlerToStack(this);
230             nextHandler.apply(ctx, parent);
231             actx.popAjaxHandlerToStack();
232         }
233         else
234         {
235             if (parent instanceof ClientBehaviorHolder)
236             {
237                 //Apply this handler directly over the parent
238                 applyAttachedObject(ctx.getFacesContext(), parent);
239             }
240             else if (UIComponent.isCompositeComponent(parent))
241             {
242                 FaceletCompositionContext mctx = FaceletCompositionContext.getCurrentInstance(ctx);
243                 // It is supposed that for composite components, this tag should
244                 // add itself as a target, but note that on whole api does not exists
245                 // some tag that expose client behaviors as targets for composite
246                 // components. In RI, there exists a tag called composite:clientBehavior,
247                 // but does not appear on spec or javadoc, maybe because this could be
248                 // understand as an implementation detail, after all there exists a key
249                 // called AttachedObjectTarget.ATTACHED_OBJECT_TARGETS_KEY that could be
250                 // used to create a tag outside jsf implementation to attach targets.
251                 mctx.addAttachedObjectHandler(
252                         parent, this);
253             }
254             else
255             {
256                 throw new TagException(this.tag,
257                         "Parent is not composite component or of type ClientBehaviorHolder, type is: "
258                                 + parent);
259             }
260         }
261         
262         registerJsfAjaxDefaultResource(ctx, parent);
263     }
264     
265     public static void registerJsfAjaxDefaultResource(FaceletContext ctx, UIComponent parent)
266     {
267         // Register the standard ajax library on the current page in this way:
268         //
269         // <h:outputScript name="jsf.js" library="javax.faces" target="head"/>
270         //
271         // If no h:head component is in the page, we must anyway render the script inline,
272         // so the only way to make sure we are doing this is add a outputScript component.
273         // Note that call directly UIViewRoot.addComponentResource or use a listener 
274         // does not work in this case, because check this condition will requires 
275         // traverse the whole tree looking for h:head component.
276         FacesContext facesContext = ctx.getFacesContext();
277         if (!facesContext.getAttributes().containsKey(STANDARD_JSF_AJAX_LIBRARY_LOADED))
278         {
279             UIComponent outputScript = facesContext.getApplication().
280                 createComponent(facesContext, "javax.faces.Output", "javax.faces.resource.Script");
281             outputScript.getAttributes().put(JSFAttr.NAME_ATTR, ResourceHandler.JSF_SCRIPT_RESOURCE_NAME);
282             outputScript.getAttributes().put(JSFAttr.LIBRARY_ATTR, ResourceHandler.JSF_SCRIPT_LIBRARY_NAME);
283             outputScript.getAttributes().put(JSFAttr.TARGET_ATTR, "head");
284 
285             //AbstractFaceletContext actx = (AbstractFaceletContext) ctx;
286 
287             // Since this component will be relocated, we need a generated clientId from the
288             // viewRoot, so when this one is relocated, its parent will be this UIViewRoot instance
289             // and prevent a duplicate id exception.
290             UniqueIdVendor uniqueIdVendor = ComponentSupport.getViewRoot(ctx, parent);
291             // UIViewRoot implements UniqueIdVendor, so there is no need to cast to UIViewRoot
292             // and call createUniqueId()
293             String uid = uniqueIdVendor.createUniqueId(ctx.getFacesContext(),null);
294             outputScript.setId(uid);
295             
296             parent.getChildren().add(outputScript);
297             
298             if (FaceletCompositionContext.getCurrentInstance(ctx).isMarkInitialState())
299             {
300                 //Call it only if we are using partial state saving
301                 outputScript.markInitialState();
302             }            
303             facesContext.getAttributes().put(STANDARD_JSF_AJAX_LIBRARY_LOADED, Boolean.TRUE);
304         }
305     }
306 
307     /**
308      * ViewDeclarationLanguage.retargetAttachedObjects uses it to check
309      * if the the target to be processed is applicable for this handler
310      */
311     public String getEventName()
312     {
313         if (_event == null)
314         {
315             return null;
316         }
317         else
318         {
319             if (_event.isLiteral())
320             {
321                 return _event.getValue();
322             }
323             else
324             {
325                 FaceletContext faceletContext = (FaceletContext) FacesContext.getCurrentInstance().
326                         getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
327                 return (String) _event.getValueExpression(faceletContext, String.class).getValue(faceletContext);
328             }
329         }
330     }
331 
332     /**
333      * This method should create an AjaxBehavior object and attach it to the
334      * parent component.
335      * 
336      * Also, it should check if the parent can apply the selected AjaxBehavior
337      * to the selected component through ClientBehaviorHolder.getEventNames() or
338      * ClientBehaviorHolder.getDefaultEventName()
339      */
340     public void applyAttachedObject(FacesContext context, UIComponent parent)
341     {
342         // Retrieve the current FaceletContext from FacesContext object
343         FaceletContext faceletContext = (FaceletContext) context
344                 .getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
345 
346         // cast to a ClientBehaviorHolder
347         ClientBehaviorHolder cvh = (ClientBehaviorHolder) parent;
348         
349         
350         String eventName = null;
351         if (_event != null)
352         {
353             if (_event.isLiteral())
354             {
355                 eventName = _event.getValue();
356             }
357             else
358             {
359                 eventName = (String) _event.getValueExpression(faceletContext, String.class).getValue(faceletContext);
360             }
361         }
362         if (eventName == null)
363         {
364             eventName = cvh.getDefaultEventName();
365             if (eventName == null)
366             {
367                 if (_wrapMode)
368                 {
369                     // No eventName defined, we can't apply this tag to this component, because
370                     // there is no event defined to attach it, but since we are in wrap mode
371                     // we have here the case that the component could not be the target
372                     // for this attached object.
373                     return;
374                 }
375                 else
376                 {
377                     throw new TagAttributeException(_event,
378                             "eventName could not be defined for f:ajax tag with no wrap mode.");
379                 }
380             }
381         }
382         else if (!cvh.getEventNames().contains(eventName))
383         {
384             if (_wrapMode)
385             {
386                 // The current component does not implement the event selected,
387                 // this ajax behavior cannot be applied, but we can't throw any exception
388                 // since we are in wrap mode and we have here the case that the 
389                 // component could not be the target for this attached object.
390                 return;
391             }
392             else
393             {
394                 throw new TagAttributeException(_event,
395                         "event it is not a valid eventName defined for this component");
396             }
397         }
398         
399         Map<String, List<ClientBehavior>> clientBehaviors = cvh.getClientBehaviors();
400 
401         List<ClientBehavior> clientBehaviorList = clientBehaviors.get(eventName);
402         if (clientBehaviorList != null && !clientBehaviorList.isEmpty())
403         {
404             for (ClientBehavior cb : clientBehaviorList)
405             {
406                 if (cb instanceof AjaxBehavior)
407                 {
408                     // The most inner one has been applied, so according to 
409                     // jsf 2.0 spec section 10.4.1.1 it is not necessary to apply
410                     // this one, because the inner one has precendece over
411                     // the outer one.
412                     return;
413                 }
414             }
415         }
416 
417         AjaxBehavior ajaxBehavior = createBehavior(context);
418 
419         if (_disabled != null)
420         {
421             if (_disabled.isLiteral())
422             {
423                 ajaxBehavior.setDisabled(_disabled.getBoolean(faceletContext));
424             }
425             else
426             {
427                 ajaxBehavior.setValueExpression("disabled", _disabled
428                         .getValueExpression(faceletContext, Boolean.class));
429             }
430         }
431         if (_execute != null)
432         {
433             ajaxBehavior.setValueExpression("execute", _execute
434                     .getValueExpression(faceletContext, Object.class));
435         }
436         if (_immediate != null)
437         {
438             if (_immediate.isLiteral())
439             {
440                 ajaxBehavior
441                         .setImmediate(_immediate.getBoolean(faceletContext));
442             }
443             else
444             {
445                 ajaxBehavior.setValueExpression("immediate", _immediate
446                         .getValueExpression(faceletContext, Boolean.class));
447             }
448         }
449         if (_listener != null)
450         {
451             MethodExpression expr = _listener.getMethodExpression(
452                     faceletContext, Void.TYPE, AJAX_BEHAVIOR_LISTENER_SIG);
453             AjaxBehaviorListener abl = new AjaxBehaviorListenerImpl(expr);
454             ajaxBehavior.addAjaxBehaviorListener(abl);
455         }
456         if (_onerror != null)
457         {
458             if (_onerror.isLiteral())
459             {
460                 ajaxBehavior.setOnerror(_onerror.getValue(faceletContext));
461             }
462             else
463             {
464                 ajaxBehavior.setValueExpression("onerror", _onerror
465                         .getValueExpression(faceletContext, String.class));
466             }
467         }
468         if (_onevent != null)
469         {
470             if (_onevent.isLiteral())
471             {
472                 ajaxBehavior.setOnevent(_onevent.getValue(faceletContext));
473             }
474             else
475             {
476                 ajaxBehavior.setValueExpression("onevent", _onevent
477                         .getValueExpression(faceletContext, String.class));
478             }
479         }
480         if (_render != null)
481         {
482             ajaxBehavior.setValueExpression("render", _render
483                     .getValueExpression(faceletContext, Object.class));
484         }
485         if (_delay != null)
486         {
487             if (_delay.isLiteral())
488             {
489                 ajaxBehavior.setDelay(_delay.getValue(faceletContext));
490             }
491             else
492             {
493                 ajaxBehavior.setValueExpression("delay", _delay
494                     .getValueExpression(faceletContext, String.class));
495             }
496         }
497        if (_resetValues != null)
498         {
499             if (_resetValues.isLiteral())
500             {
501                 ajaxBehavior
502                         .setResetValues(_resetValues.getBoolean(faceletContext));
503             }
504             else
505             {
506                 ajaxBehavior.setValueExpression("resetValues", _resetValues
507                         .getValueExpression(faceletContext, Boolean.class));
508             }
509         }
510         cvh.addClientBehavior(eventName, ajaxBehavior);
511     }
512 
513     protected AjaxBehavior createBehavior(FacesContext context)
514     {
515         return (AjaxBehavior) context.getApplication().createBehavior(AjaxBehavior.BEHAVIOR_ID);
516     }
517 
518     /**
519      * The documentation says this attribute should not be used since it is not
520      * taken into account. Instead, getEventName is used on 
521      * ViewDeclarationLanguage.retargetAttachedObjects.
522      */
523     public String getFor()
524     {
525         return null;
526     }
527 
528     /**
529      * Wraps a method expression in a AjaxBehaviorListener
530      */
531     public final static class AjaxBehaviorListenerImpl implements
532             AjaxBehaviorListener, PartialStateHolder
533     {
534         private MethodExpression _expr;
535         private boolean _transient;
536         private boolean _initialStateMarked;
537         
538         public AjaxBehaviorListenerImpl ()
539         {
540         }
541         
542         public AjaxBehaviorListenerImpl(MethodExpression expr)
543         {
544             _expr = expr;
545         }
546 
547         public void processAjaxBehavior(AjaxBehaviorEvent event)
548                 throws AbortProcessingException
549         {
550             _expr.invoke(FacesContext.getCurrentInstance().getELContext(),
551                     new Object[] { event });
552         }
553 
554         public boolean isTransient()
555         {
556             return _transient;
557         }
558 
559         public void restoreState(FacesContext context, Object state)
560         {
561             if (state == null)
562             {
563                 return;
564             }
565             _expr = (MethodExpression) state;
566         }
567 
568         public Object saveState(FacesContext context)
569         {
570             if (initialStateMarked())
571             {
572                 return null;
573             }
574             return _expr;
575         }
576 
577         public void setTransient(boolean newTransientValue)
578         {
579             _transient = newTransientValue;
580         }
581         
582         public void clearInitialState()
583         {
584             _initialStateMarked = false;
585         }
586 
587         public boolean initialStateMarked()
588         {
589             return _initialStateMarked;
590         }
591 
592         public void markInitialState()
593         {
594             _initialStateMarked = true;
595         }
596     }
597 }