/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.wicket; import java.io.Serializable; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.parser.XmlTag.TagType; import org.apache.wicket.model.IComponentAssignedModel; import org.apache.wicket.model.IDetachable; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.io.IClusterable; import org.apache.wicket.util.lang.Args; import org.apache.wicket.util.value.IValueMap; /** * This class allows a tag attribute of a component to be modified dynamically with a value obtained * from a model object. This concept can be used to programmatically alter the attributes of * components, overriding the values specified in the markup. The two primary uses of this class are * to allow overriding of markup attributes based on business logic and to support dynamic * localization. The replacement occurs as the component tag is rendered to the response. * <p> * The attribute whose value is to be modified must be given on construction of the instance of this * class along with the model containing the value to replace with. * <p> * If an attribute is not in the markup, this modifier will add an attribute. * <p> * Instances of this class should be added to components via the {@link Component#add(Behavior...)} * method after the component has been constructed. * <p> * It is possible to create new subclasses of {@code AttributeModifier} by overriding the * {@link #newValue(String, String)} method. For example, you could create an * {@code AttributeModifier} subclass which appends the replacement value like this: * * <pre> * new AttributeModifier("myAttribute", model) * { * protected String newValue(final String currentValue, final String replacementValue) * { * return currentValue + replacementValue; * } * }; * </pre> * * @author Chris Turner * @author Eelco Hillenius * @author Jonathan Locke * @author Martijn Dashorst * @author Ralf Ebert */ public class AttributeModifier extends Behavior implements IClusterable { /** * Special attribute value markers. */ public enum MarkerValue { /** Marker value to have an attribute without a value added. */ VALUELESS_ATTRIBUTE_ADD, /** Marker value to have an attribute without a value removed. */ VALUELESS_ATTRIBUTE_REMOVE } /** Marker value to have an attribute without a value added. */ public static final MarkerValue VALUELESS_ATTRIBUTE_ADD = MarkerValue.VALUELESS_ATTRIBUTE_ADD; /** Marker value to have an attribute without a value removed. */ public static final MarkerValue VALUELESS_ATTRIBUTE_REMOVE = MarkerValue.VALUELESS_ATTRIBUTE_REMOVE; private static final long serialVersionUID = 1L; /** Attribute specification. */ private final String attribute; /** The model that is to be used for the replacement. */ private final IModel<?> replaceModel; /** * Create a new attribute modifier with the given attribute name and model to replace with. The * attribute will be added with the model value or the value will be replaced with the model * value if the attribute is already present. * * @param attribute * The attribute name to replace the value for * @param replaceModel * The model to replace the value with */ public AttributeModifier(final String attribute, final IModel<?> replaceModel) { Args.notNull(attribute, "attribute"); this.attribute = attribute; this.replaceModel = replaceModel; } /** * Create a new attribute modifier with the given attribute name and model to replace with. The * attribute will be added with the model value or the value will be replaced with the value if * the attribute is already present. * * @param attribute * The attribute name to replace the value for * @param value * The value for the attribute */ public AttributeModifier(String attribute, Serializable value) { this(attribute, Model.of(value)); } /** * Detach the value if it was a {@link IDetachable}. Internal method, shouldn't be called from * the outside. If the attribute modifier is shared, the detach method will be called multiple * times. * * @param component * the model that initiates the detachment */ @Override public final void detach(Component component) { if (replaceModel != null) replaceModel.detach(); } /** * @return the attribute name to replace the value for */ public final String getAttribute() { return attribute; } @Override public final void onComponentTag(Component component, ComponentTag tag) { if (tag.getType() != TagType.CLOSE) replaceAttributeValue(component, tag); } /** * Checks the given component tag for an instance of the attribute to modify and if all criteria * are met then replace the value of this attribute with the value of the contained model * object. * * @param component * The component * @param tag * The tag to replace the attribute value for */ public final void replaceAttributeValue(final Component component, final ComponentTag tag) { if (isEnabled(component)) { final IValueMap attributes = tag.getAttributes(); final Object replacementValue = getReplacementOrNull(component); if (VALUELESS_ATTRIBUTE_ADD == replacementValue) { attributes.put(attribute, null); } else if (VALUELESS_ATTRIBUTE_REMOVE == replacementValue) { attributes.remove(attribute); } else { final String value = toStringOrNull(attributes.get(attribute)); final Serializable newValue = newValue(value, toStringOrNull(replacementValue)); if (newValue == VALUELESS_ATTRIBUTE_REMOVE) { attributes.remove(attribute); } else if (newValue != null) { attributes.put(attribute, newValue); } } } } @Override public String toString() { return "[AttributeModifier attribute=" + attribute + ", replaceModel=" + replaceModel + "]"; } /** * gets replacement with null check. * * @param component * @return replacement value */ private Object getReplacementOrNull(final Component component) { IModel<?> model = replaceModel; if (model instanceof IComponentAssignedModel) { model = ((IComponentAssignedModel<?>)model).wrapOnAssignment(component); } return (model != null) ? model.getObject() : null; } /** * gets replacement as a string with null check. * * @param replacementValue * @return replacement value as a string */ private String toStringOrNull(final Object replacementValue) { return (replacementValue != null) ? replacementValue.toString() : null; } /** * Gets the replacement model. Allows subclasses access to replace model. * * @return the replace model of this attribute modifier */ protected final IModel<?> getReplaceModel() { return replaceModel; } /** * Gets the value that should replace the current attribute value. This gives users the ultimate * means to customize what will be used as the attribute value. For instance, you might decide * to append the replacement value to the current instead of just replacing it as is Wicket's * default. * * @param currentValue * The current attribute value. This value might be null! * @param replacementValue * The replacement value. This value might be null! * @return The value that should replace the current attribute value */ protected Serializable newValue(final String currentValue, final String replacementValue) { return replacementValue; } /** * Creates a attribute modifier that replaces the current value with the given value. * * @param attributeName * @param value * @return the attribute modifier * @since 1.5 */ public static AttributeModifier replace(String attributeName, IModel<?> value) { Args.notEmpty(attributeName, "attributeName"); return new AttributeModifier(attributeName, value); } /** * Creates a attribute modifier that replaces the current value with the given value. * * @param attributeName * @param value * @return the attribute modifier * @since 1.5 */ public static AttributeModifier replace(String attributeName, Serializable value) { Args.notEmpty(attributeName, "attributeName"); return new AttributeModifier(attributeName, value); } /** * Creates a attribute modifier that appends the current value with the given {@code value} * using a default space character (' ') separator. * * @param attributeName * @param value * @return the attribute modifier * @since 1.5 * @see AttributeAppender */ public static AttributeAppender append(String attributeName, IModel<?> value) { Args.notEmpty(attributeName, "attributeName"); return new AttributeAppender(attributeName, value).setSeparator(" "); } /** * Creates a attribute modifier that appends the current value with the given {@code value} * using a default space character (' ') separator. * * @param attributeName * @param value * @return the attribute modifier * @since 1.5 * @see AttributeAppender */ public static AttributeAppender append(String attributeName, Serializable value) { Args.notEmpty(attributeName, "attributeName"); return append(attributeName, Model.of(value)); } /** * Creates a attribute modifier that prepends the current value with the given {@code value} * using a default space character (' ') separator. * * @param attributeName * @param value * @return the attribute modifier * @since 1.5 * @see AttributeAppender */ public static AttributeAppender prepend(String attributeName, IModel<?> value) { Args.notEmpty(attributeName, "attributeName"); return new AttributeAppender(attributeName, value) { private static final long serialVersionUID = 1L; @Override protected Serializable newValue(String currentValue, String replacementValue) { // swap currentValue and replacementValue in the call to the concatenator return super.newValue(replacementValue, currentValue); } }.setSeparator(" "); } /** * Creates a attribute modifier that prepends the current value with the given {@code value} * using a default space character (' ') separator. * * @param attributeName * @param value * @return the attribute modifier * @since 1.5 * @see AttributeAppender */ public static AttributeAppender prepend(String attributeName, Serializable value) { Args.notEmpty(attributeName, "attributeName"); return prepend(attributeName, Model.of(value)); } /** * Creates a attribute modifier that removes an attribute with the specified name * * @param attributeName * the name of the attribute to be removed * @return the attribute modifier * @since 1.5 */ public static AttributeModifier remove(String attributeName) { Args.notEmpty(attributeName, "attributeName"); return replace(attributeName, Model.of(VALUELESS_ATTRIBUTE_REMOVE)); } }