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.component.PartialStateHolder;
28  import javax.faces.component.UIComponent;
29  import javax.faces.component.UniqueIdVendor;
30  import javax.faces.component.behavior.AjaxBehavior;
31  import javax.faces.component.behavior.ClientBehavior;
32  import javax.faces.component.behavior.ClientBehaviorHolder;
33  import javax.faces.context.FacesContext;
34  import javax.faces.event.AbortProcessingException;
35  import javax.faces.event.AjaxBehaviorEvent;
36  import javax.faces.event.AjaxBehaviorListener;
37  import javax.faces.view.BehaviorHolderAttachedObjectHandler;
38  import javax.faces.view.facelets.ComponentHandler;
39  import javax.faces.view.facelets.FaceletContext;
40  import javax.faces.view.facelets.FaceletHandler;
41  import javax.faces.view.facelets.TagAttribute;
42  import javax.faces.view.facelets.TagAttributeException;
43  import javax.faces.view.facelets.TagConfig;
44  import javax.faces.view.facelets.TagException;
45  import javax.faces.view.facelets.TagHandler;
46  
47  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFFaceletAttribute;
48  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFFaceletTag;
49  import org.apache.myfaces.shared.renderkit.JSFAttr;
50  import org.apache.myfaces.shared.renderkit.html.util.ResourceUtils;
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: struberg $)
83   * @version $Revision: 1194861 $ $Date: 2011-10-29 05:02:34 -0500 (Sat, 29 Oct 2011) $
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     private final boolean _wrapMode;
158 
159     public AjaxHandler(TagConfig config)
160     {
161         super(config);
162         _disabled = getAttribute("disabled");
163         _event = getAttribute("event");
164         _execute = getAttribute("execute");
165         _immediate = getAttribute("immediate");
166         _listener = getAttribute("listener");
167         _onerror = getAttribute("onerror");
168         _onevent = getAttribute("onevent");
169         _render = getAttribute("render");
170         
171         // According to the spec, this tag works in two different ways:
172         // 1. Apply an ajax behavior for a selected component in this way
173         //    <x:component><f:ajax ..../></x:component>
174         // 2. Apply an ajax behavior for a group of components inside it
175         //   <f:ajax ....><x:componentA .../><x:componentB .../></f:ajax>
176         //
177         // The first problem is how to discriminate if f:ajax tag is on a
178         // "leaf" or if contain other components.
179         //
180         // One option is use the same strategy to cache instance for 
181         // <composite:interface> handler: traverse the tree for instances of 
182         // ComponentHandler. If it is found, wrapMode is used otherwise
183         // suppose f:ajax is the one wrapped by a component.
184         Collection<FaceletHandler> compHandlerList = 
185             TagHandlerUtils.findNextByType(nextHandler, ComponentHandler.class, 
186                     InsertChildrenHandler.class, InsertHandler.class, DecorateHandler.class, IncludeHandler.class);
187         
188         _wrapMode = !compHandlerList.isEmpty();
189     }
190 
191     public void apply(FaceletContext ctx, UIComponent parent)
192             throws IOException
193     {
194         //Apply only if we are creating a new component
195         if (!ComponentHandler.isNew(parent))
196         {
197             if (_wrapMode)
198             {
199                 AbstractFaceletContext actx = (AbstractFaceletContext) ctx;
200                 // In this case it will be only applied to components inserted by 
201                 // c:if or related tags, in other cases, ComponentTagHandlerDelegate should
202                 // not reapply ajax tag.
203                 actx.pushAjaxHandlerToStack(this);
204                 nextHandler.apply(ctx, parent);
205                 actx.popAjaxHandlerToStack();
206             }
207             return;
208         }
209         if (_wrapMode)
210         {
211             AbstractFaceletContext actx = (AbstractFaceletContext) ctx;
212             // Push and pop this ajax handler to the stack, to delegate the
213             // call to applyAttachedObject to ComponentTagHandlerDelegate
214             // TODO: The spec is not clear about how to deal with 
215             // composite component instances. The default one proposed here is
216             // use a different stack on DefaultFaceletContext.applyCompositeComponent,
217             // so components inside composite:implementation tag will not be
218             // affected by f:ajax outsider handlers.
219             actx.pushAjaxHandlerToStack(this);
220             nextHandler.apply(ctx, parent);
221             actx.popAjaxHandlerToStack();
222         }
223         else
224         {
225             if (parent instanceof ClientBehaviorHolder)
226             {
227                 //Apply this handler directly over the parent
228                 applyAttachedObject(ctx.getFacesContext(), parent);
229             }
230             else if (UIComponent.isCompositeComponent(parent))
231             {
232                 FaceletCompositionContext mctx = FaceletCompositionContext.getCurrentInstance(ctx);
233                 // It is supposed that for composite components, this tag should
234                 // add itself as a target, but note that on whole api does not exists
235                 // some tag that expose client behaviors as targets for composite
236                 // components. In RI, there exists a tag called composite:clientBehavior,
237                 // but does not appear on spec or javadoc, maybe because this could be
238                 // understand as an implementation detail, after all there exists a key
239                 // called AttachedObjectTarget.ATTACHED_OBJECT_TARGETS_KEY that could be
240                 // used to create a tag outside jsf implementation to attach targets.
241                 mctx.addAttachedObjectHandler(
242                         parent, this);
243             }
244             else
245             {
246                 throw new TagException(this.tag,
247                         "Parent is not composite component or of type ClientBehaviorHolder, type is: "
248                                 + parent);
249             }
250         }
251         
252         registerJsfAjaxDefaultResource(ctx, parent);
253     }
254     
255     public static void registerJsfAjaxDefaultResource(FaceletContext ctx, UIComponent parent)
256     {
257         // Register the standard ajax library on the current page in this way:
258         //
259         // <h:outputScript name="jsf.js" library="javax.faces" target="head"/>
260         //
261         // If no h:head component is in the page, we must anyway render the script inline,
262         // so the only way to make sure we are doing this is add a outputScript component.
263         // Note that call directly UIViewRoot.addComponentResource or use a listener 
264         // does not work in this case, because check this condition will requires 
265         // traverse the whole tree looking for h:head component.
266         FacesContext facesContext = ctx.getFacesContext();
267         if (!facesContext.getAttributes().containsKey(STANDARD_JSF_AJAX_LIBRARY_LOADED))
268         {
269             UIComponent outputScript = facesContext.getApplication().
270                 createComponent(facesContext, "javax.faces.Output", "javax.faces.resource.Script");
271             outputScript.getAttributes().put(JSFAttr.NAME_ATTR, ResourceUtils.JSF_JS_RESOURCE_NAME);
272             outputScript.getAttributes().put(JSFAttr.LIBRARY_ATTR, ResourceUtils.JAVAX_FACES_LIBRARY_NAME);
273             outputScript.getAttributes().put(JSFAttr.TARGET_ATTR, "head");
274 
275             //AbstractFaceletContext actx = (AbstractFaceletContext) ctx;
276 
277             // Since this component will be relocated, we need a generated clientId from the
278             // viewRoot, so when this one is relocated, its parent will be this UIViewRoot instance
279             // and prevent a duplicate id exception.
280             UniqueIdVendor uniqueIdVendor = ComponentSupport.getViewRoot(ctx, parent);
281             // UIViewRoot implements UniqueIdVendor, so there is no need to cast to UIViewRoot
282             // and call createUniqueId()
283             String uid = uniqueIdVendor.createUniqueId(ctx.getFacesContext(),null);
284             outputScript.setId(uid);
285             
286             parent.getChildren().add(outputScript);
287             
288             if (FaceletCompositionContext.getCurrentInstance(ctx).isMarkInitialState())
289             {
290                 //Call it only if we are using partial state saving
291                 outputScript.markInitialState();
292             }            
293             facesContext.getAttributes().put(STANDARD_JSF_AJAX_LIBRARY_LOADED, Boolean.TRUE);
294         }
295     }
296 
297     /**
298      * ViewDeclarationLanguage.retargetAttachedObjects uses it to check
299      * if the the target to be processed is applicable for this handler
300      */
301     public String getEventName()
302     {
303         if (_event == null)
304         {
305             return null;
306         }
307         else
308         {
309             return _event.getValue();
310         }
311     }
312 
313     /**
314      * This method should create an AjaxBehavior object and attach it to the
315      * parent component.
316      * 
317      * Also, it should check if the parent can apply the selected AjaxBehavior
318      * to the selected component through ClientBehaviorHolder.getEventNames() or
319      * ClientBehaviorHolder.getDefaultEventName()
320      */
321     public void applyAttachedObject(FacesContext context, UIComponent parent)
322     {
323         // Retrieve the current FaceletContext from FacesContext object
324         FaceletContext faceletContext = (FaceletContext) context
325                 .getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
326 
327         // cast to a ClientBehaviorHolder
328         ClientBehaviorHolder cvh = (ClientBehaviorHolder) parent;
329         
330         String eventName = getEventName();
331         if (eventName == null)
332         {
333             eventName = cvh.getDefaultEventName();
334             if (eventName == null)
335             {
336                 if (_wrapMode)
337                 {
338                     // No eventName defined, we can't apply this tag to this component, because
339                     // there is no event defined to attach it, but since we are in wrap mode
340                     // we have here the case that the component could not be the target
341                     // for this attached object.
342                     return;
343                 }
344                 else
345                 {
346                     throw new TagAttributeException(_event,
347                             "eventName could not be defined for f:ajax tag with no wrap mode.");
348                 }
349             }
350         }
351         else if (!cvh.getEventNames().contains(eventName))
352         {
353             if (_wrapMode)
354             {
355                 // The current component does not implement the event selected,
356                 // this ajax behavior cannot be applied, but we can't throw any exception
357                 // since we are in wrap mode and we have here the case that the 
358                 // component could not be the target for this attached object.
359                 return;
360             }
361             else
362             {
363                 throw new TagAttributeException(_event,
364                         "event it is not a valid eventName defined for this component");
365             }
366         }
367         
368         Map<String, List<ClientBehavior>> clientBehaviors = cvh.getClientBehaviors();
369 
370         List<ClientBehavior> clientBehaviorList = clientBehaviors.get(eventName);
371         if (clientBehaviorList != null && !clientBehaviorList.isEmpty())
372         {
373             for (ClientBehavior cb : clientBehaviorList)
374             {
375                 if (cb instanceof AjaxBehavior)
376                 {
377                     // The most inner one has been applied, so according to 
378                     // jsf 2.0 spec section 10.4.1.1 it is not necessary to apply
379                     // this one, because the inner one has precendece over
380                     // the outer one.
381                     return;
382                 }
383             }
384         }
385 
386         AjaxBehavior ajaxBehavior = createBehavior(context);
387 
388         if (_disabled != null)
389         {
390             if (_disabled.isLiteral())
391             {
392                 ajaxBehavior.setDisabled(_disabled.getBoolean(faceletContext));
393             }
394             else
395             {
396                 ajaxBehavior.setValueExpression("disabled", _disabled
397                         .getValueExpression(faceletContext, Boolean.class));
398             }
399         }
400         if (_execute != null)
401         {
402             ajaxBehavior.setValueExpression("execute", _execute
403                     .getValueExpression(faceletContext, Object.class));
404         }
405         if (_immediate != null)
406         {
407             if (_immediate.isLiteral())
408             {
409                 ajaxBehavior
410                         .setImmediate(_immediate.getBoolean(faceletContext));
411             }
412             else
413             {
414                 ajaxBehavior.setValueExpression("immediate", _immediate
415                         .getValueExpression(faceletContext, Boolean.class));
416             }
417         }
418         if (_listener != null)
419         {
420             MethodExpression expr = _listener.getMethodExpression(
421                     faceletContext, Void.TYPE, AJAX_BEHAVIOR_LISTENER_SIG);
422             AjaxBehaviorListener abl = new AjaxBehaviorListenerImpl(expr);
423             ajaxBehavior.addAjaxBehaviorListener(abl);
424         }
425         if (_onerror != null)
426         {
427             if (_onerror.isLiteral())
428             {
429                 ajaxBehavior.setOnerror(_onerror.getValue(faceletContext));
430             }
431             else
432             {
433                 ajaxBehavior.setValueExpression("onerror", _onerror
434                         .getValueExpression(faceletContext, String.class));
435             }
436         }
437         if (_onevent != null)
438         {
439             if (_onevent.isLiteral())
440             {
441                 ajaxBehavior.setOnevent(_onevent.getValue(faceletContext));
442             }
443             else
444             {
445                 ajaxBehavior.setValueExpression("onevent", _onevent
446                         .getValueExpression(faceletContext, String.class));
447             }
448         }
449         if (_render != null)
450         {
451             ajaxBehavior.setValueExpression("render", _render
452                     .getValueExpression(faceletContext, Object.class));
453         }
454         
455         cvh.addClientBehavior(eventName, ajaxBehavior);
456     }
457 
458     protected AjaxBehavior createBehavior(FacesContext context)
459     {
460         return (AjaxBehavior) context.getApplication().createBehavior(AjaxBehavior.BEHAVIOR_ID);
461     }
462 
463     /**
464      * The documentation says this attribute should not be used since it is not
465      * taken into account. Instead, getEventName is used on 
466      * ViewDeclarationLanguage.retargetAttachedObjects.
467      */
468     public String getFor()
469     {
470         return null;
471     }
472 
473     /**
474      * Wraps a method expression in a AjaxBehaviorListener
475      */
476     public final static class AjaxBehaviorListenerImpl implements
477             AjaxBehaviorListener, PartialStateHolder
478     {
479         private MethodExpression _expr;
480         private boolean _transient;
481         private boolean _initialStateMarked;
482         
483         public AjaxBehaviorListenerImpl ()
484         {
485         }
486         
487         public AjaxBehaviorListenerImpl(MethodExpression expr)
488         {
489             _expr = expr;
490         }
491 
492         public void processAjaxBehavior(AjaxBehaviorEvent event)
493                 throws AbortProcessingException
494         {
495             _expr.invoke(FacesContext.getCurrentInstance().getELContext(),
496                     new Object[] { event });
497         }
498 
499         public boolean isTransient()
500         {
501             return _transient;
502         }
503 
504         public void restoreState(FacesContext context, Object state)
505         {
506             if (state == null)
507             {
508                 return;
509             }
510             _expr = (MethodExpression) state;
511         }
512 
513         public Object saveState(FacesContext context)
514         {
515             if (initialStateMarked())
516             {
517                 return null;
518             }
519             return _expr;
520         }
521 
522         public void setTransient(boolean newTransientValue)
523         {
524             _transient = newTransientValue;
525         }
526         
527         public void clearInitialState()
528         {
529             _initialStateMarked = false;
530         }
531 
532         public boolean initialStateMarked()
533         {
534             return _initialStateMarked;
535         }
536 
537         public void markInitialState()
538         {
539             _initialStateMarked = true;
540         }
541     }
542 }