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.shared.renderkit.html;
20  
21  import org.apache.myfaces.shared.renderkit.JSFAttr;
22  import org.apache.myfaces.shared.renderkit.RendererUtils;
23  import org.apache.myfaces.shared.renderkit.html.util.FormInfo;
24  import org.apache.myfaces.shared.renderkit.html.util.JavascriptUtils;
25  import org.apache.myfaces.shared.util._ComponentUtils;
26  import org.apache.myfaces.shared.config.MyfacesConfig;
27  
28  import javax.faces.application.ViewHandler;
29  import javax.faces.component.UICommand;
30  import javax.faces.component.UIComponent;
31  import javax.faces.component.UIOutput;
32  import javax.faces.component.UIParameter;
33  import javax.faces.component.html.HtmlCommandLink;
34  import javax.faces.context.FacesContext;
35  import javax.faces.context.ResponseWriter;
36  import javax.faces.event.ActionEvent;
37  import java.io.IOException;
38  import java.io.UnsupportedEncodingException;
39  import java.net.URLEncoder;
40  import java.util.Iterator;
41  
42  /***
43   * @author Manfred Geiler
44   * @version $Revision: 947903 $ $Date: 2010-05-24 22:40:23 -0500 (Mon, 24 May 2010) $
45   */
46  public abstract class HtmlLinkRendererBase
47      extends HtmlRenderer
48  {
49      /* this one is never used
50      public static final String URL_STATE_MARKER      = "JSF_URL_STATE_MARKER=DUMMY";
51      public static final int    URL_STATE_MARKER_LEN  = URL_STATE_MARKER.length();
52      */
53  
54      //private static final Log log = LogFactory.getLog(HtmlLinkRenderer.class);
55  
56      public boolean getRendersChildren()
57      {
58          // We must be able to render the children without a surrounding anchor
59          // if the Link is disabled
60          return true;
61      }
62  
63      public void decode(FacesContext facesContext, UIComponent component)
64      {
65          super.decode(facesContext, component);  //check for NP
66  
67          if (component instanceof UICommand)
68          {
69              String clientId = component.getClientId(facesContext);
70              FormInfo formInfo = findNestingForm(component, facesContext);
71              String reqValue = (String) facesContext.getExternalContext().getRequestParameterMap().get(
72                  HtmlRendererUtils.getHiddenCommandLinkFieldName(formInfo));
73              if (reqValue != null && reqValue.equals(clientId))
74              {
75                  component.queueEvent(new ActionEvent(component));
76  
77                  RendererUtils.initPartialValidationAndModelUpdate(component, facesContext);
78              }
79          }
80          else if (component instanceof UIOutput)
81          {
82              //do nothing
83          }
84          else
85          {
86              throw new IllegalArgumentException("Unsupported component class " + component.getClass().getName());
87          }
88      }
89  
90  
91      public void encodeBegin(FacesContext facesContext, UIComponent component) throws IOException
92      {
93          super.encodeBegin(facesContext, component);  //check for NP
94  
95          if (component instanceof UICommand)
96          {
97              renderCommandLinkStart(facesContext, component,
98                                     component.getClientId(facesContext),
99                                     ((UICommand)component).getValue(),
100                                    getStyle(facesContext, component),
101                                    getStyleClass(facesContext, component));
102         }
103         else if (component instanceof UIOutput)
104         {
105             renderOutputLinkStart(facesContext, (UIOutput)component);
106         }
107         else
108         {
109             throw new IllegalArgumentException("Unsupported component class " + component.getClass().getName());
110         }
111     }
112 
113 
114     /***
115      * Can be overwritten by derived classes to overrule the style to be used.
116      */
117     protected String getStyle(FacesContext facesContext, UIComponent link)
118     {
119         if (link instanceof HtmlCommandLink)
120         {
121             return ((HtmlCommandLink)link).getStyle();
122         }
123 
124         return (String)link.getAttributes().get(HTML.STYLE_ATTR);
125 
126     }
127 
128     /***
129      * Can be overwritten by derived classes to overrule the style class to be used.
130      */
131     protected String getStyleClass(FacesContext facesContext, UIComponent link)
132     {
133         if (link instanceof HtmlCommandLink)
134         {
135             return ((HtmlCommandLink)link).getStyleClass();
136         }
137 
138         return (String)link.getAttributes().get(HTML.STYLE_CLASS_ATTR);
139 
140     }
141 
142     public void encodeChildren(FacesContext facesContext, UIComponent component) throws IOException
143     {
144         RendererUtils.renderChildren(facesContext, component);
145     }
146 
147     public void encodeEnd(FacesContext facesContext, UIComponent component) throws IOException
148     {
149         super.encodeEnd(facesContext, component);  //check for NP
150 
151         if (component instanceof UICommand)
152         {
153                 renderCommandLinkEnd(facesContext, component);
154 
155             HtmlFormRendererBase.renderScrollHiddenInputIfNecessary(
156                 findNestingForm(component, facesContext).getForm(), facesContext, facesContext.getResponseWriter());
157         }
158         else if (component instanceof UIOutput)
159         {
160             renderOutputLinkEnd(facesContext, component);
161         }
162         else
163         {
164             throw new IllegalArgumentException("Unsupported component class " + component.getClass().getName());
165         }
166     }
167 
168     protected void renderCommandLinkStart(FacesContext facesContext, UIComponent component,
169                                           String clientId,
170                                           Object value,
171                                           String style,
172                                           String styleClass)
173             throws IOException
174     {
175         ResponseWriter writer = facesContext.getResponseWriter();
176 
177         if (HtmlRendererUtils.isDisabled(component))
178         {
179             writer.startElement(HTML.SPAN_ELEM, component);
180             HtmlRendererUtils.writeIdIfNecessary(writer, component, facesContext);
181             HtmlRendererUtils.renderHTMLAttributes(writer, component, HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES);
182         }
183         else
184         {
185             String[] anchorAttrsToRender;
186             if (JavascriptUtils.isJavascriptAllowed(facesContext.getExternalContext()))
187             {
188                 renderJavaScriptAnchorStart(facesContext, writer, component, clientId);
189                 anchorAttrsToRender = HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES_WITHOUT_ONCLICK_WITHOUT_STYLE;
190             }
191             else
192             {
193                 renderNonJavaScriptAnchorStart(facesContext, writer, component, clientId);
194                 anchorAttrsToRender = HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES_WITHOUT_STYLE;
195             }
196 
197             HtmlRendererUtils.writeIdIfNecessary(writer, component, facesContext);
198             HtmlRendererUtils.renderHTMLAttributes(writer, component,
199                                                    anchorAttrsToRender);
200             HtmlRendererUtils.renderHTMLAttribute(writer, HTML.STYLE_ATTR, HTML.STYLE_ATTR,
201                                                   style);
202             HtmlRendererUtils.renderHTMLAttribute(writer, HTML.STYLE_CLASS_ATTR, HTML.STYLE_CLASS_ATTR,
203                                                   styleClass);
204         }
205 
206         // render value as required by JSF 1.1 renderkitdocs
207         if(value != null)
208         {
209             writer.writeText(value.toString(), JSFAttr.VALUE_ATTR);
210         }
211     }
212 
213     protected void renderJavaScriptAnchorStart(FacesContext facesContext,
214                                                ResponseWriter writer,
215                                                UIComponent component,
216                                                String clientId)
217         throws IOException
218     {
219         //Find form
220         FormInfo formInfo = findNestingForm(component, facesContext);
221         if (formInfo == null)
222         {
223             String path = RendererUtils.getPathToComponent(component);
224             String msg = "Link is not embedded in a form. Change component/tag '" + clientId + "' from javax.faces.*/<h:tagName /> " +
225                 "to org.apache.myfaces.*/<t:tagName />, or embed it in a form.  This is not a bug. " +
226                 "Please see: http://wiki.apache.org/myfaces/Upgrading_to_Tomahawk_1.1.3 " +
227                 "The path to this component is " + path + ". If you need to render a special form and a JSF-form's attributes are not enough," +
228                 "consider using the s:form tag of the MyFaces sandbox.";
229             throw new IllegalArgumentException(msg);
230         }
231         UIComponent nestingForm = formInfo.getForm();
232         String formName = formInfo.getFormName();
233 
234         StringBuffer onClick = new StringBuffer();
235 
236         String commandOnclick;
237         if (component instanceof HtmlCommandLink)
238         {
239             commandOnclick = ((HtmlCommandLink)component).getOnclick();
240         }
241         else
242         {
243             commandOnclick = (String)component.getAttributes().get(HTML.ONCLICK_ATTR);
244         }
245         if (commandOnclick != null)
246         {
247             onClick.append("var cf = function(){");
248             onClick.append(commandOnclick);
249             onClick.append('}');
250             onClick.append(';');
251             onClick.append("var oamSF = function(){");
252         }
253 
254         if (RendererUtils.isAdfOrTrinidadForm(formInfo.getForm())) {
255             onClick.append("submitForm('");
256             onClick.append(formInfo.getForm().getClientId(facesContext));
257             onClick.append("',1,{source:'");
258             onClick.append(component.getClientId(facesContext));
259             onClick.append("'});return false;");
260         }
261         else {
262             HtmlRendererUtils.renderFormSubmitScript(facesContext);
263 
264             StringBuffer params = addChildParameters(facesContext, component, nestingForm);
265 
266             String target = getTarget(component);
267 
268             onClick.append("return ").
269                 append(HtmlRendererUtils.SUBMIT_FORM_FN_NAME).append("('").
270                 append(formName).append("','").
271                 append(clientId).append("'");
272 
273             if (params.length() > 2 || target != null) {
274                 onClick.append(",").
275                     append(target == null ? "null" : ("'" + target + "'")).append(",").
276                     append(params);
277             }
278             onClick.append(");");
279 
280             //Not necessary since we are using oamSetHiddenInput to create input hidden fields
281             //render hidden field - todo: in here for backwards compatibility
282             if (MyfacesConfig.getCurrentInstance(facesContext.getExternalContext()).isRenderHiddenFieldsForLinkParams())
283             {
284                 String hiddenFieldName = HtmlRendererUtils.getHiddenCommandLinkFieldName(formInfo);
285                 addHiddenCommandParameter(facesContext, nestingForm, hiddenFieldName);
286             }
287 
288         }
289         
290         if (commandOnclick != null)
291         {
292             onClick.append('}');
293             onClick.append(';');
294             onClick.append("return (cf()==false)? false : oamSF();");
295         }        
296 
297         writer.startElement(HTML.ANCHOR_ELEM, component);
298         writer.writeURIAttribute(HTML.HREF_ATTR, "#", null);
299         writer.writeAttribute(HTML.ONCLICK_ATTR, onClick.toString(), null);
300     }
301 
302     private String getTarget(UIComponent component) {
303         // for performance reason: double check for the target attribute
304         String target;
305         if (component instanceof HtmlCommandLink) {
306             target = ((HtmlCommandLink) component).getTarget();
307         }
308         else {
309             target = (String) component.getAttributes().get(HTML.TARGET_ATTR);
310         }
311         return target;
312     }
313 
314     private StringBuffer addChildParameters(FacesContext context, UIComponent component, UIComponent nestingForm) {
315         //add child parameters
316         StringBuffer params = new StringBuffer();
317         params.append("[");
318         for (Iterator it = getChildren(component).iterator(); it.hasNext();) {
319 
320             UIComponent child = (UIComponent) it.next();
321             if (child instanceof UIParameter) {
322                 String name = ((UIParameter) child).getName();
323 
324                 if (name == null) {
325                     throw new IllegalArgumentException("Unnamed parameter value not allowed within command link.");
326                 }
327 
328                 //Not necessary, since we are using oamSetHiddenInput to create hidden fields
329                 if (MyfacesConfig.getCurrentInstance(context.getExternalContext()).isRenderHiddenFieldsForLinkParams())
330                 {
331                     addHiddenCommandParameter(context, nestingForm, name);
332                 }
333 
334                 Object value = ((UIParameter) child).getValue();
335 
336                 //UIParameter is no ValueHolder, so no conversion possible - calling .toString on value....
337                 // MYFACES-1832 bad charset encoding for f:param
338                 // if HTMLEncoder.encode is called, then
339                 // when is called on writer.writeAttribute, encode method
340                 // is called again so we have a duplicated encode
341                 // call.
342                 // MYFACES-2726 All '\' and "'" chars must be escaped 
343                 // because there will be inside "'" javascript quotes, 
344                 // otherwise the value will not correctly restored when
345                 // the command is post.
346                 //String strParamValue = value != null ? org.apache.myfaces.shared.renderkit.html.util.HTMLEncoder.encode(value.toString(), false, false) : "";
347                 String strParamValue = "";
348                 if (value != null)
349                 {
350                     strParamValue = value.toString();
351                     StringBuffer buff = null;
352                     for (int i = 0; i < strParamValue.length(); i++)
353                     {
354                         char c = strParamValue.charAt(i); 
355                         if (c == '\'' || c == '//')
356                         {
357                             if (buff == null)
358                             {
359                                 buff = new StringBuffer();
360                                 buff.append(strParamValue.substring(0,i));
361                             }
362                             buff.append('//');
363                             buff.append(c);
364                         }
365                         else if (buff != null)
366                         {
367                             buff.append(c);
368                         }
369                     }
370                     if (buff != null)
371                     {
372                         strParamValue = buff.toString();
373                     }
374                 }
375 
376                 if (params.length() > 1) {
377                     params.append(",");
378                 }
379 
380                 params.append("['");
381                 params.append(name);
382                 params.append("','");
383                 params.append(strParamValue);
384                 params.append("']");
385             }
386         }
387         params.append("]");
388         return params;
389     }
390 
391     /***
392      * find nesting form<br />
393      * need to be overrideable to deal with dummyForm stuff in tomahawk.
394      */
395     protected FormInfo findNestingForm(UIComponent uiComponent, FacesContext facesContext)
396     {
397         return _ComponentUtils.findNestingForm(uiComponent, facesContext);
398     }
399 
400     protected void addHiddenCommandParameter(FacesContext facesContext, UIComponent nestingForm, String hiddenFieldName)
401     {
402         if (nestingForm != null)
403         {
404             HtmlFormRendererBase.addHiddenCommandParameter(facesContext, nestingForm, hiddenFieldName);
405         }
406     }
407 
408 
409     protected void renderNonJavaScriptAnchorStart(FacesContext facesContext,
410                                                   ResponseWriter writer,
411                                                   UIComponent component,
412                                                   String clientId)
413         throws IOException
414     {
415         ViewHandler viewHandler = facesContext.getApplication().getViewHandler();
416         String viewId = facesContext.getViewRoot().getViewId();
417         String path = viewHandler.getActionURL(facesContext, viewId);
418 
419         boolean strictXhtmlLinks
420                 = MyfacesConfig.getCurrentInstance(facesContext.getExternalContext()).isStrictXhtmlLinks();
421 
422         StringBuffer hrefBuf = new StringBuffer(path);
423 
424         //add clientId parameter for decode
425 
426         if (path.indexOf('?') == -1)
427         {
428             hrefBuf.append('?');
429         }
430         else
431         {
432             if (strictXhtmlLinks) {
433                 hrefBuf.append("&amp;");
434             } else {
435                 hrefBuf.append('&');
436             }
437         }
438         String hiddenFieldName = HtmlRendererUtils.getHiddenCommandLinkFieldName(findNestingForm(component, facesContext));
439         hrefBuf.append(hiddenFieldName);
440         hrefBuf.append('=');
441         hrefBuf.append(clientId);
442 
443         if (getChildCount(component) > 0)
444         {
445             addChildParametersToHref(facesContext, component, hrefBuf,
446                                      false, //not the first url parameter
447                                      writer.getCharacterEncoding());
448         }
449 
450         String href = facesContext.getExternalContext().encodeActionURL(hrefBuf.toString());
451         writer.startElement(HTML.ANCHOR_ELEM, component);
452         writer.writeURIAttribute(HTML.HREF_ATTR,
453                                  facesContext.getExternalContext().encodeActionURL(href),
454                                  null);
455     }
456 
457     private void addChildParametersToHref(FacesContext facesContext,
458                                           UIComponent linkComponent,
459                                           StringBuffer hrefBuf,
460                                           boolean firstParameter,
461                                           String charEncoding)
462             throws IOException
463     {
464         boolean strictXhtmlLinks
465                 = MyfacesConfig.getCurrentInstance(facesContext.getExternalContext()).isStrictXhtmlLinks();
466         for (Iterator it = getChildren(linkComponent).iterator(); it.hasNext(); )
467         {
468             UIComponent child = (UIComponent)it.next();
469             if (child instanceof UIParameter)
470             {
471                 String name = ((UIParameter)child).getName();
472                 Object value = ((UIParameter)child).getValue();
473                 addParameterToHref(name, value, hrefBuf, firstParameter, charEncoding, strictXhtmlLinks);
474                 firstParameter = false;
475             }
476         }
477     }
478 
479     protected void renderOutputLinkStart(FacesContext facesContext, UIOutput output)
480             throws IOException
481     {
482         ResponseWriter writer = facesContext.getResponseWriter();
483 
484         if (HtmlRendererUtils.isDisabled(output))
485         {
486             writer.startElement(HTML.SPAN_ELEM, output);
487             HtmlRendererUtils.writeIdIfNecessary(writer, output, facesContext);
488             HtmlRendererUtils.renderHTMLAttributes(writer, output, HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES);
489         }
490         else
491         {
492             //calculate href
493             String href = org.apache.myfaces.shared.renderkit.RendererUtils.getStringValue(facesContext, output);
494             //check if there is an anchor # in it
495             int index = href.indexOf('#');
496             String anchorString = null;
497             if (index > -1)
498             {
499                 // remove anchor element and add it again after the parameter are encoded
500                 anchorString = href.substring(index,href.length());
501                 href = href.substring(0,index);
502             }
503             if (getChildCount(output) > 0)
504             {
505                 StringBuffer hrefBuf = new StringBuffer(href);
506                 addChildParametersToHref(facesContext, output, hrefBuf,
507                                      (href.indexOf('?') == -1), //first url parameter?
508                                      writer.getCharacterEncoding());
509                 href = hrefBuf.toString();
510             }
511             if (index > -1)
512             {
513                 href += anchorString;
514             }
515             href = facesContext.getExternalContext().encodeResourceURL(href);    //TODO: or encodeActionURL ?
516 
517             //write anchor
518             writer.startElement(HTML.ANCHOR_ELEM, output);
519             HtmlRendererUtils.writeIdAndNameIfNecessary(writer, output, facesContext);
520             writer.writeURIAttribute(HTML.HREF_ATTR, href, null);
521             HtmlRendererUtils.renderHTMLAttributes(writer, output, org.apache.myfaces.shared.renderkit.html.HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES);
522             writer.flush();
523         }
524     }
525 
526     private void renderLinkParameter(String name,
527                                      Object value,
528                                      StringBuffer onClick,
529                                      String jsForm,
530                                      UIComponent nestingForm)
531     {
532         if (name == null)
533         {
534             throw new IllegalArgumentException("Unnamed parameter value not allowed within command link.");
535         }
536         onClick.append(jsForm);
537         onClick.append(".elements['").append(name).append("']");
538         //UIParameter is no ValueHolder, so no conversion possible
539         String strParamValue = value != null ? org.apache.myfaces.shared.renderkit.html.util.HTMLEncoder.encode(value.toString(), false, false) : "";
540         onClick.append(".value='").append(strParamValue).append("';");
541 
542         addHiddenCommandParameter(FacesContext.getCurrentInstance(), nestingForm, name);
543     }
544 
545     private static void addParameterToHref(String name,
546                                            Object value,
547                                            StringBuffer hrefBuf,
548                                            boolean firstParameter,
549                                            String charEncoding,
550                                            boolean strictXhtmlLinks) throws UnsupportedEncodingException {
551         if (name == null) {
552             throw new IllegalArgumentException("Unnamed parameter value not allowed within command link.");
553         }
554 
555         if (firstParameter) {
556             hrefBuf.append('?');
557         } else {
558             if (strictXhtmlLinks) {
559                 hrefBuf.append("&amp;");
560             } else {
561                 hrefBuf.append('&');
562             }
563         }
564 
565         hrefBuf.append(URLEncoder.encode(name, charEncoding));
566         hrefBuf.append('=');
567         if (value != null)
568         {
569             //UIParameter is no ConvertibleValueHolder, so no conversion possible
570             hrefBuf.append(URLEncoder.encode(value.toString(), charEncoding));
571         }
572     }
573 
574 
575     protected void renderOutputLinkEnd(FacesContext facesContext, UIComponent component)
576             throws IOException
577     {
578         ResponseWriter writer = facesContext.getResponseWriter();
579 
580         if (HtmlRendererUtils.isDisabled(component))
581         {
582             writer.endElement(HTML.SPAN_ELEM);
583         }
584         else
585         {
586             // force separate end tag
587             writer.writeText("", null);
588             writer.endElement(HTML.ANCHOR_ELEM);
589         }
590     }
591 
592     protected void renderCommandLinkEnd(FacesContext facesContext, UIComponent component)
593             throws IOException
594     {
595         ResponseWriter writer = facesContext.getResponseWriter();
596         if (HtmlRendererUtils.isDisabled(component))
597         {
598 
599             writer.endElement(HTML.SPAN_ELEM);
600         }
601         else
602         {
603             writer.writeText("", null);
604             writer.endElement(HTML.ANCHOR_ELEM);
605         }
606     }
607 
608 }