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 }