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(&quot;myAttribute&quot;, 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}