/* * 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; } }