001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket; 018 019import java.io.Serializable; 020 021import org.apache.wicket.behavior.AttributeAppender; 022import org.apache.wicket.behavior.Behavior; 023import org.apache.wicket.markup.ComponentTag; 024import org.apache.wicket.markup.parser.XmlTag.TagType; 025import org.apache.wicket.model.IComponentAssignedModel; 026import org.apache.wicket.model.IDetachable; 027import org.apache.wicket.model.IModel; 028import org.apache.wicket.model.Model; 029import org.apache.wicket.util.io.IClusterable; 030import org.apache.wicket.util.lang.Args; 031import org.apache.wicket.util.value.IValueMap; 032 033/** 034 * This class allows a tag attribute of a component to be modified dynamically with a value obtained 035 * from a model object. This concept can be used to programmatically alter the attributes of 036 * components, overriding the values specified in the markup. The two primary uses of this class are 037 * to allow overriding of markup attributes based on business logic and to support dynamic 038 * localization. The replacement occurs as the component tag is rendered to the response. 039 * <p> 040 * The attribute whose value is to be modified must be given on construction of the instance of this 041 * class along with the model containing the value to replace with. 042 * <p> 043 * If an attribute is not in the markup, this modifier will add an attribute. 044 * <p> 045 * Instances of this class should be added to components via the {@link Component#add(Behavior...)} 046 * method after the component has been constructed. 047 * <p> 048 * It is possible to create new subclasses of {@code AttributeModifier} by overriding the 049 * {@link #newValue(String, String)} method. For example, you could create an 050 * {@code AttributeModifier} subclass which appends the replacement value like this: 051 * 052 * <pre> 053 * new AttributeModifier("myAttribute", model) 054 * { 055 * protected String newValue(final String currentValue, final String replacementValue) 056 * { 057 * return currentValue + replacementValue; 058 * } 059 * }; 060 * </pre> 061 * 062 * @author Chris Turner 063 * @author Eelco Hillenius 064 * @author Jonathan Locke 065 * @author Martijn Dashorst 066 * @author Ralf Ebert 067 */ 068public class AttributeModifier extends Behavior implements IClusterable 069{ 070 /** 071 * Special attribute value markers. 072 */ 073 public enum MarkerValue { 074 /** Marker value to have an attribute without a value added. */ 075 VALUELESS_ATTRIBUTE_ADD, 076 077 /** Marker value to have an attribute without a value removed. */ 078 VALUELESS_ATTRIBUTE_REMOVE 079 } 080 081 /** Marker value to have an attribute without a value added. */ 082 public static final MarkerValue VALUELESS_ATTRIBUTE_ADD = MarkerValue.VALUELESS_ATTRIBUTE_ADD; 083 084 /** Marker value to have an attribute without a value removed. */ 085 public static final MarkerValue VALUELESS_ATTRIBUTE_REMOVE = MarkerValue.VALUELESS_ATTRIBUTE_REMOVE; 086 087 private static final long serialVersionUID = 1L; 088 089 /** Attribute specification. */ 090 private final String attribute; 091 092 /** The model that is to be used for the replacement. */ 093 private final IModel<?> replaceModel; 094 095 /** 096 * Create a new attribute modifier with the given attribute name and model to replace with. The 097 * attribute will be added with the model value or the value will be replaced with the model 098 * value if the attribute is already present. 099 * 100 * @param attribute 101 * The attribute name to replace the value for 102 * @param replaceModel 103 * The model to replace the value with 104 */ 105 public AttributeModifier(final String attribute, final IModel<?> replaceModel) 106 { 107 Args.notNull(attribute, "attribute"); 108 109 this.attribute = attribute; 110 this.replaceModel = replaceModel; 111 } 112 113 /** 114 * Create a new attribute modifier with the given attribute name and model to replace with. The 115 * attribute will be added with the model value or the value will be replaced with the value if 116 * the attribute is already present. 117 * 118 * @param attribute 119 * The attribute name to replace the value for 120 * @param value 121 * The value for the attribute 122 */ 123 public AttributeModifier(String attribute, Serializable value) 124 { 125 this(attribute, Model.of(value)); 126 } 127 128 /** 129 * Detach the value if it was a {@link IDetachable}. Internal method, shouldn't be called from 130 * the outside. If the attribute modifier is shared, the detach method will be called multiple 131 * times. 132 * 133 * @param component 134 * the model that initiates the detachment 135 */ 136 @Override 137 public final void detach(Component component) 138 { 139 if (replaceModel != null) 140 replaceModel.detach(); 141 } 142 143 /** 144 * @return the attribute name to replace the value for 145 */ 146 public final String getAttribute() 147 { 148 return attribute; 149 } 150 151 @Override 152 public final void onComponentTag(Component component, ComponentTag tag) 153 { 154 if (tag.getType() != TagType.CLOSE) 155 replaceAttributeValue(component, tag); 156 } 157 158 /** 159 * Checks the given component tag for an instance of the attribute to modify and if all criteria 160 * are met then replace the value of this attribute with the value of the contained model 161 * object. 162 * 163 * @param component 164 * The component 165 * @param tag 166 * The tag to replace the attribute value for 167 */ 168 public final void replaceAttributeValue(final Component component, final ComponentTag tag) 169 { 170 if (isEnabled(component)) 171 { 172 final IValueMap attributes = tag.getAttributes(); 173 final Object replacementValue = getReplacementOrNull(component); 174 175 if (VALUELESS_ATTRIBUTE_ADD == replacementValue) 176 { 177 attributes.put(attribute, null); 178 } 179 else if (VALUELESS_ATTRIBUTE_REMOVE == replacementValue) 180 { 181 attributes.remove(attribute); 182 } 183 else 184 { 185 final String value = toStringOrNull(attributes.get(attribute)); 186 final Serializable newValue = newValue(value, toStringOrNull(replacementValue)); 187 if (newValue == VALUELESS_ATTRIBUTE_REMOVE) 188 { 189 attributes.remove(attribute); 190 } 191 else if (newValue != null) 192 { 193 attributes.put(attribute, newValue); 194 } 195 } 196 } 197 } 198 199 @Override 200 public String toString() 201 { 202 return "[AttributeModifier attribute=" + attribute + ", replaceModel=" + replaceModel + "]"; 203 } 204 205 /** 206 * gets replacement with null check. 207 * 208 * @param component 209 * @return replacement value 210 */ 211 private Object getReplacementOrNull(final Component component) 212 { 213 IModel<?> model = replaceModel; 214 if (model instanceof IComponentAssignedModel) 215 { 216 model = ((IComponentAssignedModel<?>)model).wrapOnAssignment(component); 217 } 218 return (model != null) ? model.getObject() : null; 219 } 220 221 /** 222 * gets replacement as a string with null check. 223 * 224 * @param replacementValue 225 * @return replacement value as a string 226 */ 227 private String toStringOrNull(final Object replacementValue) 228 { 229 return (replacementValue != null) ? replacementValue.toString() : null; 230 } 231 232 /** 233 * Gets the replacement model. Allows subclasses access to replace model. 234 * 235 * @return the replace model of this attribute modifier 236 */ 237 protected final IModel<?> getReplaceModel() 238 { 239 return replaceModel; 240 } 241 242 /** 243 * Gets the value that should replace the current attribute value. This gives users the ultimate 244 * means to customize what will be used as the attribute value. For instance, you might decide 245 * to append the replacement value to the current instead of just replacing it as is Wicket's 246 * default. 247 * 248 * @param currentValue 249 * The current attribute value. This value might be null! 250 * @param replacementValue 251 * The replacement value. This value might be null! 252 * @return The value that should replace the current attribute value 253 */ 254 protected Serializable newValue(final String currentValue, final String replacementValue) 255 { 256 return replacementValue; 257 } 258 259 /** 260 * Creates a attribute modifier that replaces the current value with the given value. 261 * 262 * @param attributeName 263 * @param value 264 * @return the attribute modifier 265 * @since 1.5 266 */ 267 public static AttributeModifier replace(String attributeName, IModel<?> value) 268 { 269 Args.notEmpty(attributeName, "attributeName"); 270 271 return new AttributeModifier(attributeName, value); 272 } 273 274 /** 275 * Creates a attribute modifier that replaces the current value with the given value. 276 * 277 * @param attributeName 278 * @param value 279 * @return the attribute modifier 280 * @since 1.5 281 */ 282 public static AttributeModifier replace(String attributeName, Serializable value) 283 { 284 Args.notEmpty(attributeName, "attributeName"); 285 286 return new AttributeModifier(attributeName, value); 287 } 288 289 /** 290 * Creates a attribute modifier that appends the current value with the given {@code value} 291 * using a default space character (' ') separator. 292 * 293 * @param attributeName 294 * @param value 295 * @return the attribute modifier 296 * @since 1.5 297 * @see AttributeAppender 298 */ 299 public static AttributeAppender append(String attributeName, IModel<?> value) 300 { 301 Args.notEmpty(attributeName, "attributeName"); 302 303 return new AttributeAppender(attributeName, value).setSeparator(" "); 304 } 305 306 /** 307 * Creates a attribute modifier that appends the current value with the given {@code value} 308 * using a default space character (' ') separator. 309 * 310 * @param attributeName 311 * @param value 312 * @return the attribute modifier 313 * @since 1.5 314 * @see AttributeAppender 315 */ 316 public static AttributeAppender append(String attributeName, Serializable value) 317 { 318 Args.notEmpty(attributeName, "attributeName"); 319 320 return append(attributeName, Model.of(value)); 321 } 322 323 /** 324 * Creates a attribute modifier that prepends the current value with the given {@code value} 325 * using a default space character (' ') separator. 326 * 327 * @param attributeName 328 * @param value 329 * @return the attribute modifier 330 * @since 1.5 331 * @see AttributeAppender 332 */ 333 public static AttributeAppender prepend(String attributeName, IModel<?> value) 334 { 335 Args.notEmpty(attributeName, "attributeName"); 336 337 return new AttributeAppender(attributeName, value) 338 { 339 private static final long serialVersionUID = 1L; 340 341 @Override 342 protected Serializable newValue(String currentValue, String replacementValue) 343 { 344 // swap currentValue and replacementValue in the call to the concatenator 345 return super.newValue(replacementValue, currentValue); 346 } 347 }.setSeparator(" "); 348 } 349 350 /** 351 * Creates a attribute modifier that prepends the current value with the given {@code value} 352 * using a default space character (' ') separator. 353 * 354 * @param attributeName 355 * @param value 356 * @return the attribute modifier 357 * @since 1.5 358 * @see AttributeAppender 359 */ 360 public static AttributeAppender prepend(String attributeName, Serializable value) 361 { 362 Args.notEmpty(attributeName, "attributeName"); 363 364 return prepend(attributeName, Model.of(value)); 365 } 366 367 /** 368 * Creates a attribute modifier that removes an attribute with the specified name 369 * 370 * @param attributeName 371 * the name of the attribute to be removed 372 * @return the attribute modifier 373 * @since 1.5 374 */ 375 public static AttributeModifier remove(String attributeName) 376 { 377 Args.notEmpty(attributeName, "attributeName"); 378 379 return replace(attributeName, Model.of(VALUELESS_ATTRIBUTE_REMOVE)); 380 } 381}