View Javadoc
1   /*
2    * $Id$
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts.faces.renderer;
23  
24  
25  import java.io.IOException;
26  import java.util.List;
27  import java.util.Map;
28  
29  import org.apache.struts.faces.component.HtmlComponent;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  import jakarta.el.ValueExpression;
34  import jakarta.faces.component.EditableValueHolder;
35  import jakarta.faces.component.UIComponent;
36  import jakarta.faces.component.ValueHolder;
37  import jakarta.faces.context.FacesContext;
38  import jakarta.faces.context.ResponseWriter;
39  import jakarta.faces.convert.Converter;
40  import jakarta.faces.convert.ConverterException;
41  import jakarta.faces.render.Renderer;
42  
43  
44  /**
45   * Abstract base class for concrete implementations of
46   * {@code jakarta.faces.render.Renderer} for the
47   * <em>Struts-Faces Integration Library</em>.
48   *
49   * @version $Rev$ $Date$
50   */
51  public abstract class AbstractRenderer extends Renderer {
52  
53  
54      // -------------------------------------------------------- Static Variables
55  
56  
57      /**
58       * The {@code Log} instance for this class.
59       */
60      private final Logger log =
61          LoggerFactory.getLogger(AbstractRenderer.class);
62  
63  
64      // -------------------------------------------------------- Renderer Methods
65  
66  
67      /**
68       * Decode any new state of the specified {@code UIComponent} from the
69       * request contained in the specified {@code FacesContext}, and store
70       * that state on the {@code UIComponent}.
71       *
72       * <p>The default implementation calls {@code setSubmittedValue()}
73       * unless this component has a boolean {@code disabled} or
74       * {@code readonly} attribute that is set to {@code true}.</p>
75       *
76       * @param context {@code FacesContext} for the current request
77       * @param component {@code UIComponent} to be decoded
78       *
79       * @throws NullPointerException if {@code context} or
80       *     {@code component} is {@code null}
81       */
82      public void decode(FacesContext context, UIComponent component) {
83  
84          // Enforce NPE requirements in the JavaDocs
85          if (context == null || component == null) {
86              throw new NullPointerException();
87          }
88  
89          // Disabled or read-only components are not decoded
90          if (isDisabled(component) || isReadOnly(component)) {
91              return;
92          }
93  
94          // Save submitted value on EditableValueHolder components
95          if (component instanceof EditableValueHolder) {
96              setSubmittedValue(context, component);
97          }
98  
99      }
100 
101 
102     /**
103      * Render the beginning of the specified {@code UIComponent} to the
104      * output stream or writer associated with the response we are
105      * creating.
106      *
107      * <p>The default implementation calls {@code renderStart()} and
108      * {@code renderAttributes()}.</p>
109      *
110      * @param context {@code FacesContext} for the current request
111      * @param component {@code UIComponent} to be decoded
112      *
113      * @throws NullPointerException if {@code context} or
114      *     {@code component} is {@code null}
115      *
116      * @throws IOException if an input/output error occurs
117      */
118     public void encodeBegin(FacesContext context, UIComponent component)
119         throws IOException {
120 
121         // Enforce NPE requirements in the JavaDocs
122         if (context == null || component == null) {
123             throw new NullPointerException();
124         }
125 
126         log.trace("encodeBegin(id={}, family={}, rendererType={})",
127             component.getId(), component.getFamily(), component.getRendererType());
128 
129         // Render the element and attributes for this component
130         ResponseWriter writer = context.getResponseWriter();
131         renderStart(context, component, writer);
132         renderAttributes(context, component, writer);
133 
134     }
135 
136 
137     /**
138      * Render the children of the specified {@code UIComponent} to the
139      * output stream or writer associated with the response we are
140      * creating.
141      *
142      * <p>The default implementation iterates through the children of
143      * this component and renders them.</p>
144      *
145      * @param context {@code FacesContext} for the current request
146      * @param component {@code UIComponent} to be decoded
147      *
148      * @throws NullPointerException if {@code context} or
149      *     {@code component} is {@code null}
150      *
151      * @throws IOException if an input/output error occurs
152      */
153     public void encodeChildren(FacesContext context, UIComponent component)
154         throws IOException {
155 
156         if (context == null || component == null) {
157             throw new NullPointerException();
158         }
159 
160         log.trace("encodeChildren(id={}, family={}, rendererType={})",
161             component.getId(), component.getFamily(), component.getRendererType());
162         List<UIComponent> kids = component.getChildren();
163         for (UIComponent kid : kids) {
164             kid.encodeBegin(context);
165             if (kid.getRendersChildren()) {
166                 kid.encodeChildren(context);
167             }
168             kid.encodeEnd(context);
169         }
170         log.trace("encodeChildren(id={}) end", component.getId());
171 
172     }
173 
174 
175     /**
176      * Render the ending of the specified {@code UIComponent} to the
177      * output stream or writer associated with the response we are
178      * creating.
179      *
180      * <p>The default implementation calls {@code renderEnd()}.</p>
181      *
182      * @param context {@code FacesContext} for the current request
183      * @param component {@code UIComponent} to be decoded
184      *
185      * @throws NullPointerException if {@code context} or
186      *     {@code component} is {@code null}
187      *
188      * @throws IOException if an input/output error occurs
189      */
190     public void encodeEnd(FacesContext context, UIComponent component)
191         throws IOException {
192 
193         // Enforce NPE requirements in the Javadocs
194         if (context == null || component == null) {
195             throw new NullPointerException();
196         }
197 
198         log.trace("encodeEnd(id={}, family={}, rendererType={})",
199             component.getId(), component.getFamily(), component.getRendererType());
200 
201         // Render the element closing for this component
202         ResponseWriter writer = context.getResponseWriter();
203         renderEnd(context, component, writer);
204 
205     }
206 
207 
208     // --------------------------------------------------------- Package Methods
209 
210 
211     // ------------------------------------------------------- Protected Methods
212 
213 
214     /**
215      * Render nested child components by invoking the encode methods
216      * on those components, but only when the {@code rendered}
217      * property is {@code true}.
218      */
219     protected void encodeRecursive(FacesContext context, UIComponent component)
220         throws IOException {
221 
222         // suppress rendering if "rendered" property on the component is
223         // false.
224         if (!component.isRendered()) {
225             return;
226         }
227 
228         // Render this component and its children recursively
229         log.trace("encodeRecursive(id={}, family={}, rendererType={}) encodeBegin",
230             component.getId(), component.getFamily(), component.getRendererType());
231         component.encodeBegin(context);
232         if (component.getRendersChildren()) {
233             log.trace("encodeRecursive(id={}) delegating",
234                 component.getId());
235             component.encodeChildren(context);
236         } else {
237             log.trace("encodeRecursive(id={}) recursing",
238                 component.getId());
239             List<UIComponent> kids = component.getChildren();
240             for (UIComponent kid : kids) {
241                 encodeRecursive(context, kid);
242             }
243         }
244         log.trace("encodeRecursive(id={}) encodeEnd", component.getId());
245         component.encodeEnd(context);
246 
247     }
248 
249 
250     /**
251      * Return {@code true} if the specified component is disabled.
252      *
253      * @param component {@code UIComponent} to be checked
254      */
255     protected boolean isDisabled(UIComponent component) {
256 
257         Object disabled = component.getAttributes().get("disabled");
258         if (disabled == null) {
259             return false;
260         }
261         if (disabled instanceof String) {
262             return Boolean.valueOf((String) disabled).booleanValue();
263         } else {
264             return disabled.equals(Boolean.TRUE);
265         }
266 
267     }
268 
269 
270     /**
271      * Return {@code true} if the specified component is read only.
272      *
273      * @param component {@code UIComponent} to be checked
274      */
275     protected boolean isReadOnly(UIComponent component) {
276 
277         Object readonly = component.getAttributes().get("readonly");
278         if (readonly == null) {
279             return false;
280         }
281         if (readonly instanceof String) {
282             return Boolean.valueOf((String) readonly).booleanValue();
283         } else {
284             return readonly.equals(Boolean.TRUE);
285         }
286 
287     }
288 
289 
290     /**
291      * Render the element attributes for the generated markup related to this
292      * component. Simple renderers that create a single markup element
293      * for this component should override this method and include calls to
294      * to {@code writeAttribute()} and {@code writeURIAttribute}
295      * on the specified {@code ResponseWriter}.
296      *
297      * <p>The default implementation does nothing.</p>
298      *
299      * @param context {@code FacesContext} for the current request
300      * @param component {@code EditableValueHolder} component whose
301      *     submitted value is to be stored
302      * @param writer {@code ResponseWriter} to which the element
303      *     start should be rendered
304      *
305      * @throws IOException if an input/output error occurs
306      */
307     protected void renderAttributes(FacesContext context, UIComponent component,
308                                     ResponseWriter writer) throws IOException {
309 
310     }
311 
312 
313     /**
314      * Render the element end for the generated markup related to this
315      * component. Simple renderers that create a single markup element
316      * for this component should override this method and include a call
317      * to {@code endElement()} on the specified {@code ResponseWriter}.
318      *
319      * <p>The default implementation does nothing.</p>
320      *
321      * @param context {@code FacesContext} for the current request
322      * @param component {@code EditableValueHolder} component whose
323      *     submitted value is to be stored
324      * @param writer {@code ResponseWriter} to which the element
325      *     start should be rendered
326      *
327      * @throws IOException if an input/output error occurs
328      */
329     protected void renderEnd(FacesContext context, UIComponent component,
330                              ResponseWriter writer) throws IOException {
331 
332     }
333 
334 
335     /**
336      * Render any boolean attributes on the specified list that have
337      * {@code true} values on the corresponding attribute of the
338      * specified {@code UIComponent}.
339      *
340      * @param context {@code FacesContext} for the current request
341      * @param component {@code EditableValueHolder} component whose
342      *     submitted value is to be stored
343      * @param writer {@code ResponseWriter} to which the element
344      *     start should be rendered
345      * @param names List of attribute names to be passed through
346      *
347      * @throws IOException if an input/output error occurs
348      */
349     protected void renderBoolean(FacesContext context,
350                                  UIComponent component,
351                                  ResponseWriter writer,
352                                  String names[]) throws IOException {
353 
354         if (names == null) {
355             return;
356         }
357         Map<String, Object> attributes = component.getAttributes();
358         boolean flag;
359         Object value;
360         for (String name : names) {
361             value = attributes.get(name);
362             if (value != null) {
363                 if (value instanceof String) {
364                     flag = Boolean.valueOf((String) value).booleanValue();
365                 } else {
366                     flag = Boolean.valueOf(value.toString()).booleanValue();
367                 }
368                 if (flag) {
369                     writer.writeAttribute(name, name, name);
370                     flag = false;
371                 }
372             }
373         }
374 
375     }
376 
377 
378     /**
379      * Render any attributes on the specified list directly to the
380      * specified {@code ResponseWriter} for which the specified
381      * {@code UIComponent} has a non-{@code null} attribute value.
382      * This method may be used to "pass through" commonly used attribute
383      * name/value pairs with a minimum of code.
384      *
385      * @param context {@code FacesContext} for the current request
386      * @param component {@code EditableValueHolder} component whose
387      *     submitted value is to be stored
388      * @param writer {@code ResponseWriter} to which the element
389      *     start should be rendered
390      * @param names List of attribute names to be passed through
391      *
392      * @throws IOException if an input/output error occurs
393      */
394     protected void renderPassThrough(FacesContext context,
395                                      UIComponent component,
396                                      ResponseWriter writer,
397                                      String names[]) throws IOException {
398 
399         if (names == null) {
400             return;
401         }
402         Map<String, Object> attributes = component.getAttributes();
403         Object value;
404         for (String name : names) {
405             value = attributes.get(name);
406             if (value != null) {
407                 if (value instanceof String) {
408                     writer.writeAttribute(name, value, name);
409                 } else {
410                     writer.writeAttribute(name, value.toString(), name);
411                 }
412             }
413         }
414 
415     }
416 
417 
418     /**
419      * Render the element start for the generated markup related to this
420      * component. Simple renderers that create a single markup element
421      * for this component should override this method and include a call
422      * to {@code startElement()} on the specified {@code ResponseWriter}.
423      *
424      * <p>The default implementation does nothing.</p>
425      *
426      * @param context {@code FacesContext} for the current request
427      * @param component {@code EditableValueHolder} component whose
428      *  submitted value is to be stored
429      * @param writer {@code ResponseWriter} to which the element
430      *  start should be rendered
431      *
432      * @throws IOException if an input/output error occurs
433      */
434     protected void renderStart(FacesContext context, UIComponent component,
435                                ResponseWriter writer) throws IOException {
436 
437     }
438 
439 
440     /**
441      * If a submitted value was included on this request, store it in the
442      * component as appropriate.
443      *
444      * <p>The default implementation determines whether this component
445      * implements {@code EditableValueHolder}.  If so, it checks for a
446      * request parameter with the same name as the {@code clientId}
447      * of this {@code UIComponent}. If there is such a parameter, its
448      * value is passed (as a String) to the {@code setSubmittedValue()}
449      * method on the {@code EditableValueHolder} component.</p>
450      *
451      * @param context {@code FacesContext} for the current request
452      * @param component {@code EditableValueHolder} component whose
453      *     submitted value is to be stored
454      */
455     protected void setSubmittedValue
456         (FacesContext context, UIComponent component) {
457 
458         if (!(component instanceof EditableValueHolder)) {
459             return;
460         }
461         String clientId = component.getClientId(context);
462         Map<String, String> parameters = context.getExternalContext().getRequestParameterMap();
463         if (parameters.containsKey(clientId)) {
464             log.atTrace()
465                 .setMessage("setSubmittedValue({},{})")
466                 .addArgument(clientId)
467                 .addArgument(() -> parameters.get(clientId))
468                 .log();
469             component.getAttributes().put("submittedValue",
470                                           parameters.get(clientId));
471         }
472 
473     }
474 
475 
476     // --------------------------------------------------------- Private Methods
477 
478 
479     /**
480      * Decode the current state of the specified UIComponent from the
481      * request contained in the specified {@code FacesContext}, and
482      * attempt to convert this state information into an object of the
483      * type required for this component.
484      *
485      * @param context FacesContext for the request we are processing
486      * @param component UIComponent to be decoded
487      *
488      * @throws NullPointerException if context or component is null
489      */
490     /*
491     public void decode(FacesContext context, UIComponent component) {
492 
493         // Enforce NPE requirements in the JavaDocs
494         if ((context == null) || (component == null)) {
495             throw new NullPointerException();
496         }
497 
498         // Only input components need to be decoded
499         if (!(component instanceof UIInput)) {
500             return;
501         }
502         UIInput input = (UIInput) component;
503 
504         // Save the old value for use in generating ValueChangedEvents
505         Object oldValue = input.getValue();
506         if (oldValue instanceof String) {
507             try {
508                 oldValue = getAsObject(context, component, (String) oldValue);
509             } catch (ConverterException e) {
510                 ;
511             }
512         }
513         input.setPrevious(oldValue);
514 
515         // Decode and convert (if needed) the new value
516         String clientId = component.getClientId(context);
517         Map<String, String> map = context.getExternalContext().getRequestParameterMap();
518         String newString = (String) map.get(clientId);
519         Object newValue = null;
520         try {
521             newValue = getAsObject(context, component, newString);
522             input.setValue(newValue);
523             input.setValid(true);
524         } catch (ConverterException e) {
525             input.setValue(newValue);
526             input.setValid(false);
527             addConverterMessage(context, component, e.getMessage());
528         }
529 
530     }
531     */
532 
533 
534     // --------------------------------------------------------- Package Methods
535 
536 
537     // ------------------------------------------------------- Protected Methods
538 
539 
540     /**
541      * Add an error message denoting a conversion failure.
542      *
543      * @param context The {@code FacesContext} for this request
544      * @param component The {@code UIComponent} that experienced
545      *     the conversion failure
546      * @param text The text of the error message
547      */
548     /*
549     protected void addConverterMessage(FacesContext context,
550                                        UIComponent component,
551                                        String text) {
552 
553         String clientId = component.getClientId(context);
554         FacesMessage message = new FacesMessage
555             (text,
556              "Conversion error on component '" + clientId + "'");
557         context.addMessage(clientId, message);
558 
559     }
560     */
561 
562 
563     /**
564      * Convert the String representation of this component's value
565      * to the corresponding Object representation. The default
566      * implementation utilizes the {@code getAsObject()} method of any
567      * associated {@code Converter}.<
568      *
569      * @param context The {@code FacesContext} for this request
570      * @param component The {@code UIComponent} whose value is
571      *     being converted
572      * @param value The String representation to be converted
573      *
574      * @throws ConverterException if conversion fails
575      */
576     /*
577     protected Object getAsObject(FacesContext context, UIComponent component,
578                                  String value) throws ConverterException {
579 
580         // Identify any Converter associated with this component value
581         ValueBinding vb = component.getValueBinding("value");
582         Converter converter = null;
583         if (component instanceof ValueHolder) {
584             // Acquire explicitly assigned Converter (if any)
585             converter = ((ValueHolder) component).getConverter();
586         }
587         if (converter == null && vb != null) {
588             Class<?> type = vb.getType(context);
589             if (type == null || type == String.class) {
590                 return value; // No conversion required for Strings
591             }
592             // Acquire implicit by-type Converter (if any)
593             converter = context.getApplication().createConverter(type);
594         }
595 
596         // Convert the result if we identified a Converter
597         if (converter != null) {
598             return converter.getAsObject(context, component, value);
599         } else {
600             return value;
601         }
602 
603     }
604     */
605 
606 
607     /**
608      * <p>Convert the Object representation of this component's value
609      * to the corresponding String representation.  The default implementation
610      * utilizes the {@code getAsString()} method of any associated
611      * {@code Converter}.</p>
612      *
613      * @param context The {@code FacesContext} for this request
614      * @param component The {@code UIComponent} whose value is
615      *  being converted
616      * @param value The Object representation to be converted
617      *
618      * @throws ConverterException if conversion fails
619      */
620     protected String getAsString(FacesContext context, UIComponent component,
621                                  Object value) throws ConverterException {
622 
623         // Identify any Converter associated with this component value
624         ValueExpression vb = component.getValueExpression("value");
625         Converter<Object> converter = null;
626         if (component instanceof ValueHolder) {
627             // Acquire explicitly assigned Converter (if any)
628             @SuppressWarnings("unchecked")
629             Converter<Object> conv = ((ValueHolder) component).getConverter();
630             converter = conv;
631         }
632         if (converter == null && vb != null) {
633             // Acquire implicit by-type Converter (if any)
634             Class<?> type = vb.getType(context.getELContext());
635             if (type != null) {
636                 @SuppressWarnings("unchecked")
637                 Converter<Object> conv = context.getApplication().createConverter(type);
638                 converter = conv;
639             }
640         }
641 
642         // Convert the result if we identified a Converter
643         if (converter != null) {
644             return converter.getAsString(context, component, value);
645         } else if (value == null) {
646             return "";
647         } else if (value instanceof String) {
648             return (String) value;
649         } else {
650             return value.toString();
651         }
652 
653     }
654 
655 
656     /**
657      * Return {@code true} if we should render as XHTML.
658      *
659      * @param component The component we are rendering
660      */
661     protected boolean isXhtml(UIComponent component) {
662         final HtmlComponent htmlComponent = searchComponent(HtmlComponent.class, component);
663 
664         return htmlComponent == null ? false : htmlComponent.isXhtml();
665     }
666 
667 
668     /**
669      * Search the give {@code UIComponent} in the component-tree.
670      *
671      * @param component The entry-point into component-tree.
672      *
673      * @return The {@code UIComponent} or {@code null} if the
674      *     {@code component} is not found.
675      */
676     protected <T extends UIComponent> T searchComponent(Class<T> clazz, UIComponent component) {
677         while (component != null) {
678             if (clazz.isInstance(component)) {
679                 return clazz.cast(component);
680             }
681             component = component.getParent();
682         }
683         return null;
684     }
685 }