diff --git a/api/src/main/java/org/jboss/seam/faces/component/UIInputContainer.java b/api/src/main/java/org/jboss/seam/faces/component/UIInputContainer.java index ae39b43..f9518b3 100644 --- a/api/src/main/java/org/jboss/seam/faces/component/UIInputContainer.java +++ b/api/src/main/java/org/jboss/seam/faces/component/UIInputContainer.java @@ -1,444 +1,478 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2011, Red Hat, Inc., and individual contributors - * by the @authors tag. See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jboss.seam.faces.component; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import javax.faces.FacesException; -import javax.faces.application.FacesMessage; -import javax.faces.component.EditableValueHolder; -import javax.faces.component.FacesComponent; -import javax.faces.component.NamingContainer; -import javax.faces.component.UIComponent; -import javax.faces.component.UIComponentBase; -import javax.faces.component.UIMessage; -import javax.faces.component.UINamingContainer; -import javax.faces.component.UIViewRoot; -import javax.faces.component.html.HtmlOutputLabel; -import javax.faces.context.FacesContext; -import javax.faces.validator.BeanValidator; -import javax.validation.Validation; -import javax.validation.ValidationException; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; - -/** - * UIInputContainer is a supplemental component for a JSF 2.0 composite component encapsulating one or more - * input components (EditableValueHolder), their corresponding message components (UIMessage) - * and a label (HtmlOutputLabel). This component takes care of wiring the label to the first input and the - * messages to each input in sequence. It also assigns two implicit attribute values, "required" and "invalid" to indicate that - * a required input field is present and whether there are any validation errors, respectively. To determine if a input field is - * required, both the required attribute is consulted. Finally, if the - * "label" attribute is not provided on the composite component, the label value will be derived from the id of the composite - * component, for convenience. - *
- *- * Composite component definition example (minus layout): - *
- * - * - *- * <cc:interface componentType="org.jboss.seam.faces.InputContainer"/> - * <cc:implementation> - * <h:outputLabel id="label" value="#{cc.attrs.label}:" styleClass="#{cc.attrs.invalid ? 'invalid' : ''}"> - * <h:ouputText styleClass="required" rendered="#{cc.attrs.required}" value="*"/> - * </h:outputLabel> - * <cc:insertChildren/> - * <h:message id="message" errorClass="invalid message" rendered="#{cc.attrs.invalid}"/> - * </cc:implementation> - *- * - *
- * Composite component usage example: - *
- * - * - *- * <example:inputContainer id="name"> - * <h:inputText id="input" value="#{person.name}"/> - * </example:inputContainer> - *- * - *
- * Possible enhancements: - *
- *- * NOTE: Firefox does not properly associate a label with the target input if the input id contains a colon (:), the default - * separator character in JSF. JSF 2 allows developers to set the value via an initialization parameter (context-param in - * web.xml) keyed to javax.faces.SEPARATOR_CHAR. We recommend that you override this setting to make the separator an underscore - * (_). - *
- * - * @author Dan Allen - * @author Jose Rodolfo freitas - */ -@FacesComponent(UIInputContainer.COMPONENT_TYPE) -public class UIInputContainer extends UIComponentBase implements NamingContainer { - /** - * The standard component type for this component. - */ - public static final String COMPONENT_TYPE = "org.jboss.seam.faces.InputContainer"; - - protected static final String HTML_ID_ATTR_NAME = "id"; - protected static final String HTML_CLASS_ATTR_NAME = "class"; - protected static final String HTML_STYLE_ATTR_NAME = "style"; - - private boolean beanValidationPresent = false; - - public UIInputContainer() { - beanValidationPresent = isClassPresent("javax.validation.Validator"); - } - - @Override - public String getFamily() { - return UINamingContainer.COMPONENT_FAMILY; - } - - /** - * The name of the auto-generated composite component attribute that holds a boolean indicating whether the the template - * contains an invalid input. - */ - public String getInvalidAttributeName() { - return "invalid"; - } - - /** - * The name of the auto-generated composite component attribute that holds a boolean indicating whether the template - * contains a required input. - */ - public String getRequiredAttributeName() { - return "required"; - } - - /** - * The name of the composite component attribute that holds the string label for this set of inputs. If the label attribute - * is not provided, one will be generated from the id of the composite component or, if the id is defaulted, the name of the - * property bound to the first input. - */ - public String getLabelAttributeName() { - return "label"; - } - - /** - * The name of the auto-generated composite component attribute that holds the elements in this input container. The - * elements include the label, a list of inputs and a cooresponding list of messages. - */ - public String getElementsAttributeName() { - return "elements"; - } - - /** - * The name of the composite component attribute that holds a boolean indicating whether the component template should be - * enclosed in an HTML element, so that it be referenced from JavaScript. - */ - public String getEncloseAttributeName() { - return "enclose"; - } - - public String getContainerElementName() { - return "div"; - } - - public String getDefaultLabelId() { - return "label"; - } - - public String getDefaultInputId() { - return "input"; - } - - public String getDefaultMessageId() { - return "message"; - } - - @Override - public void encodeBegin(final FacesContext context) throws IOException { - if (!isRendered()) { - return; - } - - super.encodeBegin(context); - - InputContainerElements elements = scan(getFacet(UIComponent.COMPOSITE_FACET_NAME), null, context); - // assignIds(elements, context); - wire(elements, context); - - getAttributes().put(getElementsAttributeName(), elements); - - if (elements.hasValidationError()) { - getAttributes().put(getInvalidAttributeName(), true); - } - - getAttributes().put(getRequiredAttributeName(), elements.hasRequiredInput()); - - /* - * for some reason, Mojarra is not filling Attribute Map with "label" key if label attr has an EL value, so I added a - * labelHasEmptyValue to guarantee that there was no label setted. - */ - if (getValueExpression(getLabelAttributeName()) == null - && (!getAttributes().containsKey(getLabelAttributeName()) || labelHasEmptyValue(elements))) { - getAttributes().put(getLabelAttributeName(), generateLabel(elements, context)); - } - - if (Boolean.TRUE.equals(getAttributes().get(getEncloseAttributeName()))) { - startContainerElement(context); - } - } - - @Override - public void encodeEnd(final FacesContext context) throws IOException { - if (!isRendered()) { - return; - } - - super.encodeEnd(context); - - if (Boolean.TRUE.equals(getAttributes().get(getEncloseAttributeName()))) { - endContainerElement(context); - } - } - - protected void startContainerElement(final FacesContext context) throws IOException { - context.getResponseWriter().startElement(getContainerElementName(), this); - String style = (getAttributes().get("style") != null ? getAttributes().get("style").toString().trim() : null); - if (style.length() > 0) { - context.getResponseWriter().writeAttribute(HTML_STYLE_ATTR_NAME, style, HTML_STYLE_ATTR_NAME); - } - String styleClass = (getAttributes().get("styleClass") != null ? getAttributes().get("styleClass").toString().trim() - : null); - if (styleClass.length() > 0) { - context.getResponseWriter().writeAttribute(HTML_CLASS_ATTR_NAME, styleClass, HTML_CLASS_ATTR_NAME); - } - context.getResponseWriter().writeAttribute(HTML_ID_ATTR_NAME, getClientId(context), HTML_ID_ATTR_NAME); - } - - protected void endContainerElement(final FacesContext context) throws IOException { - context.getResponseWriter().endElement(getContainerElementName()); - } - - protected String generateLabel(final InputContainerElements elements, final FacesContext context) { - String name = getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX) ? elements.getPropertyName(context) : getId(); - return name.substring(0, 1).toUpperCase() + name.substring(1); - } - - /** - * Walk the component tree branch built by the composite component and locate the input container elements. - * - * @return a composite object of the input container elements - */ - protected InputContainerElements scan(final UIComponent component, InputContainerElements elements, - final FacesContext context) { - if (elements == null) { - elements = new InputContainerElements(); - } - - // NOTE we need to walk the tree ignoring rendered attribute because it's condition - // could be based on what we discover - if ((elements.getLabel() == null) && (component instanceof HtmlOutputLabel)) { - elements.setLabel((HtmlOutputLabel) component); - } else if (component instanceof EditableValueHolder) { - elements.registerInput((EditableValueHolder) component, getDefaultValidator(context), context); - } else if (component instanceof UIMessage) { - elements.registerMessage((UIMessage) component); - } - // may need to walk smarter to ensure "element of least suprise" - for (UIComponent child : component.getChildren()) { - scan(child, elements, context); - } - - return elements; - } - - // assigning ids seems to break form submissions, but I don't know why - public void assignIds(final InputContainerElements elements, final FacesContext context) { - boolean refreshIds = false; - if (getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { - setId(elements.getPropertyName(context)); - refreshIds = true; - } - UIComponent label = elements.getLabel(); - if (label != null) { - if (label.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { - label.setId(getDefaultLabelId()); - } else if (refreshIds) { - label.setId(label.getId()); - } - } - for (int i = 0, len = elements.getInputs().size(); i < len; i++) { - UIComponent input = (UIComponent) elements.getInputs().get(i); - if (input.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { - input.setId(getDefaultInputId() + (i == 0 ? "" : (i + 1))); - } else if (refreshIds) { - input.setId(input.getId()); - } - } - for (int i = 0, len = elements.getMessages().size(); i < len; i++) { - UIComponent msg = elements.getMessages().get(i); - if (msg.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { - msg.setId(getDefaultMessageId() + (i == 0 ? "" : (i + 1))); - } else if (refreshIds) { - msg.setId(msg.getId()); - } - } - } - - /** - * Wire the label and messages to the input(s) - */ - protected void wire(final InputContainerElements elements, final FacesContext context) { - elements.wire(context); - } - - /** - * Get the default Bean Validation Validator to read the contraints for a property. - */ - private Validator getDefaultValidator(final FacesContext context) throws FacesException { - if (!beanValidationPresent) { - return null; - } - - ValidatorFactory validatorFactory; - Object cachedObject = context.getExternalContext().getApplicationMap().get(BeanValidator.VALIDATOR_FACTORY_KEY); - if (cachedObject instanceof ValidatorFactory) { - validatorFactory = (ValidatorFactory) cachedObject; - } else { - try { - validatorFactory = Validation.buildDefaultValidatorFactory(); - } catch (ValidationException e) { - throw new FacesException("Could not build a default Bean Validator factory", e); - } - context.getExternalContext().getApplicationMap().put(BeanValidator.VALIDATOR_FACTORY_KEY, validatorFactory); - } - return validatorFactory.getValidator(); - } - - private boolean isClassPresent(final String fqcn) { - try { - if (Thread.currentThread().getContextClassLoader() != null) { - return Thread.currentThread().getContextClassLoader().loadClass(fqcn) != null; - } else { - return Class.forName(fqcn) != null; - } - } catch (ClassNotFoundException e) { - return false; - } catch (NoClassDefFoundError e) { - return false; - } - } - - private boolean labelHasEmptyValue(InputContainerElements elements) { - if (elements.getLabel() == null || elements.getLabel().getValue() == null) - return false; - return (elements.getLabel().getValue().toString().trim().equals(":") || elements.getLabel().getValue().toString() - .trim().equals("")); - } - - public static class InputContainerElements { - private String propertyName; - private HtmlOutputLabel label; - private final List+ * Composite component definition example (minus layout): + *
+ * + * + *+ * <cc:interface componentType="org.jboss.seam.faces.InputContainer"/> + * <cc:implementation> + * <h:outputLabel id="label" value="#{cc.attrs.label}:" styleClass="#{cc.attrs.invalid ? 'invalid' : ''}"> + * <h:ouputText styleClass="required" rendered="#{cc.attrs.required}" value="*"/> + * </h:outputLabel> + * <cc:insertChildren/> + * <h:message id="message" errorClass="invalid message" rendered="#{cc.attrs.invalid}"/> + * </cc:implementation> + *+ * + *
+ * Composite component usage example: + *
+ * + * + *+ * <example:inputContainer id="name"> + * <h:inputText id="input" value="#{person.name}"/> + * </example:inputContainer> + *+ * + *
+ * Possible enhancements: + *
+ *+ * NOTE: Firefox does not properly associate a label with the target input if the input id contains a colon (:), the default + * separator character in JSF. JSF 2 allows developers to set the value via an initialization parameter (context-param in + * web.xml) keyed to javax.faces.SEPARATOR_CHAR. We recommend that you override this setting to make the separator an underscore + * (_). + *
+ * + * @author Dan Allen + * @author Jose Rodolfo freitas + */ +@FacesComponent(UIInputContainer.COMPONENT_TYPE) +public class UIInputContainer extends UIComponentBase implements NamingContainer { + /** + * The standard component type for this component. + */ + public static final String COMPONENT_TYPE = "org.jboss.seam.faces.InputContainer"; + + protected static final String HTML_ID_ATTR_NAME = "id"; + protected static final String HTML_CLASS_ATTR_NAME = "class"; + protected static final String HTML_STYLE_ATTR_NAME = "style"; + + private boolean beanValidationPresent = false; + + public UIInputContainer() { + beanValidationPresent = isClassPresent("javax.validation.Validator"); + } + + @Override + public String getFamily() { + return UINamingContainer.COMPONENT_FAMILY; + } + + /** + * The name of the auto-generated composite component attribute that holds a boolean indicating whether the the template + * contains an invalid input. + */ + public String getInvalidAttributeName() { + return "invalid"; + } + + /** + * The name of the auto-generated composite component attribute that holds a boolean indicating whether the template + * contains a required input. + */ + public String getRequiredAttributeName() { + return "required"; + } + + /** + * The name of the composite component attribute that holds the string label for this set of inputs. If the label attribute + * is not provided, one will be generated from the id of the composite component or, if the id is defaulted, the name of the + * property bound to the first input. + */ + public String getLabelAttributeName() { + return "label"; + } + + /** + * The name of the auto-generated composite component attribute that holds the elements in this input container. The + * elements include the label, a list of inputs and a cooresponding list of messages. + */ + public String getElementsAttributeName() { + return "elements"; + } + + /** + * The name of the composite component attribute that holds a boolean indicating whether the component template should be + * enclosed in an HTML element, so that it be referenced from JavaScript. + */ + public String getEncloseAttributeName() { + return "enclose"; + } + + public String getContainerElementName() { + return "div"; + } + + public String getDefaultLabelId() { + return "label"; + } + + public String getDefaultInputId() { + return "input"; + } + + public String getDefaultMessageId() { + return "message"; + } + + @Override + public void encodeBegin(final FacesContext context) throws IOException { + if (!isRendered()) { + return; + } + + super.encodeBegin(context); + + InputContainerElements elements = scan(getFacet(UIComponent.COMPOSITE_FACET_NAME), null, context); + // assignIds(elements, context); + + /* + * for some reason, Mojarra is not filling Attribute Map with "label" key if label attr has an EL value, so I added a + * labelHasEmptyValue to guarantee that there was no label setted. + */ + if (getValueExpression(getLabelAttributeName()) == null + && (!getAttributes().containsKey(getLabelAttributeName()) || labelHasEmptyValue(elements))) { + getAttributes().put(getLabelAttributeName(), generateLabel(elements, context)); + } + assignLabels(elements, context, this); + + wire(elements, context); + + getAttributes().put(getElementsAttributeName(), elements); + + if (elements.hasValidationError()) { + getAttributes().put(getInvalidAttributeName(), true); + } + + getAttributes().put(getRequiredAttributeName(), elements.hasRequiredInput()); + + if (Boolean.TRUE.equals(getAttributes().get(getEncloseAttributeName()))) { + startContainerElement(context); + } + } + + @Override + public void encodeEnd(final FacesContext context) throws IOException { + if (!isRendered()) { + return; + } + + super.encodeEnd(context); + + if (Boolean.TRUE.equals(getAttributes().get(getEncloseAttributeName()))) { + endContainerElement(context); + } + } + + protected void startContainerElement(final FacesContext context) throws IOException { + context.getResponseWriter().startElement(getContainerElementName(), this); + String style = (getAttributes().get("style") != null ? getAttributes().get("style").toString().trim() : null); + if (style.length() > 0) { + context.getResponseWriter().writeAttribute(HTML_STYLE_ATTR_NAME, style, HTML_STYLE_ATTR_NAME); + } + String styleClass = (getAttributes().get("styleClass") != null ? getAttributes().get("styleClass").toString().trim() + : null); + if (styleClass.length() > 0) { + context.getResponseWriter().writeAttribute(HTML_CLASS_ATTR_NAME, styleClass, HTML_CLASS_ATTR_NAME); + } + context.getResponseWriter().writeAttribute(HTML_ID_ATTR_NAME, getClientId(context), HTML_ID_ATTR_NAME); + } + + protected void endContainerElement(final FacesContext context) throws IOException { + context.getResponseWriter().endElement(getContainerElementName()); + } + + protected String generateLabel(final InputContainerElements elements, final FacesContext context) { + String name = getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX) ? elements.getPropertyName(context) : getId(); + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + + /** + * Walk the component tree branch built by the composite component and locate the input container elements. + * + * @return a composite object of the input container elements + */ + protected InputContainerElements scan(final UIComponent component, InputContainerElements elements, + final FacesContext context) { + if (elements == null) { + elements = new InputContainerElements(); + } + + // NOTE we need to walk the tree ignoring rendered attribute because it's condition + // could be based on what we discover + if ((elements.getLabel() == null) && (component instanceof HtmlOutputLabel)) { + elements.setLabel((HtmlOutputLabel) component); + } else if (component instanceof EditableValueHolder) { + elements.registerInput((EditableValueHolder) component, getDefaultValidator(context), context); + } else if (component instanceof UIMessage) { + elements.registerMessage((UIMessage) component); + } + // may need to walk smarter to ensure "element of least suprise" + for (UIComponent child : component.getChildren()) { + scan(child, elements, context); + } + + return elements; + } + + // assigning ids seems to break form submissions, but I don't know why + public void assignIds(final InputContainerElements elements, final FacesContext context) { + boolean refreshIds = false; + if (getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { + setId(elements.getPropertyName(context)); + refreshIds = true; + } + UIComponent label = elements.getLabel(); + if (label != null) { + if (label.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { + label.setId(getDefaultLabelId()); + } else if (refreshIds) { + label.setId(label.getId()); + } + } + for (int i = 0, len = elements.getInputs().size(); i < len; i++) { + UIComponent input = (UIComponent) elements.getInputs().get(i); + if (input.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { + input.setId(getDefaultInputId() + (i == 0 ? "" : (i + 1))); + } else if (refreshIds) { + input.setId(input.getId()); + } + } + for (int i = 0, len = elements.getMessages().size(); i < len; i++) { + UIComponent msg = elements.getMessages().get(i); + if (msg.getId().startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) { + msg.setId(getDefaultMessageId() + (i == 0 ? "" : (i + 1))); + } else if (refreshIds) { + msg.setId(msg.getId()); + } + } + } + + // assign label attribute for any input component (better validation message) + private void assignLabels(InputContainerElements elements, + FacesContext context, UIInputContainer uiInputContainer) { + + // label attribute handling + ValueExpression labelValueExpression = null; + Object labelValue = null; + for (int i = 0, len = elements.getInputs().size(); i < len; i++) { + UIComponent input = (UIComponent) elements.getInputs().get(i); + if (input.getValueExpression("label") == null + && (!input.getAttributes().containsKey("label"))) { + + // retrieving container label only if necessary + if (labelValueExpression == null && labelValue == null) { + labelValueExpression = uiInputContainer.getValueExpression(getLabelAttributeName()); + if (labelValueExpression == null) { + labelValue = uiInputContainer.getAttributes().get(getLabelAttributeName()); + } + } + + // copy container label to input + if (labelValueExpression != null) { + input.setValueExpression("label", labelValueExpression); + } + if (labelValue != null) { + input.getAttributes().put("label", labelValue); + } + } + } + } + + /** + * Wire the label and messages to the input(s) + */ + protected void wire(final InputContainerElements elements, final FacesContext context) { + elements.wire(context); + } + + /** + * Get the default Bean Validation Validator to read the contraints for a property. + */ + private Validator getDefaultValidator(final FacesContext context) throws FacesException { + if (!beanValidationPresent) { + return null; + } + + ValidatorFactory validatorFactory; + Object cachedObject = context.getExternalContext().getApplicationMap().get(BeanValidator.VALIDATOR_FACTORY_KEY); + if (cachedObject instanceof ValidatorFactory) { + validatorFactory = (ValidatorFactory) cachedObject; + } else { + try { + validatorFactory = Validation.buildDefaultValidatorFactory(); + } catch (ValidationException e) { + throw new FacesException("Could not build a default Bean Validator factory", e); + } + context.getExternalContext().getApplicationMap().put(BeanValidator.VALIDATOR_FACTORY_KEY, validatorFactory); + } + return validatorFactory.getValidator(); + } + + private boolean isClassPresent(final String fqcn) { + try { + if (Thread.currentThread().getContextClassLoader() != null) { + return Thread.currentThread().getContextClassLoader().loadClass(fqcn) != null; + } else { + return Class.forName(fqcn) != null; + } + } catch (ClassNotFoundException e) { + return false; + } catch (NoClassDefFoundError e) { + return false; + } + } + + private boolean labelHasEmptyValue(InputContainerElements elements) { + if (elements.getLabel() == null || elements.getLabel().getValue() == null) + return false; + return (elements.getLabel().getValue().toString().trim().equals(":") || elements.getLabel().getValue().toString() + .trim().equals("")); + } + + public static class InputContainerElements { + private String propertyName; + private HtmlOutputLabel label; + private final List