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