blob: 4bd970812829c76e6dcb4fcf10eeed99306b1341 [file] [log] [blame]
/*
* 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;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.parser.XmlTag;
import org.apache.wicket.markup.parser.XmlTag.TagType;
import org.apache.wicket.markup.parser.filter.HtmlHandler;
import org.apache.wicket.request.Response;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Generics;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A subclass of MarkupElement which represents a "significant" markup tag, such as a component open
* tag. Insignificant markup tags (those which are merely concerned with markup formatting
* operations and do not denote components or component nesting) are coalesced into instances of
* RawMarkup (also a subclass of MarkupElement).
*
* @author Jonathan Locke
*/
public class ComponentTag extends MarkupElement
{
/**
* Factory that creates component during markup root container's initialization. These
* components get queued, which allows other components to be dequeued under these auto
* components.
*
* @author igor
*/
public interface IAutoComponentFactory
{
/**
* Creates a new instance of auto component to be queued
*
* @param container
* The component that will become a parent of the newly created auto component
* @param tag
* The markup element for the newly created auto component
*/
Component newComponent(MarkupContainer container, ComponentTag tag);
}
/** Log. */
private static final Logger log = LoggerFactory.getLogger(ComponentTag.class);
/** True if a href attribute is available and autolinking is on */
private final static int AUTOLINK = 0x0001;
/** True, if attributes have been modified or added */
private final static int MODIFIED = 0x0002;
/** If true, then the MarkupParser will ignore (remove) it. Temporary working variable */
private final static int IGNORE = 0x0004;
/** If true, then the tag contain an automatically created wicket id */
private final static int AUTO_COMPONENT = 0x0008;
/** Some HTML tags are allow to have no close tag, e.g. 'br' */
private final static int NO_CLOSE_TAG = 0x0010;
/** Render the tag as RawMarkup even if no Component can be found */
public final static int RENDER_RAW = 0x0020;
/** If true, the current tag contains a child or a descendant with the "wicket:id" attribute */
public final static int CONTAINS_WICKET_ID = 0x0040;
/** If close tag, than reference to the corresponding open tag */
private ComponentTag openTag;
/** The underlying xml tag */
protected final XmlTag xmlTag;
/** Boolean flags. See above */
private int flags = 0;
/**
* By default this is equal to the wicket:id="xxx" attribute value, but may be provided e.g. for
* auto-tags
*/
private String id;
/**
* In case of inherited markup, the base and the extended markups are merged and the information
* about the tags origin is lost. In some cases like wicket:head and wicket:link this
* information however is required.
*/
private WeakReference<Class<? extends Component>> markupClassRef = null;
/** added behaviors */
private List<Behavior> behaviors;
/** Filters and Handlers may add their own attributes to the tag */
private Map<String, Object> userData;
private IAutoComponentFactory autoComponentFactory;
/**
* Automatically create a XmlTag, assign the name and the type, and construct a ComponentTag
* based on this XmlTag.
*
* @param name
* The name of html tag
* @param type
* The type of tag
*/
public ComponentTag(final String name, final XmlTag.TagType type)
{
final XmlTag tag = new XmlTag();
tag.setName(name);
tag.setType(type);
xmlTag = tag;
}
/**
* Construct.
*
* @param tag
* The underlying xml tag
*/
public ComponentTag(final XmlTag tag)
{
super();
xmlTag = tag;
}
/**
* Constructor
*
* @param tag
* The ComponentTag tag which this wicket tag is based upon.
*/
public ComponentTag(final ComponentTag tag)
{
this(tag.getXmlTag());
tag.copyPropertiesTo(this);
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT USE IT!
*
* @param flag
* The flag to set
* @param set
* True to turn the flag on, false to turn it off
*/
public final void setFlag(final int flag, final boolean set)
{
if (set)
{
flags |= flag;
}
else
{
flags &= ~flag;
}
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT USE IT!
*
* @param flag
* The flag to test
* @return True if the flag is set
*/
public final boolean getFlag(final int flag)
{
return (flags & flag) != 0;
}
/**
* Adds a behavior to this component tag.
*
* @param behavior
*/
public final void addBehavior(final Behavior behavior)
{
Args.notNull(behavior, "behavior");
if (behaviors == null)
{
behaviors = Generics.newArrayList();
}
behaviors.add(behavior);
}
/**
* @return true if this tag has any behaviors added, false otherwise
*/
public final boolean hasBehaviors()
{
return behaviors != null;
}
/**
* @return read only iterator over added behaviors
*/
// TODO change to return unmodifiable list which will never be null. See Component.getBehavior
public final Iterator<? extends Behavior> getBehaviors()
{
if (behaviors == null)
{
List<Behavior> lst = Collections.emptyList();
return lst.iterator();
}
return Collections.unmodifiableCollection(behaviors).iterator();
}
/**
* Gets whether this tag closes the provided open tag.
*
* @param open
* The open tag
* @return True if this tag closes the given open tag
*/
@Override
public final boolean closes(final MarkupElement open)
{
if (open instanceof ComponentTag)
{
return (openTag == open) || getXmlTag().closes(((ComponentTag)open).getXmlTag());
}
return false;
}
/**
* If autolink is set to true, href attributes will automatically be converted into Wicket
* bookmarkable URLs.
*
* @param autolink
* enable/disable automatic href conversion
*/
public final void enableAutolink(final boolean autolink)
{
setFlag(AUTOLINK, autolink);
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#getAttributes()
* @return The tag#s attributes
*/
public final IValueMap getAttributes()
{
return xmlTag.getAttributes();
}
/**
* A convenient method. The same as getAttributes().getString(name)
*
* @param name
* @return The attributes value
*/
public final String getAttribute(String name)
{
return xmlTag.getAttributes().getString(name);
}
/**
* Get the tag's component id
*
* @return The component id attribute of this tag
*/
public final String getId()
{
return id;
}
/**
* Gets the length of the tag in characters.
*
* @return The tag's length
*/
public final int getLength()
{
return xmlTag.getLength();
}
/**
* @return The tag's name
*/
public final String getName()
{
return xmlTag.getName();
}
/**
* @return The tag's namespace
*/
public final String getNamespace()
{
return xmlTag.getNamespace();
}
/**
* If set, return the corresponding open tag (ComponentTag).
*
* @return The corresponding open tag
*/
public final ComponentTag getOpenTag()
{
return openTag;
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#getPos()
* @return Tag location (index in input string)
*/
public final int getPos()
{
return xmlTag.getPos();
}
/**
* @return the tag type (OPEN, CLOSE or OPEN_CLOSE).
*/
public final TagType getType()
{
return xmlTag.getType();
}
/**
* True if autolink is enabled and the tag contains a href attribute.
*
* @return True, if the href contained should automatically be converted
*/
public final boolean isAutolinkEnabled()
{
return getFlag(AUTOLINK);
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#isClose()
* @return True if this tag is a close tag
*/
public final boolean isClose()
{
return xmlTag.isClose();
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#isOpen()
* @return True if this tag is an open tag
*/
public final boolean isOpen()
{
return xmlTag.isOpen();
}
/**
* @param id
* Required component id
* @return True if this tag is an open tag with the given component name
* @see org.apache.wicket.markup.parser.XmlTag#isOpen()
*/
public final boolean isOpen(String id)
{
return xmlTag.isOpen() && this.id.equals(id);
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#isOpenClose()
* @return True if this tag is an open and a close tag
*/
public final boolean isOpenClose()
{
return xmlTag.isOpenClose();
}
/**
* @param id
* Required component id
* @return True if this tag is an openclose tag with the given component id
* @see org.apache.wicket.markup.parser.XmlTag#isOpenClose()
*/
public final boolean isOpenClose(String id)
{
return xmlTag.isOpenClose() && this.id.equals(id);
}
/**
* 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.
*/
public final void makeImmutable()
{
xmlTag.makeImmutable();
}
/**
* 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 ComponentTag mutable()
{
if (xmlTag.isMutable())
{
return this;
}
else
{
ComponentTag tag = new ComponentTag(xmlTag.mutable());
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 ComponentTag dest)
{
dest.id = id;
dest.flags = flags;
dest.autoComponentFactory = autoComponentFactory;
if (markupClassRef != null)
{
dest.setMarkupClass(markupClassRef.get());
}
if (behaviors != null)
{
dest.behaviors = new ArrayList<>(behaviors);
}
if (userData != null)
{
dest.userData = new HashMap<>(userData);
}
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#put(String, boolean)
* @param key
* The key
* @param value
* The value
*/
public final void put(final String key, final boolean value)
{
xmlTag.put(key, value);
setModified(true);
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#put(String, int)
* @param key
* The key
* @param value
* The value
*/
public final void put(final String key, final int value)
{
xmlTag.put(key, value);
setModified(true);
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#put(String, CharSequence)
* @param key
* The key
* @param value
* The value
*/
public final void put(String key, CharSequence value)
{
checkIdAttribute(key);
putInternal(key, value);
}
/**
* THIS METHOD IS NOT PART OF THE PUBLIC API, DO NOT CALL IT
*
* @see org.apache.wicket.markup.parser.XmlTag#put(String, CharSequence)
* @param key
* The key
* @param value
* The value
*/
public final void putInternal(String key, CharSequence value)
{
xmlTag.put(key, value);
setModified(true);
}
/**
* @param key
*/
private void checkIdAttribute(String key)
{
if ("id".equalsIgnoreCase(key))
{
log.warn("Please use component.setMarkupId(String) to change the tag's 'id' attribute.");
}
}
/**
* Appends specified {@code value} to the attribute
*
* @param key
* The key
* @param value
* The value
* @param separator
* The separator used to append the value
*/
public final void append(String key, CharSequence value, String separator)
{
String current = getAttribute(key);
if (Strings.isEmpty(current))
{
xmlTag.put(key, value);
}
else
{
xmlTag.put(key, current + separator + value);
}
setModified(true);
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#put(String, StringValue)
* @param key
* The key
* @param value
* The value
*/
public final void put(String key, StringValue value)
{
xmlTag.put(key, value);
setModified(true);
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#putAll(Map)
* @param map
* a key/value map
*/
public final void putAll(final Map<String, Object> map)
{
xmlTag.putAll(map);
setModified(true);
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#remove(String)
* @param key
* The key to remove
*/
public final void remove(String key)
{
xmlTag.remove(key);
setModified(true);
}
/**
* Gets whether this tag does not require a closing tag.
*
* @return True if this tag does not require a closing tag
*/
public final boolean requiresCloseTag()
{
if (getNamespace() == null)
{
return HtmlHandler.requiresCloseTag(getName());
}
else
{
return HtmlHandler.requiresCloseTag(getNamespace() + ":" + getName());
}
}
/**
* Set the component's id. The value is usually taken from the tag's id attribute, e.g.
* wicket:id="componentId".
*
* @param id
* The component's id assigned to the tag.
*/
public final void setId(final String id)
{
this.id = id;
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#setName(String)
* @param name
* New tag name
*/
public final void setName(String name)
{
xmlTag.setName(name);
}
/**
* @see org.apache.wicket.markup.parser.XmlTag#setNamespace(String)
* @param namespace
* New tag name namespace
*/
public final void setNamespace(String namespace)
{
xmlTag.setNamespace(namespace);
}
/**
* 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 final void setOpenTag(final ComponentTag tag)
{
openTag = tag;
getXmlTag().setOpenTag(tag.getXmlTag());
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
*
* @param type
* The new type
*/
public final void setType(final TagType type)
{
if (type != xmlTag.getType())
{
xmlTag.setType(type);
setModified(true);
}
}
/**
* @return A synthetic close tag for this tag
*/
public final CharSequence syntheticCloseTagString()
{
AppendingStringBuffer buf = new AppendingStringBuffer();
buf.append("</");
if (getNamespace() != null)
{
buf.append(getNamespace()).append(':');
}
buf.append(getName()).append('>');
return buf;
}
/**
* @see org.apache.wicket.markup.MarkupElement#toCharSequence()
*/
@Override
public CharSequence toCharSequence()
{
return xmlTag.toCharSequence();
}
/**
* Converts this object to a string representation.
*
* @return String version of this object
*/
@Override
public final String toString()
{
return toCharSequence().toString();
}
/**
* Write the tag to the response
*
* @param response
* The response to write to
* @param stripWicketAttributes
* if true, wicket:id are removed from output
* @param namespace
* Wicket's namespace to use
*/
public final void writeOutput(final Response response, final boolean stripWicketAttributes,
final String namespace)
{
response.write("<");
if (getType() == TagType.CLOSE)
{
response.write("/");
}
if (getNamespace() != null)
{
response.write(getNamespace());
response.write(":");
}
response.write(getName());
String namespacePrefix = null;
if (stripWicketAttributes == true)
{
namespacePrefix = namespace + ":";
}
if (getAttributes().size() > 0)
{
for (String key : getAttributes().keySet())
{
if (key == null)
{
continue;
}
if ((namespacePrefix == null) || (key.startsWith(namespacePrefix) == false))
{
response.write(" ");
response.write(key);
CharSequence value = getAttribute(key);
// attributes without values are possible, e.g.' disabled'
if (value != null)
{
response.write("=\"");
value = Strings.escapeMarkup(value);
response.write(value);
response.write("\"");
}
}
}
}
if (getType() == TagType.OPEN_CLOSE)
{
response.write("/");
}
response.write(">");
}
/**
* Converts this object to a string representation including useful information for debugging
*
* @return String version of this object
*/
@Override
public final String toUserDebugString()
{
return xmlTag.toUserDebugString();
}
/**
* @return Returns the underlying xml tag.
*/
public final XmlTag getXmlTag()
{
return xmlTag;
}
/**
* Manually mark the ComponentTag being modified. Flagging the tag being modified does not
* happen automatically.
*
* @param modified
*/
public final void setModified(final boolean modified)
{
setFlag(MODIFIED, modified);
}
/**
*
* @return True, if the component tag has been marked modified
*/
final boolean isModified()
{
return getFlag(MODIFIED);
}
/**
*
* @return True if the HTML tag (e.g. br) has no close tag
*/
public boolean hasNoCloseTag()
{
return getFlag(NO_CLOSE_TAG);
}
/**
* True if the HTML tag (e.g. br) has no close tag
*
* @param hasNoCloseTag
*/
public void setHasNoCloseTag(boolean hasNoCloseTag)
{
setFlag(NO_CLOSE_TAG, hasNoCloseTag);
}
/**
* Sets the flag to indicate if the current tag contains a child
* or a descendant with the "wicket:id" attribute.
*
* @param containsWicketId
*/
public void setContainsWicketId(boolean containsWicketId)
{
setFlag(CONTAINS_WICKET_ID, containsWicketId);
}
/**
* Says if the current tag contains a child or a descendant with the "wicket:id" attribute.
* @return true if the current tag contains a child or a descendant with the "wicket:id" attribute.
*/
public boolean containsWicketId()
{
return getFlag(CONTAINS_WICKET_ID);
}
/**
* In case of inherited markup, the base and the extended markups are merged and the information
* about the tags origin is lost. In some cases like wicket:head and wicket:link this
* information however is required.
*
* @return wicketHeaderClass
*/
public Class<? extends Component> getMarkupClass()
{
return (markupClassRef == null ? null : markupClassRef.get());
}
/**
* Set the class of wicket component which contains the wicket:head tag.
*
* @param <C>
*
* @param wicketHeaderClass
* wicketHeaderClass
*/
public <C extends Component> void setMarkupClass(Class<C> wicketHeaderClass)
{
if (wicketHeaderClass == null)
{
markupClassRef = null;
}
else
{
markupClassRef = new WeakReference<Class<? extends Component>>(wicketHeaderClass);
}
}
/**
* @see org.apache.wicket.markup.MarkupElement#equalTo(org.apache.wicket.markup.MarkupElement)
*/
@Override
public boolean equalTo(final MarkupElement element)
{
if (element instanceof ComponentTag)
{
final ComponentTag that = (ComponentTag)element;
return getXmlTag().equalTo(that.getXmlTag());
}
return false;
}
/**
* Gets ignore.
*
* @return If true than MarkupParser will remove it from the markup
*/
public boolean isIgnore()
{
return getFlag(IGNORE);
}
/**
* Sets ignore.
*
* @param ignore
* If true than MarkupParser will remove it from the markup
*/
public void setIgnore(boolean ignore)
{
setFlag(IGNORE, ignore);
}
/**
* @return True, if wicket:id has been automatically created (internal component)
*/
public boolean isAutoComponentTag()
{
return getFlag(AUTO_COMPONENT);
}
/**
* @param auto
* True, if wicket:id has been automatically created (internal component)
*/
public void setAutoComponentTag(boolean auto)
{
setFlag(AUTO_COMPONENT, auto);
}
/**
* Gets userData.
*
* @param key
* The key to store and retrieve the value
* @return userData
*/
public Object getUserData(final String key)
{
if (userData == null)
{
return null;
}
return userData.get(key);
}
/**
* Sets userData.
*
* @param key
* The key to store and retrieve the value
* @param value
* The user specific value to store
*/
public void setUserData(final String key, final Object value)
{
if (userData == null)
{
userData = new HashMap<>();
}
userData.put(key, value);
}
/**
* For subclasses to override. Gets called just before a Component gets rendered. It is
* guaranteed that the markupStream is set on the Component and determineVisibility is not yet
* called.
*
* @param component
* The component that is about to be rendered
* @param markupStream
* The current markup stream
*/
public void onBeforeRender(final Component component, final MarkupStream markupStream)
{
}
public IAutoComponentFactory getAutoComponentFactory()
{
return autoComponentFactory;
}
public void setAutoComponentFactory(IAutoComponentFactory autoComponentFactory)
{
this.autoComponentFactory = autoComponentFactory;
}
}