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 }