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.renderkit.html;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.RandomAccess;
26  
27  import javax.faces.FacesException;
28  import javax.faces.component.ActionSource;
29  import javax.faces.component.EditableValueHolder;
30  import javax.faces.component.UIComponent;
31  import javax.faces.component.behavior.AjaxBehavior;
32  import javax.faces.component.behavior.ClientBehavior;
33  import javax.faces.component.behavior.ClientBehaviorContext;
34  import javax.faces.context.FacesContext;
35  import javax.faces.event.AjaxBehaviorEvent;
36  import javax.faces.event.PhaseId;
37  import javax.faces.render.ClientBehaviorRenderer;
38  import org.apache.myfaces.shared.renderkit.html.util.SharedStringBuilder;
39  
40  /**
41   * @author Werner Punz  (latest modification by $Author$)
42   * @version $Revision$ $Date$
43   */
44  public class HtmlAjaxBehaviorRenderer extends ClientBehaviorRenderer
45  {
46  
47      private static final String QUOTE = "'";
48      private static final String BLANK = " ";
49  
50      private static final String AJAX_KEY_ONERROR = "onerror";
51      private static final String AJAX_KEY_ONEVENT = "onevent";
52      private static final String AJAX_KEY_EXECUTE = "execute";
53      private static final String AJAX_KEY_RENDER = "render";
54      private static final String AJAX_KEY_DELAY = "delay";
55      private static final String AJAX_KEY_RESETVALUES = "resetValues";
56  
57      private static final String AJAX_VAL_THIS = "this";
58      private static final String AJAX_VAL_EVENT = "event";
59      private static final String JS_AJAX_REQUEST = "jsf.ajax.request";
60  
61      private static final String COLON = ":";
62      private static final String EMPTY = "";
63      private static final String COMMA = ",";
64  
65      private static final String ERR_NO_AJAX_BEHAVIOR = "The behavior must be an instance of AjaxBehavior";
66      private static final String L_PAREN = "(";
67      private static final String R_PAREN = ")";
68  
69      /*if this marker is present in the request we have to dispatch a behavior event*/
70      /*if an attached behavior triggers an ajax request this request param must be added*/
71      private static final String BEHAVIOR_EVENT = "javax.faces.behavior.event";
72      private static final String IDENTIFYER_MARKER = "@";
73      
74      private static final String AJAX_SB = "oam.renderkit.AJAX_SB";
75      private static final String AJAX_PARAM_SB = "oam.renderkit.AJAX_PARAM_SB";
76  
77      public void decode(FacesContext context, UIComponent component,
78                         ClientBehavior behavior)
79      {
80          assertBehavior(behavior);
81          AjaxBehavior ajaxBehavior = (AjaxBehavior) behavior;
82          if (ajaxBehavior.isDisabled() || !component.isRendered())
83          {
84              return;
85          }
86  
87          dispatchBehaviorEvent(component, ajaxBehavior);
88      }
89  
90  
91      public String getScript(ClientBehaviorContext behaviorContext,
92                              ClientBehavior behavior)
93      {
94          assertBehavior(behavior);
95          AjaxBehavior ajaxBehavior = (AjaxBehavior) behavior;
96  
97          if (ajaxBehavior.isDisabled())
98          {
99              return null;
100         }
101 
102         return makeAjax(behaviorContext, ajaxBehavior).toString();
103     }
104 
105 
106     private final void dispatchBehaviorEvent(UIComponent component, AjaxBehavior ajaxBehavior)
107     {
108         AjaxBehaviorEvent event = new AjaxBehaviorEvent(component, ajaxBehavior);
109         
110         boolean isImmediate = false;
111         if (ajaxBehavior.isImmediateSet())
112         {
113             isImmediate = ajaxBehavior.isImmediate();
114         }            
115         else
116         {
117             isImmediate = isComponentImmediate(component);
118         }
119         PhaseId phaseId = isImmediate ?
120                 PhaseId.APPLY_REQUEST_VALUES :
121                 PhaseId.INVOKE_APPLICATION;
122 
123         event.setPhaseId(phaseId);
124 
125         component.queueEvent(event);
126     }
127 
128 
129     private final boolean isComponentImmediate(UIComponent component)
130     {
131         boolean isImmediate = false;
132         if (component instanceof EditableValueHolder)
133         {
134             isImmediate = ((EditableValueHolder)component).isImmediate();
135         }
136         else if (component instanceof ActionSource)
137         {
138             isImmediate = ((ActionSource)component).isImmediate();
139         }
140         return isImmediate;
141     }
142 
143 
144     /**
145      * builds the generic ajax call depending upon
146      * the ajax behavior parameters
147      *
148      * @param context  the Client behavior context
149      * @param behavior the behavior
150      * @return a fully working javascript with calls into jsf.js
151      */
152     private final StringBuilder makeAjax(ClientBehaviorContext context, AjaxBehavior behavior)
153     {
154         StringBuilder retVal = SharedStringBuilder.get(context.getFacesContext(), AJAX_SB, 60);
155         StringBuilder paramBuffer = SharedStringBuilder.get(context.getFacesContext(), AJAX_PARAM_SB, 20);
156 
157         String executes = mapToString(context, paramBuffer, AJAX_KEY_EXECUTE, behavior.getExecute());
158         String render = mapToString(context, paramBuffer, AJAX_KEY_RENDER, behavior.getRender());
159 
160         String onError = behavior.getOnerror();
161         if (onError != null && !onError.trim().equals(EMPTY))
162         {
163             //onError = AJAX_KEY_ONERROR + COLON + onError;
164             paramBuffer.setLength(0);
165             paramBuffer.append(AJAX_KEY_ONERROR);
166             paramBuffer.append(COLON);
167             paramBuffer.append(onError);
168             onError = paramBuffer.toString();
169         }
170         else
171         {
172             onError = null;
173         }
174         String onEvent = behavior.getOnevent();
175         if (onEvent != null && !onEvent.trim().equals(EMPTY))
176         {
177             paramBuffer.setLength(0);
178             paramBuffer.append(AJAX_KEY_ONEVENT);
179             paramBuffer.append(COLON);
180             paramBuffer.append(onEvent);
181             onEvent = paramBuffer.toString();
182         }
183         else
184         {
185             onEvent = null;
186         }
187         /*
188          * since version 2.2
189          */
190         String delay = behavior.getDelay();
191         if (delay != null && !delay.trim().equals(EMPTY))
192         {
193             paramBuffer.setLength(0);
194             paramBuffer.append(AJAX_KEY_DELAY);
195             paramBuffer.append(COLON);
196             if ("none".equals(delay))
197             {
198                 paramBuffer.append('\'');
199                 paramBuffer.append(delay);
200                 paramBuffer.append('\'');
201             }
202             else
203             {
204                 paramBuffer.append(delay);
205             }
206             delay = paramBuffer.toString();
207         }
208         else
209         {
210             delay = null;
211         }
212         /*
213          * since version 2.2
214          */
215         String resetValues = Boolean.toString(behavior.isResetValues());
216         if (resetValues.equals("true"))
217         {
218             paramBuffer.setLength(0);
219             paramBuffer.append(AJAX_KEY_RESETVALUES);
220             paramBuffer.append(COLON);
221             paramBuffer.append(resetValues);
222             resetValues = paramBuffer.toString();
223         }
224         else
225         {
226             resetValues = null;
227         }
228 
229         String sourceId = null;
230         if (context.getSourceId() == null) 
231         {
232             sourceId = AJAX_VAL_THIS;
233         }
234         else
235         {
236             paramBuffer.setLength(0);
237             paramBuffer.append('\'');
238             paramBuffer.append(context.getSourceId());
239             paramBuffer.append('\'');
240             sourceId = paramBuffer.toString();
241             
242             if (!context.getSourceId().trim().equals(
243                 context.getComponent().getClientId(context.getFacesContext())))
244             {
245                 // Check if sourceId is not a clientId and there is no execute set
246                 UIComponent ref = context.getComponent();
247                 ref = (ref.getParent() == null) ? ref : ref.getParent();
248                 UIComponent instance = null;
249                 try
250                 {
251                     instance = ref.findComponent(context.getSourceId());
252                 }
253                 catch (IllegalArgumentException e)
254                 {
255                     // No Op
256                 }
257                 if (instance == null && executes == null)
258                 {
259                     // set the clientId of the component so the behavior can be decoded later, 
260                     // otherwise the behavior will fail
261                     List<String> list = new ArrayList<String>();
262                     list.add(context.getComponent().getClientId(context.getFacesContext()));
263                     executes = mapToString(context, paramBuffer, AJAX_KEY_EXECUTE, list);
264                 }
265             }
266         }
267 
268 
269         String event = context.getEventName();
270 
271         retVal.append(JS_AJAX_REQUEST);
272         retVal.append(L_PAREN);
273         retVal.append(sourceId);
274         retVal.append(COMMA);
275         retVal.append(AJAX_VAL_EVENT);
276         retVal.append(COMMA);
277 
278         Collection<ClientBehaviorContext.Parameter> params = context.getParameters();
279         int paramSize = (params != null) ? params.size() : 0;
280 
281         List<String> parameterList = new ArrayList<String>(paramSize + 2);
282         if (executes != null)
283         {
284             parameterList.add(executes);
285         }
286         if (render != null)
287         {
288             parameterList.add(render);
289         }
290         if (onError != null)
291         {
292             parameterList.add(onError);
293         }
294         if (onEvent != null)
295         {
296             parameterList.add(onEvent);
297         }
298         /*
299          * since version 2.2
300          */
301         if (delay != null)
302         {
303             parameterList.add(delay);
304         }
305         /*
306          * since version 2.2
307          */
308         if (resetValues != null)
309         {
310             parameterList.add(resetValues);
311         }
312         if (paramSize > 0)
313         {
314             /**
315              * see ClientBehaviorContext.html of the spec
316              * the param list has to be added in the post back
317              */
318             // params are in 99% RamdonAccess instace created in 
319             // HtmlRendererUtils.getClientBehaviorContextParameters(Map<String, String>)
320             if (params instanceof RandomAccess)
321             {
322                 List<ClientBehaviorContext.Parameter> list = (List<ClientBehaviorContext.Parameter>) params;
323                 for (int i = 0, size = list.size(); i < size; i++)
324                 {
325                     ClientBehaviorContext.Parameter param = list.get(i);
326                     append(paramBuffer, parameterList, param);
327                 }
328             }
329             else
330             {
331                 for (ClientBehaviorContext.Parameter param : params)
332                 {
333                     append(paramBuffer, parameterList, param);
334                 }
335             }
336         }
337 
338         //parameterList.add(QUOTE + BEHAVIOR_EVENT + QUOTE + COLON + QUOTE + event + QUOTE);
339         paramBuffer.setLength(0);
340         paramBuffer.append(QUOTE);
341         paramBuffer.append(BEHAVIOR_EVENT);
342         paramBuffer.append(QUOTE);
343         paramBuffer.append(COLON);
344         paramBuffer.append(QUOTE);
345         paramBuffer.append(event);
346         paramBuffer.append(QUOTE);
347         parameterList.add(paramBuffer.toString());
348         
349         /**
350          * I assume here for now that the options are the same which also
351          * can be sent via the options attribute to javax.faces.ajax
352          * this still needs further clarifications but I assume so for now
353          */
354         retVal.append(buildOptions(paramBuffer, parameterList));
355 
356         retVal.append(R_PAREN);
357 
358         return retVal;
359     }
360 
361     private void append(StringBuilder paramBuffer, List<String> parameterList, ClientBehaviorContext.Parameter param)
362     {
363         //TODO we may need a proper type handling in this part
364         //lets leave it for now as it is
365         //quotes etc.. should be transferred directly
366         //and the rest is up to the toString properly implemented
367         //ANS: Both name and value should be quoted
368         paramBuffer.setLength(0);
369         paramBuffer.append(QUOTE);
370         paramBuffer.append(param.getName());
371         paramBuffer.append(QUOTE);
372         paramBuffer.append(COLON);
373         paramBuffer.append(QUOTE);
374         paramBuffer.append(param.getValue().toString());
375         paramBuffer.append(QUOTE);
376         parameterList.add(paramBuffer.toString());
377     }
378 
379 
380     private StringBuilder buildOptions(StringBuilder retVal, List<String> options)
381     {
382         retVal.setLength(0);
383 
384         retVal.append("{");
385 
386         boolean first = true;
387 
388         for (int i = 0, size = options.size(); i < size; i++)
389         {
390             String option = options.get(i);
391             if (option != null && !option.trim().equals(EMPTY))
392             {
393                 if (!first)
394                 {
395                     retVal.append(COMMA);
396                 }
397                 else
398                 {
399                     first = false;
400                 }
401                 retVal.append(option);
402             }
403         }
404         retVal.append("}");
405         return retVal;
406     }
407 
408     private final String mapToString(ClientBehaviorContext context, StringBuilder retVal, 
409             String target, Collection<String> dataHolder)
410     {
411         //Clear buffer
412         retVal.setLength(0);
413 
414         if (dataHolder == null)
415         {
416             dataHolder = Collections.emptyList();
417         }
418         int executeSize = dataHolder.size();
419         if (executeSize > 0)
420         {
421 
422             retVal.append(target);
423             retVal.append(COLON);
424             retVal.append(QUOTE);
425 
426             int cnt = 0;
427             
428             // perf: dataHolder is a Collection : ajaxBehaviour.getExecute() 
429             // and ajaxBehaviour.getRender() API
430             // In most cases comes here a ArrayList, because 
431             // javax.faces.component.behavior.AjaxBehavior.getCollectionFromSpaceSplitString
432             // creates it.
433             if (dataHolder instanceof RandomAccess)
434             {
435                 List<String> list = (List<String>) dataHolder;
436                 for (; cnt  < executeSize; cnt++)
437                 {
438                     String strVal = list.get(cnt);
439                     build(context, executeSize, retVal, cnt, strVal);
440                 }
441             }
442             else
443             {
444                 for (String strVal : dataHolder)
445                 {
446                     cnt++;
447                     build(context, executeSize, retVal, cnt, strVal);
448                 }
449             }
450 
451             retVal.append(QUOTE);
452             return retVal.toString();
453         }
454         return null;
455 
456     }
457 
458     public void build(ClientBehaviorContext context,
459             int size, StringBuilder retVal, int cnt,
460             String strVal)
461     {
462         strVal = strVal.trim();
463         if (!EMPTY.equals(strVal))
464         {
465             if (!strVal.startsWith(IDENTIFYER_MARKER))
466             {
467                 String componentId = getComponentId(context, strVal);
468                 retVal.append(componentId);
469             }
470             else
471             {
472                 // @this should be resolved server side, because there are cases like in h:selectOneRadio where
473                 // the id of the tag is not a clientId. @this could be valid to be resolve in the client if 
474                 // jsf.ajax.request(...) is called manually (without f:ajax intervention)
475                 if (strVal.equalsIgnoreCase("@this"))
476                 {
477                     retVal.append(context.getComponent().getClientId(context.getFacesContext()));
478                 }
479                 else
480                 {
481                     retVal.append(strVal);
482                 }
483             }
484             if (cnt < size)
485             {
486                 retVal.append(BLANK);
487             }
488         }
489     }
490 
491     private final String getComponentId(ClientBehaviorContext context, String id)
492     {
493 
494         UIComponent contextComponent = context.getComponent();
495         UIComponent target = contextComponent.findComponent(id);
496         if (target == null)
497         {
498             target = contextComponent.findComponent(
499                 context.getFacesContext().getNamingContainerSeparatorChar() + id);
500         }
501         if (target != null)
502         {
503             return target.getClientId(context.getFacesContext());
504         }
505         throw new FacesException("Component with id:" + id + " not found");
506     }
507 
508     private final void assertBehavior(ClientBehavior behavior)
509     {
510         if (!(behavior instanceof AjaxBehavior))
511         {
512             throw new FacesException(ERR_NO_AJAX_BEHAVIOR);
513         }
514     }
515 
516 }