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.markup.parser;
018
019import java.util.Map;
020
021import org.apache.wicket.util.lang.Objects;
022import org.apache.wicket.util.string.AppendingStringBuffer;
023import org.apache.wicket.util.string.StringValue;
024import org.apache.wicket.util.value.AttributeMap;
025import org.apache.wicket.util.value.IValueMap;
026
027
028/**
029 * A subclass of MarkupElement which represents a tag including namespace and its optional
030 * attributes. XmlTags are returned by the XML parser.
031 * 
032 * @author Jonathan Locke
033 */
034public class XmlTag
035{
036        /**
037         * Enumerated type for different kinds of component tags.
038         */
039        public static enum TagType {
040                /** A close tag, like </TAG>. */
041                CLOSE,
042
043                /** An open tag, like <TAG componentId = "xyz">. */
044                OPEN,
045
046                /** An open/close tag, like <TAG componentId = "xyz"/>. */
047                OPEN_CLOSE;
048        }
049
050        TextSegment text;
051
052        /** Attribute map. */
053        private AttributeMap attributes;
054
055        /** Name of tag, such as "img" or "input". */
056        String name;
057
058        /** Namespace of the tag, if available, such as <wicket:link ...> */
059        String namespace;
060
061        /** The tag type (OPEN, CLOSE or OPEN_CLOSE). */
062        TagType type;
063
064        /** Any component tag that this tag closes. */
065        private XmlTag closes;
066
067        /** If mutable, the immutable tag that this tag is a mutable copy of. */
068        private XmlTag copyOf = this;
069
070        /** True if this tag is mutable, false otherwise. */
071        private boolean isMutable = true;
072
073        /**
074         * Construct.
075         */
076        public XmlTag()
077        {
078                super();
079        }
080
081        /**
082         * Construct.
083         * 
084         * @param text
085         * @param type
086         */
087        public XmlTag(final TextSegment text, final TagType type)
088        {
089                this.text = text;
090                this.type = type;
091        }
092
093        /**
094         * Gets whether this tag closes the provided open tag.
095         * 
096         * @param open
097         *            The open tag
098         * @return True if this tag closes the given open tag
099         */
100        public final boolean closes(final XmlTag open)
101        {
102                return (closes == open) || ((closes == open.copyOf) && (this != open));
103        }
104
105        /**
106         * @param element
107         * @return true, if namespace, name and attributes are the same
108         */
109        public final boolean equalTo(final XmlTag element)
110        {
111                final XmlTag that = element;
112                if (!Objects.equal(getNamespace(), that.getNamespace()))
113                {
114                        return false;
115                }
116                if (!getName().equals(that.getName()))
117                {
118                        return false;
119                }
120                return getAttributes().equals(that.getAttributes());
121        }
122
123        /**
124         * Gets a hashmap of this tag's attributes.
125         * 
126         * @return The tag's attributes
127         */
128        public IValueMap getAttributes()
129        {
130                return attributes();
131        }
132
133        private AttributeMap attributes()
134        {
135                if (attributes == null)
136                {
137                        if ((copyOf == this) || (copyOf == null) || (copyOf.attributes == null))
138                        {
139                                attributes = new AttributeMap();
140                        }
141                        else
142                        {
143                                attributes = new AttributeMap(copyOf.attributes);
144                        }
145                }
146                return attributes;
147        }
148
149        /**
150         * @return true if there 1 or more attributes.
151         */
152        public boolean hasAttributes()
153        {
154                return attributes != null && attributes.size() > 0;
155        }
156
157        /**
158         * Get the column number.
159         * 
160         * @return Returns the columnNumber.
161         */
162        public int getColumnNumber()
163        {
164                return (text != null ? text.columnNumber : 0);
165        }
166
167        /**
168         * Gets the length of the tag in characters.
169         * 
170         * @return The tag's length
171         */
172        public int getLength()
173        {
174                return (text != null ? text.text.length() : 0);
175        }
176
177        /**
178         * Get the line number.
179         * 
180         * @return Returns the lineNumber.
181         */
182        public int getLineNumber()
183        {
184                return (text != null ? text.lineNumber : 0);
185        }
186
187        /**
188         * Gets the name of the tag, for example the tag <code>&lt;b&gt;</code>'s name would be 'b'.
189         * 
190         * @return The tag's name
191         */
192        public String getName()
193        {
194                return name;
195        }
196
197        /**
198         * Namespace of the tag, if available. For example, &lt;wicket:link&gt;.
199         * 
200         * @return The tag's namespace
201         */
202        public String getNamespace()
203        {
204                return namespace;
205        }
206
207        /**
208         * Assuming this is a close tag, return the corresponding open tag
209         * 
210         * @return The open tag. Null, if no open tag available
211         */
212        public final XmlTag getOpenTag()
213        {
214                return closes;
215        }
216
217        /**
218         * Gets the location of the tag in the input string.
219         * 
220         * @return Tag location (index in input string)
221         */
222        public int getPos()
223        {
224                return (text != null ? text.pos : 0);
225        }
226
227        /**
228         * Get a string attribute.
229         * 
230         * @param key
231         *            The key
232         * @return The string value
233         */
234        public CharSequence getAttribute(final String key)
235        {
236                return getAttributes().getCharSequence(key);
237        }
238
239        /**
240         * Get the tag type.
241         * 
242         * @return the tag type (OPEN, CLOSE or OPEN_CLOSE).
243         */
244        public TagType getType()
245        {
246                return type;
247        }
248
249        /**
250         * Gets whether this is a close tag.
251         * 
252         * @return True if this tag is a close tag
253         */
254        public boolean isClose()
255        {
256                return type == TagType.CLOSE;
257        }
258
259        /**
260         * 
261         * @return True, if tag is mutable
262         */
263        public final boolean isMutable()
264        {
265                return isMutable;
266        }
267
268        /**
269         * Gets whether this is an open tag.
270         * 
271         * @return True if this tag is an open tag
272         */
273        public boolean isOpen()
274        {
275                return type == TagType.OPEN;
276        }
277
278        /**
279         * Gets whether this tag is an open/ close tag.
280         * 
281         * @return True if this tag is an open and a close tag
282         */
283        public boolean isOpenClose()
284        {
285                return type == TagType.OPEN_CLOSE;
286        }
287
288        /**
289         * Makes this tag object immutable by making the attribute map unmodifiable. Immutable tags
290         * cannot be made mutable again. They can only be copied into new mutable tag objects.
291         * 
292         * @return this
293         */
294        public XmlTag makeImmutable()
295        {
296                if (isMutable)
297                {
298                        isMutable = false;
299                        if (attributes != null)
300                        {
301                                attributes.makeImmutable();
302                                text = null;
303                        }
304                }
305                return this;
306        }
307
308        /**
309         * Gets this tag if it is already mutable, or a mutable copy of this tag if it is immutable.
310         * 
311         * @return This tag if it is already mutable, or a mutable copy of this tag if it is immutable.
312         */
313        public XmlTag mutable()
314        {
315                if (isMutable)
316                {
317                        return this;
318                }
319                else
320                {
321                        final XmlTag tag = new XmlTag();
322                        copyPropertiesTo(tag);
323                        return tag;
324                }
325        }
326
327        /**
328         * Copies all internal properties from this tag to <code>dest</code>. This is basically cloning
329         * without instance creation.
330         * 
331         * @param dest
332         *            tag whose properties will be set
333         */
334        void copyPropertiesTo(final XmlTag dest)
335        {
336                dest.namespace = namespace;
337                dest.name = name;
338                dest.text = text;
339                dest.type = type;
340                dest.isMutable = true;
341                dest.closes = closes;
342                dest.copyOf = copyOf;
343                if (attributes != null)
344                {
345                        dest.attributes = new AttributeMap(attributes);
346                }
347        }
348
349        /**
350         * Puts a boolean attribute.
351         * 
352         * @param key
353         *            The key
354         * @param value
355         *            The value
356         * @return previous value associated with specified key, or null if there was no mapping for
357         *         key. A null return can also indicate that the map previously associated null with the
358         *         specified key, if the implementation supports null values.
359         */
360        public Object put(final String key, final boolean value)
361        {
362                return put(key, Boolean.toString(value));
363        }
364
365        /**
366         * Puts an int attribute.
367         * 
368         * @param key
369         *            The key
370         * @param value
371         *            The value
372         * @return previous value associated with specified key, or null if there was no mapping for
373         *         key. A null return can also indicate that the map previously associated null with the
374         *         specified key, if the implementation supports null values.
375         */
376        public Object put(final String key, final int value)
377        {
378                return put(key, Integer.toString(value));
379        }
380
381        /**
382         * Puts a string attribute.
383         * 
384         * @param key
385         *            The key
386         * @param value
387         *            The value
388         * @return previous value associated with specified key, or null if there was no mapping for
389         *         key. A null return can also indicate that the map previously associated null with the
390         *         specified key, if the implementation supports null values.
391         */
392        public Object put(final String key, final CharSequence value)
393        {
394                return getAttributes().put(key, value);
395        }
396
397        /**
398         * Puts a {@link StringValue}attribute.
399         * 
400         * @param key
401         *            The key
402         * @param value
403         *            The value
404         * @return previous value associated with specified key, or null if there was no mapping for
405         *         key. A null return can also indicate that the map previously associated null with the
406         *         specified key, if the implementation supports null values.
407         */
408        public Object put(final String key, final StringValue value)
409        {
410                return getAttributes().put(key, (value != null) ? value.toString() : null);
411        }
412
413        /**
414         * Puts all attributes in map
415         * 
416         * @param map
417         *            A key/value map
418         */
419        public void putAll(final Map<String, Object> map)
420        {
421                for (final Map.Entry<String, Object> entry : map.entrySet())
422                {
423                        Object value = entry.getValue();
424                        put(entry.getKey(), (value != null) ? value.toString() : null);
425                }
426        }
427
428        /**
429         * Removes an attribute.
430         * 
431         * @param key
432         *            The key to remove
433         */
434        public void remove(final String key)
435        {
436                getAttributes().remove(key);
437        }
438
439        /**
440         * Sets the tag name.
441         * 
442         * @param name
443         *            New tag name
444         */
445        public void setName(final String name)
446        {
447                if (isMutable)
448                {
449                        this.name = name.intern();
450                }
451                else
452                {
453                        throw new UnsupportedOperationException("Attempt to set name of immutable tag");
454                }
455        }
456
457        /**
458         * Sets the tag namespace.
459         * 
460         * @param namespace
461         *            New tag name
462         */
463        public void setNamespace(final String namespace)
464        {
465                if (isMutable)
466                {
467                        this.namespace = namespace != null ? namespace.intern() : null;
468                }
469                else
470                {
471                        throw new UnsupportedOperationException("Attempt to set namespace of immutable tag");
472                }
473        }
474
475        /**
476         * Assuming this is a close tag, assign it's corresponding open tag.
477         * 
478         * @param tag
479         *            the open-tag
480         * @throws RuntimeException
481         *             if 'this' is not a close tag
482         */
483        public void setOpenTag(final XmlTag tag)
484        {
485                closes = tag;
486        }
487
488        /**
489         * Sets type of this tag if it is not immutable.
490         * 
491         * @param type
492         *            The new type
493         */
494        public void setType(final TagType type)
495        {
496                if (isMutable)
497                {
498                        this.type = type;
499                }
500                else
501                {
502                        throw new UnsupportedOperationException("Attempt to set type of immutable tag");
503                }
504        }
505
506        /**
507         * Converts this object to a string representation.
508         * 
509         * @return String version of this object
510         */
511        public String toDebugString()
512        {
513                return "[Tag name = " + name + ", pos = " + text.pos + ", line = " + text.lineNumber +
514                        ", attributes = [" + getAttributes() + "], type = " + type + "]";
515        }
516
517        /**
518         * Converts this object to a string representation.
519         * 
520         * @return String version of this object
521         */
522        @Override
523        public String toString()
524        {
525                return toCharSequence().toString();
526        }
527
528        /**
529         * @return The string representation of the tag
530         */
531        public CharSequence toCharSequence()
532        {
533                if (!isMutable && (text != null))
534                {
535                        return text.text;
536                }
537
538                return toXmlString();
539        }
540
541        /**
542         * String representation with line and column number
543         * 
544         * @return String version of this object
545         */
546        public String toUserDebugString()
547        {
548                return " '" + toString() + "' (line " + getLineNumber() + ", column " + getColumnNumber() +
549                        ")";
550        }
551
552        /**
553         * Assuming some attributes have been changed, toXmlString() rebuilds the String on based on the
554         * tags informations.
555         * 
556         * @return A xml string matching the tag
557         */
558        public CharSequence toXmlString()
559        {
560                final AppendingStringBuffer buffer = new AppendingStringBuffer();
561
562                buffer.append('<');
563
564                if (type == TagType.CLOSE)
565                {
566                        buffer.append('/');
567                }
568
569                if (namespace != null)
570                {
571                        buffer.append(namespace);
572                        buffer.append(':');
573                }
574
575                buffer.append(name);
576
577                buffer.append(attributes().toCharSequence());
578
579                if (type == TagType.OPEN_CLOSE)
580                {
581                        buffer.append('/');
582                }
583
584                buffer.append('>');
585                return buffer;
586        }
587
588        static class TextSegment
589        {
590                /** Column number. */
591                final int columnNumber;
592
593                /** Line number. */
594                final int lineNumber;
595
596                /** Position of this tag in the input that was parsed. */
597                final int pos;
598
599                /** Full text of tag. */
600                final CharSequence text;
601
602                TextSegment(final CharSequence text, final int pos, final int line, final int col)
603                {
604                        this.text = text;
605                        this.pos = pos;
606                        lineNumber = line;
607                        columnNumber = col;
608                }
609
610                /**
611                 * 
612                 * @return The xml markup text
613                 */
614                public final CharSequence getText()
615                {
616                        return text;
617                }
618
619                /**
620                 * @see java.lang.Object#toString()
621                 */
622                @Override
623                public String toString()
624                {
625                        return text.toString();
626                }
627        }
628}