/* * Copyright 2017 OmniFaces * * 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.omnifaces.taghandler; import static java.lang.String.format; import static org.omnifaces.util.Utils.unmodifiableSet; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.faces.component.UIComponent; import javax.faces.view.facelets.ComponentHandler; import javax.faces.view.facelets.FaceletContext; import javax.faces.view.facelets.TagAttribute; import javax.faces.view.facelets.TagConfig; import javax.faces.view.facelets.TagHandler; /** * <p> * The <strong><o:massAttribute></strong> sets an attribute of the given name and value on all nested components, * if they don't already have an attribute set. On boolean attributes like <code>disabled</code>, <code>readonly</code> * and <code>rendered</code>, any literal (static) attribute value will be ignored and overridden. Only if they have * already a value expression <code>#{...}</code> as attribute value, then it won't be overridden. This is a technical * limitation specifically for boolean attributes as they don't default to <code>null</code>. * * <h3>Usage</h3> * <p> * For example, the following setup * <pre> * <o:massAttribute name="disabled" value="true"> * <h:inputText id="input1" /> * <h:inputText id="input2" disabled="true" /> * <h:inputText id="input3" disabled="false" /> * <h:inputText id="input4" disabled="#{true}" /> * <h:inputText id="input5" disabled="#{false}" /> * </o:massAttribute> * </pre> * will set the <code>disabled="true"</code> attribute in <code>input1</code>, <code>input2</code> and * <code>input3</code> as those are the only components <strong>without</strong> a value expression on the boolean attribute. * <p> * As another general example without booleans, the following setup * <pre> * <o:massAttribute name="styleClass" value="#{component.valid ? '' : 'error'}"> * <h:inputText id="input1" /> * <h:inputText id="input2" styleClass="some" /> * <h:inputText id="input3" styleClass="#{'some'}" /> * <h:inputText id="input4" styleClass="#{null}" /> * </o:massAttribute> * </pre> * will only set the <code>styleClass="#{component.valid ? '' : 'error'}"</code> attribute in <code>input1</code> as * that's the only component on which the attribute is absent. * Do note that the specified EL expression will actually be evaluated on a per-component basis. * <p> * To target a specific component (super)class, use the <code>target</code> attribute. The example below skips labels * (as that would otherwise fail in the example below because they don't have the <code>valid</code> property): * <pre> * <o:massAttribute name="styleClass" value="#{component.valid ? '' : 'error'}" target="javax.faces.component.UIInput"> * <h:outputLabel for="input1" /> * <h:inputText id="input1" /> * <h:outputLabel for="input2" /> * <h:inputText id="input2" /> * <h:outputLabel for="input3" /> * <h:inputText id="input3" /> * </o:massAttribute> * </pre> * * @author Bauke Scholtz * @since 1.8 */ public class MassAttribute extends TagHandler { // Constants ------------------------------------------------------------------------------------------------------ private static final Set<String> ILLEGAL_NAMES = unmodifiableSet("id", "binding"); private static final String ERROR_ILLEGAL_NAME = "The 'name' attribute may not be set to 'id' or 'binding'."; private static final String ERROR_UNAVAILABLE_TARGET = "The 'target' attribute must represent a valid class name." + " Encountered '%s' which cannot be found in the classpath."; private static final String ERROR_INVALID_TARGET = "The 'target' attribute must represent an UIComponent class." + " Encountered '%s' which is not an UIComponent class."; // Properties ----------------------------------------------------------------------------------------------------- private String name; private TagAttribute value; private Class<UIComponent> targetClass; // Constructors --------------------------------------------------------------------------------------------------- /** * The tag constructor. * @param config The tag config. */ @SuppressWarnings("unchecked") public MassAttribute(TagConfig config) { super(config); name = getRequiredAttribute("name").getValue(); if (ILLEGAL_NAMES.contains(name)) { throw new IllegalArgumentException(ERROR_ILLEGAL_NAME); } value = getRequiredAttribute("value"); TagAttribute target = getAttribute("target"); if (target != null) { String className = target.getValue(); Class<?> cls = null; try { cls = Class.forName(className); } catch (ClassNotFoundException e) { throw new IllegalArgumentException(format(ERROR_UNAVAILABLE_TARGET, className), e); } if (!UIComponent.class.isAssignableFrom(cls)) { throw new IllegalArgumentException(format(ERROR_INVALID_TARGET, cls)); } targetClass = (Class<UIComponent>) cls; } } // Actions -------------------------------------------------------------------------------------------------------- @Override public void apply(FaceletContext context, UIComponent parent) throws IOException { List<UIComponent> oldChildren = new ArrayList<>(parent.getChildren()); nextHandler.apply(context, parent); if (ComponentHandler.isNew(parent)) { List<UIComponent> newChildren = new ArrayList<>(parent.getChildren()); newChildren.removeAll(oldChildren); applyMassAttribute(context, newChildren); } } private void applyMassAttribute(FaceletContext context, List<UIComponent> children) { for (UIComponent component : children) { if ((targetClass == null || targetClass.isAssignableFrom(component.getClass())) && component.getValueExpression(name) == null) { Object literalValue = component.getAttributes().get(name); if (literalValue == null || literalValue instanceof Boolean) { Class<?> type = (literalValue == null) ? Object.class : Boolean.class; component.setValueExpression(name, value.getValueExpression(context, type)); } } applyMassAttribute(context, component.getChildren()); } } }