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