/* * 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.markup.parser; import java.util.Iterator; import java.util.Map; import org.apache.wicket.markup.parser.IXmlPullParser.HttpTagType; import org.apache.wicket.util.lang.Objects; import org.apache.wicket.util.string.AppendingStringBuffer; import org.apache.wicket.util.string.StringValue; import org.apache.wicket.util.string.Strings; import org.apache.wicket.util.value.IValueMap; import org.apache.wicket.util.value.ValueMap; /** * A subclass of MarkupElement which represents a tag including namespace and its optional * attributes. XmlTags are returned by the XML parser. * * @author Jonathan Locke */ public class XmlTag { /** * Enumerated type for different kinds of component tags. */ public static enum TagType { /** A close tag, like </TAG>. */ CLOSE("CLOSE"), /** An open tag, like <TAG componentId = "xyz">. */ OPEN("OPEN"), /** An open/close tag, like <TAG componentId = "xyz"/>. */ OPEN_CLOSE("OPEN_CLOSE"); private String name; TagType(final String name) { this.name = name; } } TextSegment text; /** Attribute map. */ private IValueMap attributes; /** Name of tag, such as "img" or "input". */ String name; /** Namespace of the tag, if available, such as <wicket:link ...> */ String namespace; /** The tag type (OPEN, CLOSE or OPEN_CLOSE). */ TagType type; /** Any component tag that this tag closes. */ private XmlTag closes; /** If mutable, the immutable tag that this tag is a mutable copy of. */ private XmlTag copyOf = this; /** True if this tag is mutable, false otherwise. */ private boolean isMutable = true; private HttpTagType httpTagType; /** * Construct. */ public XmlTag() { super(); } /** * Construct. * * @param text * @param type */ public XmlTag(final TextSegment text, final TagType type) { this.text = text; this.type = type; } /** * Gets whether this tag closes the provided open tag. * * @param open * The open tag * @return True if this tag closes the given open tag */ public final boolean closes(final XmlTag open) { return (closes == open) || ((closes == open.copyOf) && (this != open)); } /** * @param element * @return true, if namespace, name and attributes are the same */ public final boolean equalTo(final XmlTag element) { final XmlTag that = element; if (!Objects.equal(getNamespace(), that.getNamespace())) { return false; } if (!getName().equals(that.getName())) { return false; } return getAttributes().equals(that.getAttributes()); } /** * Gets a hashmap of this tag's attributes. * * @return The tag's attributes */ public IValueMap getAttributes() { if (attributes == null) { if ((copyOf == this) || (copyOf == null) || (copyOf.attributes == null)) { attributes = new ValueMap(); } else { attributes = new ValueMap(copyOf.attributes); } } return attributes; } /** * @return true if there 1 or more attributes. */ public boolean hasAttributes() { return attributes != null && attributes.size() > 0; } /** * Get the column number. * * @return Returns the columnNumber. */ public int getColumnNumber() { return (text != null ? text.columnNumber : 0); } /** * Gets the length of the tag in characters. * * @return The tag's length */ public int getLength() { return (text != null ? text.text.length() : 0); } /** * Get the line number. * * @return Returns the lineNumber. */ public int getLineNumber() { return (text != null ? text.lineNumber : 0); } /** * Gets the name of the tag, for example the tag <code><b></code>'s name would be 'b'. * * @return The tag's name */ public String getName() { return name; } /** * Namespace of the tag, if available. For example, <wicket:link>. * * @return The tag's namespace */ public String getNamespace() { return namespace; } /** * Assuming this is a close tag, return the corresponding open tag * * @return The open tag. Null, if no open tag available */ public final XmlTag getOpenTag() { return closes; } /** * Gets the location of the tag in the input string. * * @return Tag location (index in input string) */ public int getPos() { return (text != null ? text.pos : 0); } /** * Get a string attribute. * * @param key * The key * @return The string value */ public CharSequence getAttribute(final String key) { return getAttributes().getCharSequence(key); } /** * Get the tag type. * * @return the tag type (OPEN, CLOSE or OPEN_CLOSE). */ public TagType getType() { return type; } /** * Gets whether this is a close tag. * * @return True if this tag is a close tag */ public boolean isClose() { return type == TagType.CLOSE; } /** * * @return True, if tag is mutable */ public final boolean isMutable() { return isMutable; } /** * Gets whether this is an open tag. * * @return True if this tag is an open tag */ public boolean isOpen() { return type == TagType.OPEN; } /** * Gets whether this tag is an open/ close tag. * * @return True if this tag is an open and a close tag */ public boolean isOpenClose() { return type == TagType.OPEN_CLOSE; } /** * Makes this tag object immutable by making the attribute map unmodifiable. Immutable tags * cannot be made mutable again. They can only be copied into new mutable tag objects. * * @return this */ public XmlTag makeImmutable() { if (isMutable) { isMutable = false; if (attributes != null) { attributes.makeImmutable(); text = null; } } return this; } /** * Gets this tag if it is already mutable, or a mutable copy of this tag if it is immutable. * * @return This tag if it is already mutable, or a mutable copy of this tag if it is immutable. */ public XmlTag mutable() { if (isMutable) { return this; } else { final XmlTag tag = new XmlTag(); copyPropertiesTo(tag); return tag; } } /** * Copies all internal properties from this tag to <code>dest</code>. This is basically cloning * without instance creation. * * @param dest * tag whose properties will be set */ void copyPropertiesTo(final XmlTag dest) { dest.namespace = namespace; dest.name = name; dest.text = text; dest.type = type; dest.isMutable = true; dest.closes = closes; dest.copyOf = copyOf; if (attributes != null) { dest.attributes = new ValueMap(attributes); } } /** * Puts a boolean attribute. * * @param key * The key * @param value * The value * @return previous value associated with specified key, or null if there was no mapping for * key. A null return can also indicate that the map previously associated null with the * specified key, if the implementation supports null values. */ public Object put(final String key, final boolean value) { return put(key, Boolean.toString(value)); } /** * Puts an int attribute. * * @param key * The key * @param value * The value * @return previous value associated with specified key, or null if there was no mapping for * key. A null return can also indicate that the map previously associated null with the * specified key, if the implementation supports null values. */ public Object put(final String key, final int value) { return put(key, Integer.toString(value)); } /** * Puts a string attribute. * * @param key * The key * @param value * The value * @return previous value associated with specified key, or null if there was no mapping for * key. A null return can also indicate that the map previously associated null with the * specified key, if the implementation supports null values. */ public Object put(final String key, final CharSequence value) { return getAttributes().put(key, value); } /** * Puts a {@link StringValue}attribute. * * @param key * The key * @param value * The value * @return previous value associated with specified key, or null if there was no mapping for * key. A null return can also indicate that the map previously associated null with the * specified key, if the implementation supports null values. */ public Object put(final String key, final StringValue value) { return getAttributes().put(key, (value != null) ? value.toString() : null); } /** * Puts all attributes in map * * @param map * A key/value map */ public void putAll(final Map<String, Object> map) { for (final Map.Entry<String, Object> entry : map.entrySet()) { Object value = entry.getValue(); put(entry.getKey(), (value != null) ? value.toString() : null); } } /** * Removes an attribute. * * @param key * The key to remove */ public void remove(final String key) { getAttributes().remove(key); } /** * Sets the tag name. * * @param name * New tag name */ public void setName(final String name) { if (isMutable) { this.name = name.intern(); } else { throw new UnsupportedOperationException("Attempt to set name of immutable tag"); } } /** * Sets the tag namespace. * * @param namespace * New tag name */ public void setNamespace(final String namespace) { if (isMutable) { this.namespace = namespace != null ? namespace.intern() : null; } else { throw new UnsupportedOperationException("Attempt to set namespace of immutable tag"); } } /** * Assuming this is a close tag, assign it's corresponding open tag. * * @param tag * the open-tag * @throws RuntimeException * if 'this' is not a close tag */ public void setOpenTag(final XmlTag tag) { closes = tag; } /** * Sets type of this tag if it is not immutable. * * @param type * The new type */ public void setType(final TagType type) { if (isMutable) { this.type = type; } else { throw new UnsupportedOperationException("Attempt to set type of immutable tag"); } } /** * Converts this object to a string representation. * * @return String version of this object */ public String toDebugString() { return "[Tag name = " + name + ", pos = " + text.pos + ", line = " + text.lineNumber + ", attributes = [" + getAttributes() + "], type = " + type + "]"; } /** * Converts this object to a string representation. * * @return String version of this object */ @Override public String toString() { return toCharSequence().toString(); } /** * @return The string representation of the tag */ public CharSequence toCharSequence() { if (!isMutable && (text != null)) { return text.text; } return toXmlString(null); } /** * String representation with line and column number * * @return String version of this object */ public String toUserDebugString() { return " '" + toString() + "' (line " + getLineNumber() + ", column " + getColumnNumber() + ")"; } /** * Assuming some attributes have been changed, toXmlString() rebuilds the String on based on the * tags informations. * * @param attributeToBeIgnored * @return A xml string matching the tag */ public CharSequence toXmlString(final String attributeToBeIgnored) { final AppendingStringBuffer buffer = new AppendingStringBuffer(); buffer.append('<'); if (type == TagType.CLOSE) { buffer.append('/'); } if (namespace != null) { buffer.append(namespace); buffer.append(':'); } buffer.append(name); final IValueMap attributes = getAttributes(); if (attributes.size() > 0) { final Iterator<String> iterator = attributes.keySet().iterator(); for (; iterator.hasNext();) { final String key = iterator.next(); if ((key != null) && ((attributeToBeIgnored == null) || !key.equalsIgnoreCase(attributeToBeIgnored))) { buffer.append(' '); buffer.append(key); CharSequence value = getAttribute(key); // Attributes without values are possible, e.g. 'disabled' if (value != null) { buffer.append("=\""); value = Strings.escapeMarkup(value); buffer.append(value); buffer.append('"'); } } } } if (type == TagType.OPEN_CLOSE) { buffer.append('/'); } buffer.append('>'); return buffer; } static class TextSegment { /** Column number. */ final int columnNumber; /** Line number. */ final int lineNumber; /** Position of this tag in the input that was parsed. */ final int pos; /** Full text of tag. */ final CharSequence text; TextSegment(final CharSequence text, final int pos, final int line, final int col) { this.text = text; this.pos = pos; lineNumber = line; columnNumber = col; } /** * * @return The xml markup text */ public final CharSequence getText() { return text; } /** * @see java.lang.Object#toString() */ @Override public String toString() { return text.toString(); } } }