blob: 558c13fb2c7a80b508512249aa16d8e8333875bb [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;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections4.map.LinkedMap;
import org.apache.wicket.core.util.string.ComponentStrings;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.ComponentTag.IAutoComponentFactory;
import org.apache.wicket.markup.IMarkupFragment;
import org.apache.wicket.markup.Markup;
import org.apache.wicket.markup.MarkupElement;
import org.apache.wicket.markup.MarkupException;
import org.apache.wicket.markup.MarkupFactory;
import org.apache.wicket.markup.MarkupNotFoundException;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.MarkupType;
import org.apache.wicket.markup.WicketTag;
import org.apache.wicket.markup.html.border.Border;
import org.apache.wicket.markup.html.form.AutoLabelResolver;
import org.apache.wicket.markup.resolver.ComponentResolvers;
import org.apache.wicket.model.IComponentInheritedModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.IWrapModel;
import org.apache.wicket.settings.DebugSettings;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Classes;
import org.apache.wicket.util.lang.Generics;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.visit.ClassVisitFilter;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;
import org.apache.wicket.util.visit.Visits;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A MarkupContainer holds a map of child components.
* <ul>
* <li><b>Children </b>- Children can be added by calling the {@link #add(Component...)} method, and
* they can be looked up using a colon separated path. For example, if a container called "a" held a
* nested container "b" which held a nested component "c", then a.get("b:c") would return the
* Component with id "c". The number of children in a MarkupContainer can be determined by calling
* size(), and the whole hierarchy of children held by a MarkupContainer can be traversed by calling
* visitChildren(), passing in an implementation of IVisitor.
*
* <li><b>Markup Rendering </b>- A MarkupContainer also holds/references associated markup which is
* used to render the container. As the markup stream for a container is rendered, component
* references in the markup are resolved by using the container to look up Components in the
* container's component map by id. Each component referenced by the markup stream is given an
* opportunity to render itself using the markup stream.
* <p>
* Components may alter their referring tag, replace the tag's body or insert markup after the tag.
* But components cannot remove tags from the markup stream. This is an important guarantee because
* graphic designers may be setting attributes on component tags that affect visual presentation.
* <p>
* The type of markup held in a given container subclass can be determined by calling
* {@link #getMarkupType()}. Markup is accessed via a MarkupStream object which allows a component
* to traverse ComponentTag and RawMarkup MarkupElements while rendering a response. Markup in the
* stream may be HTML or some other kind of markup, such as VXML, as determined by the specific
* container subclass.
* <p>
* A markup stream may be directly associated with a container via setMarkupStream. However, a
* container which does not have a markup stream (its getMarkupStream() returns null) may inherit a
* markup stream from a container above it in the component hierarchy. The
* {@link #findMarkupStream()} method will locate the first container at or above this container
* which has a markup stream.
* <p>
* All Page containers set a markup stream before rendering by calling the method
* {@link #getAssociatedMarkupStream(boolean)} to load the markup associated with the page. Since
* Page is at the top of the container hierarchy, it is guaranteed that {@link #findMarkupStream()}
* will always return a valid markup stream.
*
* @see MarkupStream
* @author Jonathan Locke
*/
public abstract class MarkupContainer extends Component implements Iterable<Component>
{
private static final long serialVersionUID = 1L;
private static final int INITIAL_CHILD_LIST_CAPACITY = 12;
/**
* The threshold where we start using a Map to store children in, replacing a List. Adding
* components to a list is O(n), and to a map O(1). The magic number is 24, due to a Map using
* more memory to store its elements and below 24 children there's no discernible difference
* between adding to a Map or a List.
*
* We have focused on adding elements to a list, instead of indexed lookups because adding is an
* action that is performed very often, and lookups often are done by component IDs, not index.
*/
static final int MAPIFY_THRESHOLD = 24; // 32 * 0.75
/** Log for reporting. */
private static final Logger log = LoggerFactory.getLogger(MarkupContainer.class);
/**
* Metadata key for looking up the list of removed children necessary for tracking modifications
* during iteration of the children of this markup container.
*
* This is stored in meta data because it only is necessary when a child is removed, and this
* saves the memory necessary for a field on a widely used class.
*/
private static final MetaDataKey<LinkedList<RemovedChild>> REMOVALS_KEY = new MetaDataKey<LinkedList<RemovedChild>>()
{
private static final long serialVersionUID = 1L;
};
/**
* Administrative class for detecting removed children during child iteration. Not intended to
* be serializable but for e.g. determining the size of the component it has to be serializable.
*/
private static class RemovedChild implements Serializable
{
private static final long serialVersionUID = 1L;
private transient final Component removedChild;
private transient final Component previousSibling;
private RemovedChild(Component removedChild, Component previousSibling)
{
this.removedChild = removedChild;
this.previousSibling = previousSibling;
}
}
/**
* Administrative counter to keep track of modifications to the list of children during
* iteration.
*
* When the {@link #children_size()} changes due to an addition or removal of a child component,
* the modCounter is increased. This way iterators that iterate over the children of this
* container can keep track when they need to change their iteration strategy.
*/
private transient int modCounter = 0;
/**
* The children of this markup container, if any. Can be a Component when there's only one
* child, a List when the number of children is fewer than {@link #MAPIFY_THRESHOLD} or a Map
* when there are more children.
*/
private Object children;
public MarkupContainer(final String id)
{
this(id, null);
}
public MarkupContainer(final String id, IModel<?> model)
{
super(id, model);
}
/**
* Adds the child component(s) to this container.
*
* @param children
* The child(ren) to add.
* @throws IllegalArgumentException
* Thrown if a child with the same id is replaced by the add operation.
* @return This
*/
public MarkupContainer add(final Component... children)
{
for (Component child : children)
{
Args.notNull(child, "child");
if (this == child)
{
throw new IllegalArgumentException(
exceptionMessage("Trying to add this component to itself."));
}
MarkupContainer parent = getParent();
while (parent != null)
{
if (child == parent)
{
String msg = "You can not add a component's parent as child to the component (loop): Component: " +
this.toString(false) + "; parent == child: " + parent.toString(false);
if (child instanceof Border.BorderBodyContainer)
{
msg += ". Please consider using Border.addToBorder(new " +
Classes.simpleName(this.getClass()) + "(\"" + this.getId() +
"\", ...) instead of add(...)";
}
throw new WicketRuntimeException(msg);
}
parent = parent.getParent();
}
checkHierarchyChange(child);
if (log.isDebugEnabled())
{
log.debug("Add " + child.getId() + " to " + this);
}
// Add the child to my children
Component previousChild = children_put(child);
if (previousChild != null && previousChild != child)
{
throw new IllegalArgumentException(
exceptionMessage("A child '" + previousChild.getClass().getSimpleName() +
"' with id '" + child.getId() + "' already exists"));
}
addedComponent(child);
}
return this;
}
/**
* Replaces a child component of this container with another or just adds it in case no child
* with the same id existed yet.
*
* @param children
* The child(ren) to be added or replaced
* @return this markup container
*/
public MarkupContainer addOrReplace(final Component... children)
{
for (Component child : children)
{
Args.notNull(child, "child");
checkHierarchyChange(child);
if (get(child.getId()) == null)
{
add(child);
}
else
{
replace(child);
}
}
return this;
}
/**
* This method allows a component to be added by an auto-resolver such as AutoLinkResolver.
* While the component is being added, the component's FLAG_AUTO boolean is set. The isAuto()
* method of Component returns true if a component or any of its parents has this bit set. When
* a component is added via autoAdd(), the logic in Page that normally (a) checks for
* modifications during the rendering process, and (b) versions components, is bypassed if
* Component.isAuto() returns true.
* <p>
* The result of all this is that components added with autoAdd() are free from versioning and
* can add their own children without the usual exception that would normally be thrown when the
* component hierarchy is modified during rendering.
*
* @param component
* The component to add
* @param markupStream
* Null, if the parent container is able to provide the markup. Else the markup
* stream to be used to render the component.
* @return True, if component has been added
*/
public final boolean autoAdd(final Component component, MarkupStream markupStream)
{
Args.notNull(component, "component");
// Replace strategy
component.setAuto(true);
if (markupStream != null)
{
component.setMarkup(markupStream.getMarkupFragment());
}
// Add the child to the parent.
// Arguably child.setParent() can be used as well. It connects the child to the parent and
// that's all what most auto-components need. Unfortunately child.onDetach() will not / can
// not be invoked, since the parent doesn't known its one of his children. Hence we need to
// properly add it.
children_remove(component.getId());
add(component);
return true;
}
/**
* @param component
* The component to check
* @param recurse
* True if all descendents should be considered
* @return True if the component is contained in this container
*/
public boolean contains(final Component component, final boolean recurse)
{
Args.notNull(component, "component");
if (recurse)
{
// Start at component and continue while we're not out of parents
for (Component current = component; current != null;)
{
// Get parent
final MarkupContainer parent = current.getParent();
// If this container is the parent, then the component is
// recursively contained by this container
if (parent == this)
{
// Found it!
return true;
}
// Move up the chain to the next parent
current = parent;
}
// Failed to find this container in component's ancestry
return false;
}
else
{
// Is the component contained in this container?
return component.getParent() == this;
}
}
/**
* Get a child component by looking it up with the given path.
* <p>
* A component path consists of component ids separated by colons, e.g. "b:c" identifies a
* component "c" inside container "b" inside this container.
*
* @param path
* path to component
* @return The component at the path
*/
@Override
public final Component get(String path)
{
// Reference to this container
if (Strings.isEmpty(path))
{
return this;
}
// process parent .. references
MarkupContainer container = this;
String id = Strings.firstPathComponent(path, Component.PATH_SEPARATOR);
while (Component.PARENT_PATH.equals(id))
{
container = container.getParent();
if (container == null)
{
return null;
}
path = path.length() == id.length() ? "" : path.substring(id.length() + 1);
id = Strings.firstPathComponent(path, Component.PATH_SEPARATOR);
}
if (Strings.isEmpty(id))
{
return container;
}
// Get child by id
Component child = container.children_get(id);
// Found child?
if (child != null)
{
String path2 = Strings.afterFirstPathComponent(path, Component.PATH_SEPARATOR);
// Recurse on latter part of path
return child.get(path2);
}
return null;
}
/**
* Gets a fresh markup stream that contains the (immutable) markup resource for this class.
*
* @param throwException
* If true, throw an exception, if markup could not be found
* @return A stream of MarkupElement elements
*/
public MarkupStream getAssociatedMarkupStream(final boolean throwException)
{
IMarkupFragment markup = getAssociatedMarkup();
// If we found markup for this container
if (markup != null)
{
return new MarkupStream(markup);
}
if (throwException == true)
{
// throw exception since there is no associated markup
throw new MarkupNotFoundException(
"Markup of type '" +
getMarkupType().getExtension() +
"' for component '" +
getClass().getName() +
"' not found." +
" Enable debug messages for org.apache.wicket.util.resource to get a list of all filenames tried.: " +
toString());
}
return null;
}
/**
* Gets a fresh markup stream that contains the (immutable) markup resource for this class.
*
* @return A stream of MarkupElement elements. Null if not found.
*/
public Markup getAssociatedMarkup()
{
try
{
Markup markup = MarkupFactory.get().getMarkup(this, false);
// If we found markup for this container
if ((markup != null) && (markup != Markup.NO_MARKUP))
{
return markup;
}
return null;
}
catch (MarkupException ex)
{
// re-throw it. The exception contains already all the information
// required.
throw ex;
}
catch (MarkupNotFoundException ex)
{
// re-throw it. The exception contains already all the information
// required.
throw ex;
}
catch (WicketRuntimeException ex)
{
// throw exception since there is no associated markup
throw new MarkupNotFoundException(
exceptionMessage("Markup of type '" + getMarkupType().getExtension() +
"' for component '" + getClass().getName() + "' not found." +
" Enable debug messages for org.apache.wicket.util.resource to get a list of all filenames tried"),
ex);
}
}
/**
* Get the markup of the child.
*
* @see Component#getMarkup()
*
* @param child
* The child component. If null, the container's markup will be returned. See Border,
* Panel or Enclosure where getMarkup(null) != getMarkup().
* @return The child's markup
*/
public IMarkupFragment getMarkup(final Component child)
{
// Delegate request to attached markup sourcing strategy.
return getMarkupSourcingStrategy().getMarkup(this, child);
}
/**
* Get the type of associated markup for this component. The markup type for a component is
* independent of whether or not the component actually has an associated markup resource file
* (which is determined at runtime).
*
* @return The type of associated markup for this component (for example, "html", "wml" or
* "vxml"). If there is no markup type for a component, null may be returned, but this
* means that no markup can be loaded for the class. Null is also returned if the
* component, or any of its parents, has not been added to a Page.
*/
public MarkupType getMarkupType()
{
MarkupContainer parent = getParent();
if (parent != null)
{
return parent.getMarkupType();
}
return null;
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT USE IT.
*
* Adds a child component to this container.
*
* @param child
* The child
* @throws IllegalArgumentException
* Thrown if a child with the same id is replaced by the add operation.
*/
public void internalAdd(final Component child)
{
if (log.isDebugEnabled())
{
log.debug("internalAdd " + child.getId() + " to " + this);
}
// Add to map
children_put(child);
addedComponent(child);
}
/**
* Gives an iterator that allow you to iterate through the children of this markup container in
* the order the children were added. The iterator supports additions and removals from the list
* of children during iteration.
*
* @return Iterator that iterates through children in the order they were added
*/
@Override
public Iterator<Component> iterator()
{
/**
* Iterator that knows how to change between a single child, list of children and map of
* children. Keeps track when the iterator was last sync'd with the markup container's
* tracking of changes to the list of children.
*/
class MarkupChildIterator implements Iterator<Component>
{
private int indexInRemovalsSinceLastUpdate = removals_size();
private int expectedModCounter = -1;
private Component currentComponent = null;
private Iterator<Component> internalIterator = null;
@Override
public boolean hasNext()
{
refreshInternalIteratorIfNeeded();
return internalIterator.hasNext();
}
@Override
public Component next()
{
refreshInternalIteratorIfNeeded();
return currentComponent = internalIterator.next();
}
@Override
public void remove()
{
MarkupContainer.this.remove(currentComponent);
refreshInternalIteratorIfNeeded();
}
private void refreshInternalIteratorIfNeeded()
{
if (modCounter != 0 && expectedModCounter >= modCounter)
return;
if (children == null)
{
internalIterator = Collections.emptyIterator();
}
else if (children instanceof Component)
{
internalIterator = Collections.singleton((Component)children).iterator();
}
else if (children instanceof List)
{
List<Component> childrenList = children();
internalIterator = childrenList.iterator();
}
else
{
Map<String, Component> childrenMap = children();
internalIterator = childrenMap.values().iterator();
}
// since we now have a new iterator, we need to set it to the last known position
currentComponent = findLastExistingChildAlreadyReturned(currentComponent);
expectedModCounter = modCounter;
indexInRemovalsSinceLastUpdate = removals_size();
if (currentComponent != null)
{
// move the new internal iterator to the place of the last processed component
while (internalIterator.hasNext() &&
internalIterator.next() != currentComponent)
// noop
;
}
}
private Component findLastExistingChildAlreadyReturned(Component target)
{
while (true)
{
if (target == null)
return null;
RemovedChild removedChild = null;
for (int i = indexInRemovalsSinceLastUpdate; i < removals_size(); i++)
{
RemovedChild curRemovedChild = removals_get(i);
if (curRemovedChild.removedChild == target ||
curRemovedChild.removedChild == null)
{
removedChild = curRemovedChild;
break;
}
}
if (removedChild == null)
{
return target;
}
else
{
target = removedChild.previousSibling;
}
}
}
};
return new MarkupChildIterator();
}
/**
* Creates an iterator that iterates over children in the order specified by comparator. This
* works on a copy of the children list.
*
* @param comparator
* The comparator
* @return Iterator that iterates over children in the order specified by comparator
*/
public final Iterator<Component> iterator(Comparator<Component> comparator)
{
final List<Component> sorted = copyChildren();
Collections.sort(sorted, comparator);
return sorted.iterator();
}
/**
* Removes a component from the children identified by the {@code component.getId()}
*
* @param component
* Component to remove from this container
* @return {@code this} for chaining
*/
public MarkupContainer remove(final Component component)
{
checkHierarchyChange(component);
Args.notNull(component, "component");
children_remove(component.getId());
removedComponent(component);
return this;
}
/**
* Removes the given component
*
* @param id
* The id of the component to remove
* @return {@code this} for chaining
*/
public MarkupContainer remove(final String id)
{
Args.notNull(id, "id");
final Component component = get(id);
if (component != null)
{
remove(component);
}
else
{
throw new WicketRuntimeException("Unable to find a component with id '" + id +
"' to remove");
}
return this;
}
/**
* Removes all children from this container.
* <p>
* Note: implementation does not call {@link MarkupContainer#remove(Component) } for each
* component.
*
* @return {@code this} for method chaining
*/
public MarkupContainer removeAll()
{
if (children != null)
{
addStateChange();
for (Component child : this)
{
// Do not call remove() because the state change would then be
// recorded twice.
child.internalOnRemove();
child.detach();
child.setParent(null);
}
children = null;
removals_add(null, null);
}
return this;
}
/**
* Renders the entire associated markup for a container such as a Border or Panel. Any leading
* or trailing raw markup in the associated markup is skipped.
*
* @param openTagName
* the tag to render the associated markup for
* @param exceptionMessage
* message that will be used for exceptions
*/
public final void renderAssociatedMarkup(final String openTagName, final String exceptionMessage)
{
// Get associated markup file for the Border or Panel component
final MarkupStream associatedMarkupStream = new MarkupStream(getMarkup(null));
// Get open tag in associated markup of border component
MarkupElement elem = associatedMarkupStream.get();
if ((elem instanceof ComponentTag) == false)
{
associatedMarkupStream.throwMarkupException("Expected the open tag. " +
exceptionMessage);
}
// Check for required open tag name
ComponentTag associatedMarkupOpenTag = (ComponentTag)elem;
if (!(associatedMarkupOpenTag.isOpen() && (associatedMarkupOpenTag instanceof WicketTag)))
{
associatedMarkupStream.throwMarkupException(exceptionMessage);
}
try
{
setIgnoreAttributeModifier(true);
renderComponentTag(associatedMarkupOpenTag);
associatedMarkupStream.next();
String className = null;
final boolean outputClassName = getApplication().getDebugSettings()
.isOutputMarkupContainerClassName();
if (outputClassName)
{
className = Classes.name(getClass());
getResponse().write("<!-- MARKUP FOR ");
getResponse().write(className);
getResponse().write(" BEGIN -->");
}
renderComponentTagBody(associatedMarkupStream, associatedMarkupOpenTag);
if (outputClassName)
{
getResponse().write("<!-- MARKUP FOR ");
getResponse().write(className);
getResponse().write(" END -->");
}
renderClosingComponentTag(associatedMarkupStream, associatedMarkupOpenTag, false);
}
finally
{
setIgnoreAttributeModifier(false);
}
}
/**
* Replaces a child component of this container with another
*
* @param child
* The child
* @throws IllegalArgumentException
* Thrown if there was no child with the same id.
* @return This
*/
public MarkupContainer replace(final Component child)
{
Args.notNull(child, "child");
checkHierarchyChange(child);
if (log.isDebugEnabled())
{
log.debug("Replacing " + child.getId() + " in " + this);
}
if (child.getParent() != this)
{
final Component replaced = children_put(child);
// Look up to make sure it was already in the map
if (replaced == null)
{
throw new WicketRuntimeException(
exceptionMessage("Cannot replace a component which has not been added: id='" +
child.getId() + "', component=" + child));
}
// first remove the component.
removedComponent(replaced);
// The generated markup id remains the same
child.setMarkupId(replaced);
// then add the other one.
addedComponent(child);
}
return this;
}
@Override
public MarkupContainer setDefaultModel(final IModel<?> model)
{
final IModel<?> previous = getModelImpl();
super.setDefaultModel(model);
if (previous instanceof IComponentInheritedModel)
{
visitChildren(new IVisitor<Component, Void>()
{
@Override
public void component(final Component component, final IVisit<Void> visit)
{
IModel<?> compModel = component.getDefaultModel();
if (compModel instanceof IWrapModel)
{
compModel = ((IWrapModel<?>)compModel).getWrappedModel();
}
if (compModel == previous)
{
component.setDefaultModel(null);
}
else if (compModel == model)
{
component.modelChanged();
}
}
});
}
return this;
}
/**
* Get the number of children in this container.
*
* @return Number of children in this container
*/
public int size()
{
return children_size();
}
@Override
public String toString()
{
return toString(false);
}
/**
* @param detailed
* True if a detailed string is desired
* @return String representation of this container
*/
@Override
public String toString(final boolean detailed)
{
final StringBuilder buffer = new StringBuilder();
buffer.append('[').append(Classes.simpleName(this.getClass())).append(' ');
buffer.append(super.toString(detailed));
if (detailed && children_size() != 0)
{
buffer.append(", children = ");
// Loop through child components
boolean first = true;
for (Component child : this)
{
if (first)
{
buffer.append(' ');
first = false;
}
buffer.append(child.toString());
}
}
buffer.append(']');
return buffer.toString();
}
/**
* Traverses all child components of the given class in this container, calling the visitor's
* visit method at each one.
*
* Make sure that if you give a type S that the clazz parameter will only resolve to those
* types. Else a class cast exception will occur.
*
* @param <S>
* The type that goes into the Visitor.component() method.
* @param <R>
* @param clazz
* The class of child to visit
* @param visitor
* The visitor to call back to
* @return The return value from a visitor which halted the traversal, or null if the entire
* traversal occurred
*/
public final <S extends Component, R> R visitChildren(final Class<?> clazz,
final IVisitor<S, R> visitor)
{
return Visits.visitChildren(this, visitor, new ClassVisitFilter(clazz));
}
/**
* Traverses all child components in this container, calling the visitor's visit method at each
* one.
*
* @param <R>
* @param visitor
* The visitor to call back to
* @return The return value from a visitor which halted the traversal, or null if the entire
* traversal occurred
*/
public final <R> R visitChildren(final IVisitor<Component, R> visitor)
{
return Visits.visitChildren(this, visitor);
}
/**
* @param child
* Component being added
*/
private void addedComponent(final Component child)
{
// Check for degenerate case
Args.notNull(child, "child");
MarkupContainer parent = child.getParent();
if (parent != null && parent != this)
{
parent.remove(child);
}
// Set child's parent
child.setParent(this);
final DebugSettings debugSettings = Application.get().getDebugSettings();
if (debugSettings.isLinePreciseReportingOnAddComponentEnabled()
&& debugSettings.getComponentUseCheck())
{
child.setMetaData(ADDED_AT_KEY,
ComponentStrings.toString(child, new MarkupException("added")));
}
Page page = findPage();
// if we have a path to page, dequeue any container children.
if (page != null && child instanceof MarkupContainer)
{
MarkupContainer childContainer = (MarkupContainer)child;
// if we are already dequeueing there is no need to dequeue again
if (!childContainer.getRequestFlag(RFLAG_CONTAINER_DEQUEING))
{
childContainer.dequeue();
}
}
if (page != null)
{
// tell the page a component has been added first, to allow it to initialize
page.componentAdded(child);
// initialize the component
if (page.isInitialized())
{
child.internalInitialize();
}
}
// if the PREPARED_FOR_RENDER flag is set, we have already called
// beforeRender on this component's children. So we need to initialize the newly added one
if (isPreparedForRender())
{
child.beforeRender();
}
}
/**
* THIS METHOD IS NOT PART OF THE PUBLIC API, DO NOT CALL IT
*
* Overrides {@link Component#internalInitialize()} to call {@link Component#fireInitialize()}
* for itself and for all its children.
*
* @see org.apache.wicket.Component#fireInitialize()
*/
@Override
public final void internalInitialize()
{
super.fireInitialize();
visitChildren(new IVisitor<Component, Void>()
{
@Override
public void component(final Component component, final IVisit<Void> visit)
{
component.fireInitialize();
}
});
}
/*
* === Internal management for keeping track of child components ===
*
* A markup container is the base component for containing child objects. It is one of the most
* heavily used components so we should keep it's (memory and CPU) footprint small.
*
* The goals for the internal management of the list of child components are:
*
* - as low big-O complexity as possible, preferrably O(1)
*
* - as low memory consumption as possible (don't use more memory than strictly necessary)
*
* - ensure that iterating through the (list of) children be as consistent as possible
*
* - retain the order of addition in the iteration
*
* These goals are attained by storing the children in a single field that is implemented using:
*
* - a component when there's only one child
*
* - a list of components when there are more than 1 children
*
* - a map of components when the number of children makes looking up children by id more costly
* than an indexed search (see MAPIFY_THRESHOLD)
*
* To ensure that iterating through the list of children keeps working even when children are
* added, replaced and removed without throwing a ConcurrentModificationException a special
* iterator is used. The markup container tracks removals from and additions to the children
* during the request, enabling the iterator to skip over those items and adjust to changing
* internal data structures.
*/
/**
* A type washing accessor method for getting the children without having to cast the field
* explicitly.
*
* @return the children as a T
*/
@SuppressWarnings("unchecked")
private <T> T children()
{
return (T)children;
}
/**
* Gets the child with the given {@code childId}
*
* @param childId
* the component identifier
* @return The child component or {@code null} when no child with the given identifier exists
*/
private Component children_get(final String childId)
{
if (children == null)
{
return null;
}
if (children instanceof Component)
{
Component child = children();
return child.getId().equals(childId) ? child : null;
}
if (children instanceof List)
{
List<Component> kids = children();
for (Component child : kids)
{
if (child.getId().equals(childId))
{
return child;
}
}
return null;
}
Map<String, Component> kids = children();
return kids.get(childId);
}
/**
* Removes the child component identified by {@code childId} from the list of children.
*
* Will change the internal list or map to a single component when the number of children hits
* 1, but not change the internal map to a list when the threshold is reached (the memory was
* already claimed, so there's little to be gained other than wasting CPU cycles for the
* conversion).
*
* @param childId
* the id of the child component to remove
*/
private void children_remove(String childId)
{
if (children instanceof Component)
{
Component oldChild = children();
if (oldChild.getId().equals(childId))
{
children = null;
removals_add(oldChild, null);
}
}
else if (children instanceof List)
{
List<Component> childrenList = children();
Iterator<Component> it = childrenList.iterator();
Component prevChild = null;
while (it.hasNext())
{
Component child = it.next();
if (child.getId().equals(childId))
{
it.remove();
removals_add(child, prevChild);
if (childrenList.size() == 1)
{
children = childrenList.get(0);
}
return;
}
prevChild = child;
}
}
else if (children instanceof LinkedMap)
{
LinkedMap<String, Component> childrenMap = children();
if (childrenMap.containsKey(childId))
{
String prevSiblingId = childrenMap.previousKey(childId);
Component oldChild = childrenMap.remove(childId);
removals_add(oldChild, childrenMap.get(prevSiblingId));
if (childrenMap.size() == 1)
{
children = childrenMap.values().iterator().next();
}
}
}
}
/**
* Gets the number of child components of this markup container.
*
* @return The number of children
*/
private int children_size()
{
if (children == null)
{
return 0;
}
if (children instanceof Component)
{
return 1;
}
if (children instanceof List)
{
List<?> kids = children();
return kids.size();
}
return ((Map<?, ?>)children).size();
}
/**
* Puts the {@code child} component into the list of children of this markup container. If a
* component existed with the same {@code child.getId()} it is replaced and the old component is
* returned.
*
* When a component is replaced, the internal structure of the children is not modified, so we
* don't have to update the internal {@link #modCounter} in those circumstances. When a
* component is added, we do have to increase the {@link #modCounter} to notify iterators of
* this change.
*
* @param child
* The child
* @return Any component that was replaced
*/
private Component children_put(final Component child)
{
if (children == null)
{
children = child;
// it is an addtion, so we need to notify the iterators of this change.
modCounter++;
return null;
}
if (children instanceof Component)
{
/* first see if the child replaces the existing child */
Component oldChild = children();
if (oldChild.getId().equals(child.getId()))
{
children = child;
return oldChild;
}
else
{
/*
* the put doesn't replace the existing child, so we need to increase the children
* storage to a list and add the existing and new child to it
*/
Component originalChild = children();
List<Component> newChildren = new ArrayList<>(INITIAL_CHILD_LIST_CAPACITY);
newChildren.add(originalChild);
newChildren.add(child);
children = newChildren;
// it is an addtion, so we need to notify the iterators of this change.
modCounter++;
return null;
}
}
if (children instanceof List)
{
List<Component> childrenList = children();
// first see if the child replaces an existing child
for (int i = 0; i < childrenList.size(); i++)
{
Component curChild = childrenList.get(i);
if (curChild.getId().equals(child.getId()))
{
return childrenList.set(i, child);
}
}
// it is an addtion, so we need to notify the iterators of this change.
modCounter++;
/*
* If it still fits in the allotted number of items of a List, just add it, otherwise
* change the internal data structure to a Map for speedier lookups.
*/
if (childrenList.size() < MAPIFY_THRESHOLD)
{
childrenList.add(child);
}
else
{
Map<String, Component> newChildren = new LinkedMap<>(MAPIFY_THRESHOLD * 2);
for (Component curChild : childrenList)
{
newChildren.put(curChild.getId(), curChild);
}
newChildren.put(child.getId(), child);
children = newChildren;
}
return null;
}
Map<String, Component> childrenMap = children();
Component oldChild = childrenMap.put(child.getId(), child);
if (oldChild == null)
{
// it is an addtion, so we need to notify the iterators of this change.
modCounter++;
}
return oldChild;
}
/**
* Retrieves the during the request removed children. These are stored in the metadata and
* cleared at the end of the request {@link #onDetach()}
*
* @return the list of removed children, may be {@code null}
*/
private LinkedList<RemovedChild> removals_get()
{
return getMetaData(REMOVALS_KEY);
}
/**
* Sets the during the request removed children. These are stored in the metadata and cleared at
* the end of the request, see {@link #onDetach()}.
*
* @param removals
* the new list of removals
*/
private void removals_set(LinkedList<RemovedChild> removals)
{
setMetaData(REMOVALS_KEY, removals);
}
/**
* Removes the list of removals from the metadata.
*/
private void removals_clear()
{
setMetaData(REMOVALS_KEY, null);
}
/**
* Adds the {@code removedChild} to the list of removals and links it to the
* {@code previousSibling}
*
* @param removedChild
* the child that was removed
* @param prevSibling
* the child that was the previous sibling of the removed child
*/
private void removals_add(Component removedChild, Component prevSibling)
{
modCounter++;
LinkedList<RemovedChild> removals = removals_get();
if (removals == null)
{
removals = new LinkedList<>();
removals_set(removals);
}
removals.add(new RemovedChild(removedChild, prevSibling));
}
/**
* Gets the {@link RemovedChild} from the list of removals at given position.
*
* @param i
* the position
* @return the removed child
*/
private RemovedChild removals_get(int i)
{
return getMetaData(REMOVALS_KEY).get(i);
}
/**
* Gets the number of removals that happened during the request.
*
* @return the number of removals
*/
private int removals_size()
{
LinkedList<RemovedChild> removals = removals_get();
return removals == null ? 0 : removals.size();
}
/**
* @param component
* Component being removed
*/
private void removedComponent(final Component component)
{
// Notify Page that component is being removed
final Page page = component.findPage();
if (page != null)
{
page.componentRemoved(component);
}
component.detach();
component.internalOnRemove();
// Component is removed
component.setParent(null);
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT USE OR OVERWRITE IT.
*
* Renders the next element of markup in the given markup stream.
*
* @param markupStream
* The markup stream
* @return true, if element was rendered as RawMarkup
*/
protected boolean renderNext(final MarkupStream markupStream)
{
// Get the current markup element
final MarkupElement element = markupStream.get();
// If it's a tag like <wicket..> or <span wicket:id="..." >
if ((element instanceof ComponentTag) && !markupStream.atCloseTag())
{
// Get element as tag
final ComponentTag tag = (ComponentTag)element;
// Get component id
final String id = tag.getId();
// Get the component for the id from the given container
Component component = get(id);
if (component == null)
{
component = ComponentResolvers.resolve(this, markupStream, tag, null);
if ((component != null) && (component.getParent() == null))
{
autoAdd(component, markupStream);
}
else if (component != null)
{
component.setMarkup(markupStream.getMarkupFragment());
}
}
// Failed to find it?
if (component != null)
{
component.render();
}
else if (tag.getFlag(ComponentTag.RENDER_RAW))
{
// No component found, but "render as raw markup" flag found
if (canRenderRawTag(tag))
{
getResponse().write(element.toCharSequence());
}
return true;
}
else
{
throwException(markupStream, tag);
}
}
else
{
// Render as raw markup
if (canRenderRawTag(element))
{
getResponse().write(element.toCharSequence());
}
return true;
}
return false;
}
/**
* Says if the given tag can be handled as a raw markup.
*
* @param tag
* the current tag.
* @return true if the tag can be handled as raw markup, false otherwise.
*/
private boolean canRenderRawTag(MarkupElement tag)
{
boolean isWicketTag = tag instanceof WicketTag;
boolean stripTag = isWicketTag ? Application.get().getMarkupSettings().getStripWicketTags() : false;
return !stripTag;
}
/**
* Throws a {@code org.apache.wicket.markup.MarkupException} when the
* component markup is not consistent.
*
* @param markupStream
* the source stream for the component markup.
* @param tag
* the tag that can not be handled.
*/
private void throwException(final MarkupStream markupStream, final ComponentTag tag)
{
final String id = tag.getId();
if (tag instanceof WicketTag)
{
if (((WicketTag)tag).isChildTag())
{
markupStream.throwMarkupException("Found " + tag.toString() +
" but no <wicket:extend>. Container: " + toString());
}
else
{
markupStream.throwMarkupException("Failed to handle: " +
tag.toString() +
". It might be that no resolver has been registered to handle this special tag. " +
" But it also could be that you declared wicket:id=" + id +
" in your markup, but that you either did not add the " +
"component to your page at all, or that the hierarchy does not match. " +
"Container: " + toString());
}
}
List<String> names = findSimilarComponents(id);
// No one was able to handle the component id
StringBuilder msg = new StringBuilder(500);
msg.append("Unable to find component with id '");
msg.append(id);
msg.append("' in ");
msg.append(this.toString());
msg.append("\n\tExpected: '");
msg.append(getPageRelativePath());
msg.append(PATH_SEPARATOR);
msg.append(id);
msg.append("'.\n\tFound with similar names: '");
msg.append(Strings.join("', ", names));
msg.append('\'');
log.error(msg.toString());
markupStream.throwMarkupException(msg.toString());
}
private List<String> findSimilarComponents(final String id)
{
final List<String> names = Generics.newArrayList();
Page page = findPage();
if (page != null)
{
page.visitChildren(new IVisitor<Component, Void>()
{
@Override
public void component(Component component, IVisit<Void> visit)
{
if (Strings.getLevenshteinDistance(id.toLowerCase(), component.getId()
.toLowerCase()) < 3)
{
names.add(component.getPageRelativePath());
}
}
});
}
return names;
}
/**
* Handle the container's body. If your override of this method does not advance the markup
* stream to the close tag for the openTag, a runtime exception will be thrown by the framework.
*
* @param markupStream
* The markup stream
* @param openTag
* The open tag for the body
*/
@Override
public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag)
{
renderComponentTagBody(markupStream, openTag);
}
@Override
protected void onRender()
{
internalRenderComponent();
}
/**
* Renders markup for the body of a ComponentTag from the current position in the given markup
* stream. If the open tag passed in does not require a close tag, nothing happens. Markup is
* rendered until the closing tag for openTag is reached.
*
* @param markupStream
* The markup stream
* @param openTag
* The open tag
*/
private void renderComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag)
{
if ((markupStream != null) && (markupStream.getCurrentIndex() > 0))
{
// If the original tag has been changed from open-close to open-body-close, than we are
// done. Other components, e.g. BorderBody, rely on this method being called.
ComponentTag origOpenTag = (ComponentTag)markupStream.get(markupStream.getCurrentIndex() - 1);
if (origOpenTag.isOpenClose())
{
return;
}
}
// If the open tag requires a close tag
boolean render = openTag.requiresCloseTag();
if (render == false)
{
// Tags like <p> do not require a close tag, but they may have.
render = !openTag.hasNoCloseTag();
}
if (render)
{
renderAll(markupStream, openTag);
}
}
/**
* Loop through the markup in this container
*
* @param markupStream
* @param openTag
*/
protected final void renderAll(final MarkupStream markupStream, final ComponentTag openTag)
{
while (markupStream.hasMore())
{
// In case of Page we need to render the whole file. For all other components just what
// is in between the open and the close tag.
if ((openTag != null) && markupStream.get().closes(openTag))
{
break;
}
// Remember where we are
final int index = markupStream.getCurrentIndex();
// Render the markup element
boolean rawMarkup = renderNext(markupStream);
// Go back to where we were and move the markup stream forward to whatever the next
// element is.
markupStream.setCurrentIndex(index);
if (rawMarkup)
{
markupStream.next();
}
else if (!markupStream.getTag().isClose())
{
markupStream.skipComponent();
}
else
{
throw new WicketRuntimeException("Ups. This should never happen. " +
markupStream.toString());
}
}
}
@Override
void removeChildren()
{
super.removeChildren();
for (Component component : this)
{
component.internalOnRemove();
}
}
@Override
void detachChildren()
{
super.detachChildren();
for (Component component : this)
{
component.detach();
}
}
@Override
void internalMarkRendering(boolean setRenderingFlag)
{
super.internalMarkRendering(setRenderingFlag);
for (Component child : this)
{
child.internalMarkRendering(setRenderingFlag);
}
}
/**
* @return a copy of the children array.
*/
@SuppressWarnings("unchecked")
private List<Component> copyChildren()
{
if (children == null)
{
return Collections.emptyList();
}
else if (children instanceof Component)
{
return Collections.singletonList((Component)children);
}
else if (children instanceof List)
{
return new ArrayList<>((List<Component>)children);
}
else
{
return new ArrayList<>(((Map<String, Component>)children).values());
}
}
@Override
void onBeforeRenderChildren()
{
super.onBeforeRenderChildren();
try
{
// Loop through child components
for (final Component child : this)
{
// Get next child
// Call begin request on the child
// We need to check whether the child's wasn't removed from the
// component in the meanwhile (e.g. from another's child
// onBeforeRender)
if (child.getParent() == this)
{
child.beforeRender();
}
}
}
catch (RuntimeException ex)
{
if (ex instanceof WicketRuntimeException)
{
throw ex;
}
else
{
throw new WicketRuntimeException("Error attaching this container for rendering: " +
this, ex);
}
}
}
@Override
void onEnabledStateChanged()
{
super.onEnabledStateChanged();
visitChildren(new IVisitor<Component, Void>()
{
@Override
public void component(Component component, IVisit<Void> visit)
{
component.clearEnabledInHierarchyCache();
}
});
}
@Override
void onVisibleStateChanged()
{
super.onVisibleStateChanged();
visitChildren(new IVisitor<Component, Void>()
{
@Override
public void component(Component component, IVisit<Void> visit)
{
component.clearVisibleInHierarchyCache();
}
});
}
@Override
protected void onAfterRenderChildren()
{
for (Component child : this)
{
// set RENDERING_FLAG to false for auto-component's children (like Enclosure)
child.markRendering(false);
}
super.onAfterRenderChildren();
}
@Override
protected void onDetach()
{
super.onDetach();
modCounter = 0;
removals_clear();
if (queue != null && !queue.isEmpty() && hasBeenRendered())
{
throw new WicketRuntimeException(
String.format("Detach called on component with id '%s' while it had a non-empty queue: %s",
getId(), queue));
}
}
private transient ComponentQueue queue;
/**
* Queues one or more components to be dequeued later. The advantage of this method over the
* {@link #add(Component...)} method is that the component does not have to be added to its
* direct parent, only to a parent upstream; it will be dequeued into the correct parent using
* the hierarchy defined in the markup. This allows the component hierarchy to be maintained only
* in markup instead of in markup and in java code; affording designers and developers more
* freedom when moving components in markup.
*
* @param components
* the components to queue
* @return {@code this} for method chaining
*/
public MarkupContainer queue(Component... components)
{
if (queue == null)
{
queue = new ComponentQueue();
}
queue.add(components);
Page page = findPage();
if (page != null)
{
dequeue();
}
return this;
}
/**
* @see IQueueRegion#dequeue()
*/
public void dequeue()
{
if (this instanceof IQueueRegion)
{
DequeueContext dequeue = newDequeueContext();
dequeuePreamble(dequeue);
}
else
{
MarkupContainer containerWithQueue = this;
// check if there are any parent containers that have queued components, up till our
// queue region
while (containerWithQueue.isQueueEmpty() &&
!(containerWithQueue instanceof IQueueRegion))
{
containerWithQueue = containerWithQueue.getParent();
if (containerWithQueue == null)
{
// no queued components are available for dequeuing, so we can stop
return;
}
}
// when there are no components to be dequeued, just stop
if (containerWithQueue.isQueueEmpty())
return;
// get the queue region where we are going to dequeue components in
MarkupContainer queueRegion = containerWithQueue;
// the container with queued components could be a queue region, if not, find the region
// to dequeue in
if (!queueRegion.isQueueRegion())
{
queueRegion = (MarkupContainer)queueRegion.findParent(IQueueRegion.class);
}
if (queueRegion != null && !queueRegion.getRequestFlag(RFLAG_CONTAINER_DEQUEING))
{
queueRegion.dequeue();
}
}
}
/**
* @return {@code true} when one or more components are queued
*/
private boolean isQueueEmpty()
{
return queue == null || queue.isEmpty();
}
/**
* @return {@code true} when this markup container is a queue region
*/
private boolean isQueueRegion()
{
return IQueueRegion.class.isInstance(this);
}
/**
* Run preliminary operations before running {@link #dequeue(DequeueContext)}. More in detail it
* throws an exception if the container is already dequeuing, and it also takes care of setting
* flag {@code RFLAG_CONTAINER_DEQUEING} to true before running {@link #dequeue(DequeueContext)}
* and setting it back to false after dequeuing is completed.
*
* @param dequeue
* the dequeue context to use
*/
protected void dequeuePreamble(DequeueContext dequeue)
{
if (getRequestFlag(RFLAG_CONTAINER_DEQUEING))
{
throw new IllegalStateException("This container is already dequeing: " + this);
}
setRequestFlag(RFLAG_CONTAINER_DEQUEING, true);
try
{
if (dequeue == null)
{
return;
}
if (dequeue.peekTag() != null)
{
dequeue(dequeue);
}
}
finally
{
setRequestFlag(RFLAG_CONTAINER_DEQUEING, false);
}
}
/**
* Dequeues components. The default implementation iterates direct children of this container
* found in its markup and tries to find matching
* components in queues filled by a call to {@link #queue(Component...)}. It then delegates the
* dequeueing to these children.
*
*
* Certain components that implement custom markup behaviors (such as repeaters and borders)
* override this method to bring dequeueing in line with their custom markup handling.
*
* @param dequeue
* the dequeue context to use
*/
public void dequeue(DequeueContext dequeue)
{
while (dequeue.isAtOpenOrOpenCloseTag())
{
ComponentTag tag = dequeue.takeTag();
// see if child is already added to parent
Component child = get(tag.getId());
if (child == null)
{
// the container does not yet have a child with this id, see if we can
// dequeue
child = dequeue.findComponentToDequeue(tag);
//if tag has an autocomponent factory let's use it
if (child == null && tag.getAutoComponentFactory() != null)
{
IAutoComponentFactory autoComponentFactory = tag.getAutoComponentFactory();
child = autoComponentFactory.newComponent(this, tag);
}
if (child != null)
{
addDequeuedComponent(child, tag);
}
}
if (tag.isOpen() && !tag.hasNoCloseTag())
{
dequeueChild(child, tag, dequeue);
}
}
}
/**
* Propagates dequeuing to child component.
*
* @param child
* the child component
* @param tag
* the child tag
* @param dequeue
* the dequeue context to use
*/
private void dequeueChild(Component child, ComponentTag tag, DequeueContext dequeue)
{
ChildToDequeueType childType = ChildToDequeueType.fromChild(child);
if (childType == ChildToDequeueType.QUEUE_REGION ||
childType == ChildToDequeueType.BORDER)
{
((IQueueRegion)child).dequeue();
}
if (childType == ChildToDequeueType.MARKUP_CONTAINER)
{
// propagate dequeuing to containers
MarkupContainer childContainer = (MarkupContainer)child;
dequeue.pushContainer(childContainer);
childContainer.dequeue(dequeue);
dequeue.popContainer();
}
if (childType == ChildToDequeueType.NULL ||
childType == ChildToDequeueType.QUEUE_REGION)
{
dequeue.skipToCloseTag();
}
// pull the close tag off
ComponentTag close = dequeue.takeTag();
do
{
if (close != null && close.closes(tag))
{
return;
}
} while ((close = dequeue.takeTag()) != null);
throw new IllegalStateException(String.format("Could not find the closing tag for '%s'", tag));
}
/** @see IQueueRegion#newDequeueContext() */
public DequeueContext newDequeueContext()
{
IMarkupFragment markup = getRegionMarkup();
if (markup == null)
{
return null;
}
return new DequeueContext(markup, this, false);
}
/** @see IQueueRegion#getRegionMarkup() */
public IMarkupFragment getRegionMarkup()
{
return getAssociatedMarkup();
}
/**
* Checks if this container can dequeue a child represented by the specified tag. This method
* should be overridden when containers can dequeue components represented by non-standard tags.
* For example, borders override this method and dequeue their body container when processing
* the body tag.
*
* By default all {@link ComponentTag}s are supported as well as {@link WicketTag}s that return
* a non-null value from {@link WicketTag#getAutoComponentFactory()} method.
*
* @param tag
*/
protected DequeueTagAction canDequeueTag(ComponentTag tag)
{
if (tag instanceof WicketTag)
{
WicketTag wicketTag = (WicketTag)tag;
if (wicketTag.isContainerTag())
{
return DequeueTagAction.DEQUEUE;
}
else if (wicketTag.getAutoComponentFactory() != null)
{
return DequeueTagAction.DEQUEUE;
}
else if (wicketTag.isFragmentTag())
{
return DequeueTagAction.SKIP;
}
else if (wicketTag.isChildTag())
{
return DequeueTagAction.IGNORE;
}
else if (wicketTag.isHeadTag())
{
return DequeueTagAction.SKIP;
}
else if (wicketTag.isLinkTag())
{
return DequeueTagAction.DEQUEUE;
}
else
{
return null; // don't know
}
}
//if is a label tag, ignore it
if (tag.isAutoComponentTag()
&& tag.getId().startsWith(AutoLabelResolver.LABEL_ATTR))
{
return DequeueTagAction.IGNORE;
}
return DequeueTagAction.DEQUEUE;
}
/**
* Queries this container to find a child that can be dequeued that matches the specified tag.
* The default implementation will check if there is a component in the queue that has the same
* id as a tag, but sometimes custom tags can be dequeued and in those situations this method
* should be overridden.
*
* @param tag
* @return
*/
public Component findComponentToDequeue(ComponentTag tag)
{
return queue == null ? null : queue.remove(tag.getId());
}
/**
* Adds a dequeued component to this container. This method should rarely be overridden because
* the common case of simply forwarding the component to
* {@link MarkupContainer#add(Component...))} method should cover most cases. Components that
* implement a custom hierarchy, such as borders, may wish to override it to support edge-case
* non-standard behavior.
*
* @param component
* @param tag
*/
protected void addDequeuedComponent(Component component, ComponentTag tag)
{
add(component);
}
}