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  
20  package org.apache.myfaces.renderkit.html;
21  
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.EnumSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.RandomAccess;
29  import java.util.Set;
30  import javax.faces.component.UIComponent;
31  import javax.faces.component.UIParameter;
32  import javax.faces.component.behavior.AjaxBehavior;
33  import javax.faces.component.behavior.ClientBehaviorContext;
34  import javax.faces.component.behavior.ClientBehaviorHolder;
35  import javax.faces.component.html.HtmlCommandScript;
36  import javax.faces.component.search.SearchExpressionContext;
37  import javax.faces.component.search.SearchExpressionHandler;
38  import javax.faces.component.search.SearchExpressionHint;
39  import javax.faces.context.FacesContext;
40  import javax.faces.context.ResponseWriter;
41  import javax.faces.event.ActionEvent;
42  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFRenderer;
43  import org.apache.myfaces.shared.renderkit.RendererUtils;
44  import org.apache.myfaces.shared.renderkit.html.HTML;
45  import org.apache.myfaces.shared.renderkit.html.HtmlRenderer;
46  import org.apache.myfaces.shared.renderkit.html.HtmlRendererUtils;
47  import org.apache.myfaces.shared.renderkit.html.util.FormInfo;
48  import org.apache.myfaces.shared.renderkit.html.util.JavascriptUtils;
49  import org.apache.myfaces.shared.renderkit.html.util.ResourceUtils;
50  import org.apache.myfaces.shared.renderkit.html.util.SharedStringBuilder;
51  import org.apache.myfaces.shared.util.StringUtils;
52  
53  @JSFRenderer(
54      renderKitId="HTML_BASIC",
55      family="javax.faces.Command",
56      type="javax.faces.Script")
57  public class HtmlCommandScriptRenderer extends HtmlRenderer
58  {
59      private static final Set<SearchExpressionHint> EXPRESSION_HINTS =
60              EnumSet.of(SearchExpressionHint.RESOLVE_CLIENT_SIDE, SearchExpressionHint.RESOLVE_SINGLE_COMPONENT);
61      
62      private static final String AJAX_KEY_ONERROR = "onerror";
63      private static final String AJAX_KEY_ONEVENT = "onevent";
64      private static final String AJAX_KEY_EXECUTE = "execute";
65      private static final String AJAX_KEY_RENDER = "render";
66      private static final String AJAX_KEY_DELAY = "delay";
67      private static final String AJAX_KEY_RESETVALUES = "resetValues";
68  
69      private static final String AJAX_VAL_THIS = "this";
70      private static final String JS_AJAX_REQUEST = "jsf.ajax.request";
71      
72      private static final String AJAX_SB = "oam.renderkit.AJAX_SB";
73      private static final String AJAX_PARAM_SB = "oam.renderkit.AJAX_PARAM_SB";
74      
75      @Override
76      public void encodeBegin(FacesContext context, UIComponent component) throws IOException
77      {
78          super.encodeBegin(context, component);
79  
80          HtmlCommandScript commandScript = (HtmlCommandScript) component;
81          ResponseWriter writer = context.getResponseWriter();
82          
83          ResourceUtils.renderDefaultJsfJsInlineIfNecessary(context, writer);
84          
85          writer.startElement(HTML.SPAN_ELEM, component);
86          writer.writeAttribute(HTML.ID_ATTR, component.getClientId(context), null);
87          writer.startElement(HTML.SCRIPT_ELEM, component);
88          writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
89          
90          HtmlRendererUtils.ScriptContext script = new HtmlRendererUtils.ScriptContext();
91          
92          //Write content
93          String cmdName = commandScript.getName();
94          String name;
95          if (cmdName != null && cmdName.length() > 0)
96          {
97              name = JavascriptUtils.getValidJavascriptName(cmdName, true);
98          }
99          else
100         {
101             name = JavascriptUtils.getValidJavascriptName(component.getClientId(context), true);
102         }
103         
104         script.prettyLine();
105         script.increaseIndent();
106         script.append("var "+name+" = function(o){var o=(typeof o==='object')&&o?o:{};");
107         script.prettyLine();
108         
109         // TODO ajaxBehavior not required actually....
110         AjaxBehavior ajaxBehavior = new AjaxBehavior();
111         Boolean resetValues = commandScript.getResetValues();
112         if (resetValues != null)
113         {
114             ajaxBehavior.setResetValues(resetValues);
115         }
116         ajaxBehavior.setOnerror(commandScript.getOnerror());
117         ajaxBehavior.setOnevent(commandScript.getOnevent());
118         
119         Collection<ClientBehaviorContext.Parameter> eventParameters = null;    
120         //eventParameters.add(new ClientBehaviorContext.Parameter("params", "o"));
121         ClientBehaviorContext ccc = ClientBehaviorContext.createClientBehaviorContext(
122                                     context, component, "action",
123                                     commandScript.getClientId(context), eventParameters);
124         
125         script.append(makeAjax(context, ccc, ajaxBehavior, commandScript).toString());
126         script.decreaseIndent();
127         script.append("}");
128         
129         
130         if (commandScript.isAutorun())
131         {
132             script.append(";");
133             script.append("myfaces._impl.core._Runtime.addOnLoad(window,");
134             script.append(name);
135             script.append(");");
136         }
137         
138         writer.writeText(script.toString(), null);
139     }
140     
141     @Override
142     public void encodeEnd(FacesContext context, UIComponent component) throws IOException
143     {
144         super.encodeEnd(context, component); //
145         ResponseWriter writer = context.getResponseWriter();
146         writer.endElement(HTML.SCRIPT_ELEM);
147         writer.endElement(HTML.SPAN_ELEM);
148     }
149 
150     @Override
151     public void decode(FacesContext facesContext, UIComponent component)
152     {
153         super.decode(facesContext, component); 
154         
155         HtmlCommandScript commandScript = (HtmlCommandScript) component;
156         
157         if (HtmlRendererUtils.isDisabled(component) || !commandScript.isRendered())
158         {
159             return;
160         }
161         
162         Map<String, String> paramMap = facesContext.getExternalContext().getRequestParameterMap();
163         String behaviorEventName = paramMap.get(ClientBehaviorContext.BEHAVIOR_EVENT_PARAM_NAME);
164         if (behaviorEventName != null)
165         {
166             String sourceId = paramMap.get(ClientBehaviorContext.BEHAVIOR_SOURCE_PARAM_NAME);
167             String componentClientId = component.getClientId(facesContext);
168             String clientId = sourceId;
169             if (sourceId.startsWith(componentClientId) && sourceId.length() > componentClientId.length())
170             {
171                 String item = sourceId.substring(componentClientId.length()+1);
172                 // If is item it should be an integer number, otherwise it can be related to a child 
173                 // component, because that could conflict with the clientId naming convention.
174                 if (StringUtils.isInteger(item))
175                 {
176                     clientId = componentClientId;
177                 }
178             }
179             if (component.getClientId(facesContext).equals(clientId))
180             {
181                 boolean disabled = HtmlRendererUtils.isDisabled(component);
182                 FormInfo formInfo = RendererUtils.findNestingForm(component, facesContext);
183                 boolean activateActionEvent = false;
184                 if (formInfo != null && !disabled)
185                 {
186                     String reqValue = (String) facesContext.getExternalContext().getRequestParameterMap().get(
187                             HtmlRendererUtils.getHiddenCommandLinkFieldName(formInfo, facesContext));
188                     activateActionEvent = reqValue != null && reqValue.equals(clientId)
189                         || HtmlRendererUtils.isPartialOrBehaviorSubmit(facesContext, clientId);
190                     if (activateActionEvent)
191                     {
192                         RendererUtils.initPartialValidationAndModelUpdate(component, facesContext);
193                     }
194                 }
195                 if (activateActionEvent)
196                 {
197                     component.queueEvent(new ActionEvent(component));
198                 }
199             }
200         }
201         if (component instanceof ClientBehaviorHolder && !HtmlRendererUtils.isDisabled(component))
202         {
203             HtmlRendererUtils.decodeClientBehaviors(facesContext, component);
204         }
205     }
206 
207     /**
208      * builds the generic ajax call depending upon
209      * the ajax behavior parameters
210      *
211      * @param context  the Client behavior context
212      * @param behavior the behavior
213      * @param commandScript the component
214      * @return a fully working javascript with calls into jsf.js
215      */
216     private StringBuilder makeAjax(FacesContext facesContext, ClientBehaviorContext context, AjaxBehavior behavior,
217             HtmlCommandScript commandScript)
218     {
219         StringBuilder retVal = SharedStringBuilder.get(context.getFacesContext(), AJAX_SB, 60);
220         StringBuilder paramBuffer = SharedStringBuilder.get(context.getFacesContext(), AJAX_PARAM_SB, 20);
221     
222         SearchExpressionContext searchExpressionContext = SearchExpressionContext.createSearchExpressionContext(
223                             context.getFacesContext(), context.getComponent(), EXPRESSION_HINTS, null);
224         
225         String executes = resolveExpressionsAsParameter(paramBuffer, AJAX_KEY_EXECUTE, commandScript.getExecute(),
226                 searchExpressionContext);
227         String render = resolveExpressionsAsParameter(paramBuffer, AJAX_KEY_RENDER, commandScript.getRender(),
228                 searchExpressionContext);
229 
230         String onError = behavior.getOnerror();
231         if (onError != null && !onError.trim().isEmpty())
232         {
233             //onError = AJAX_KEY_ONERROR + COLON + onError;
234             paramBuffer.setLength(0);
235             paramBuffer.append(AJAX_KEY_ONERROR);
236             paramBuffer.append(':');
237             paramBuffer.append(onError);
238             onError = paramBuffer.toString();
239         }
240         else
241         {
242             onError = null;
243         }
244         String onEvent = behavior.getOnevent();
245         if (onEvent != null && !onEvent.trim().isEmpty())
246         {
247             paramBuffer.setLength(0);
248             paramBuffer.append(AJAX_KEY_ONEVENT);
249             paramBuffer.append(':');
250             paramBuffer.append(onEvent);
251             onEvent = paramBuffer.toString();
252         }
253         else
254         {
255             onEvent = null;
256         }
257 
258         String delay = behavior.getDelay();
259         if (delay != null && !delay.trim().isEmpty())
260         {
261             paramBuffer.setLength(0);
262             paramBuffer.append(AJAX_KEY_DELAY);
263             paramBuffer.append(':');
264             if ("none".equals(delay))
265             {
266                 paramBuffer.append('\'');
267                 paramBuffer.append(delay);
268                 paramBuffer.append('\'');
269             }
270             else
271             {
272                 paramBuffer.append(delay);
273             }
274             delay = paramBuffer.toString();
275         }
276         else
277         {
278             delay = null;
279         }
280 
281         String resetValues = Boolean.toString(behavior.isResetValues());
282         if (resetValues.equals("true"))
283         {
284             paramBuffer.setLength(0);
285             paramBuffer.append(AJAX_KEY_RESETVALUES);
286             paramBuffer.append(':');
287             paramBuffer.append(resetValues);
288             resetValues = paramBuffer.toString();
289         }
290         else
291         {
292             resetValues = null;
293         }
294 
295         String sourceId = null;
296         if (context.getSourceId() == null)
297         {
298             sourceId = AJAX_VAL_THIS;
299         }
300         else
301         {
302             paramBuffer.setLength(0);
303             paramBuffer.append('\'');
304             paramBuffer.append(context.getSourceId());
305             paramBuffer.append('\'');
306             sourceId = paramBuffer.toString();
307 
308             if (!context.getSourceId().trim().equals(
309                 context.getComponent().getClientId(context.getFacesContext())))
310             {
311                 // Check if sourceId is not a clientId and there is no execute set
312                 UIComponent ref = context.getComponent();
313                 ref = (ref.getParent() == null) ? ref : ref.getParent();
314                 UIComponent instance = null;
315                 try
316                 {
317                     instance = ref.findComponent(context.getSourceId());
318                 }
319                 catch (IllegalArgumentException e)
320                 {
321                     // No Op
322                 }
323                 if (instance == null && executes == null)
324                 {
325                     // set the clientId of the component so the behavior can be decoded later,
326                     // otherwise the behavior will fail
327                     executes = resolveExpressionsAsParameter(paramBuffer, AJAX_KEY_EXECUTE,
328                             context.getComponent().getClientId(context.getFacesContext()), searchExpressionContext);
329                 }
330             }
331         }
332 
333         String event = context.getEventName();
334 
335         retVal.append(JS_AJAX_REQUEST);
336         retVal.append('(');
337         retVal.append(sourceId);
338         retVal.append(",window.event,myfaces._impl._util._Lang.mixMaps(");
339         
340         Collection<ClientBehaviorContext.Parameter> params = context.getParameters();
341         int paramSize = (params != null) ? params.size() : 0;
342 
343         List<String> parameterList = new ArrayList<>(paramSize + 2);
344         if (executes != null)
345         {
346             parameterList.add(executes);
347         }
348         if (render != null)
349         {
350             parameterList.add(render);
351         }
352         if (onError != null)
353         {
354             parameterList.add(onError);
355         }
356         if (onEvent != null)
357         {
358             parameterList.add(onEvent);
359         }
360         if (delay != null)
361         {
362             parameterList.add(delay);
363         }
364         if (resetValues != null)
365         {
366             parameterList.add(resetValues);
367         }
368         if (paramSize > 0)
369         {
370             /**
371              * see ClientBehaviorContext.html of the spec
372              * the param list has to be added in the post back
373              */
374             // params are in 99% RamdonAccess instace created in
375             // HtmlRendererUtils.getClientBehaviorContextParameters(Map<String, String>)
376             if (params instanceof RandomAccess)
377             {
378                 List<ClientBehaviorContext.Parameter> list = (List<ClientBehaviorContext.Parameter>) params;
379                 for (int i = 0, size = list.size(); i < size; i++)
380                 {
381                     ClientBehaviorContext.Parameter param = list.get(i);
382                     append(paramBuffer, parameterList, param.getName(), param.getValue());
383                 }
384             }
385             else
386             {
387                 for (ClientBehaviorContext.Parameter param : params)
388                 {
389                     append(paramBuffer, parameterList, param.getName(), param.getValue());
390                 }
391             }
392         }
393         
394         List<UIParameter> uiParams = HtmlRendererUtils.getValidUIParameterChildren(
395                 facesContext, commandScript.getChildren(), false, false);
396         if (uiParams != null && uiParams.size() > 0)
397         {
398             for (int i = 0, size = uiParams.size(); i < size; i++)
399             {
400                 UIParameter param = uiParams.get(i);
401                 append(paramBuffer, parameterList, param.getName(), param.getValue());
402             }
403         }
404             
405         paramBuffer.setLength(0);
406         paramBuffer.append('\'');
407         paramBuffer.append(ClientBehaviorContext.BEHAVIOR_EVENT_PARAM_NAME);
408         paramBuffer.append("\':\'");
409         paramBuffer.append(event);
410         paramBuffer.append('\'');
411         parameterList.add(paramBuffer.toString());
412 
413         /**
414          * I assume here for now that the options are the same which also
415          * can be sent via the options attribute to javax.faces.ajax
416          * this still needs further clarifications but I assume so for now
417          */
418         retVal.append(buildOptions(paramBuffer, parameterList));
419 
420         //mixMaps
421         retVal.append(",o,false))");
422 
423         return retVal;
424     }
425 
426     private void append(StringBuilder paramBuffer, List<String> parameterList, String paramName, Object paramValue)
427     {
428         //TODO we may need a proper type handling in this part
429         //lets leave it for now as it is
430         //quotes etc.. should be transferred directly
431         //and the rest is up to the toString properly implemented
432         //ANS: Both name and value should be quoted
433         paramBuffer.setLength(0);
434         paramBuffer.append('\'');
435         paramBuffer.append(paramName);
436         paramBuffer.append("\':\'");
437         if (paramValue != null)
438         {
439             paramBuffer.append(paramValue.toString());
440         }
441         paramBuffer.append('\'');
442         parameterList.add(paramBuffer.toString());
443     }
444 
445     private StringBuilder buildOptions(StringBuilder retVal, List<String> options)
446     {
447         retVal.setLength(0);
448 
449         retVal.append("{");
450 
451         boolean first = true;
452 
453         for (int i = 0, size = options.size(); i < size; i++)
454         {
455             String option = options.get(i);
456             if (option != null && !option.trim().isEmpty())
457             {
458                 if (!first)
459                 {
460                     retVal.append(',');
461                 }
462                 else
463                 {
464                     first = false;
465                 }
466                 retVal.append(option);
467             }
468         }
469         retVal.append("}");
470         return retVal;
471     }
472 
473     private String resolveExpressionsAsParameter(StringBuilder retVal, String target, String expressions,
474             SearchExpressionContext searchExpressionContext)
475     {
476         if (expressions != null && !expressions.trim().isEmpty())
477         {
478             retVal.setLength(0);
479             retVal.append(target);
480             retVal.append(':');
481             retVal.append('\'');
482 
483             SearchExpressionHandler handler =
484                     searchExpressionContext.getFacesContext().getApplication().getSearchExpressionHandler();
485             List<String> clientIds =
486                     handler.resolveClientIds(searchExpressionContext, expressions);
487             
488             if (clientIds != null && !clientIds.isEmpty())
489             {
490                 for (int i = 0; i < clientIds.size(); i++)
491                 {
492                     if (i > 0)
493                     {
494                         retVal.append(' ');
495                     }
496                     retVal.append(clientIds.get(i));
497                 }
498             }
499             
500             retVal.append('\'');
501             return retVal.toString();
502         }
503 
504         return null;
505     }
506 }