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