blob: 96829a90c12bec296a7dc8f572dae76d389291d9 [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.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.apache.wicket.markup.ComponentTag;
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.internal.InlineEnclosure;
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.IDebugSettings;
import org.apache.wicket.util.iterator.ComponentHierarchyIterator;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Generics;
import org.apache.wicket.util.string.ComponentStrings;
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 add() method, and they can be looked
* up using a dotted 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
* 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 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
* getAssociatedMarkupStream() to load the markup associated with the page. Since Page is at the top
* of the container hierarchy, it is guaranteed that 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;
/** Log for reporting. */
private static final Logger log = LoggerFactory.getLogger(MarkupContainer.class);
/** List of children or single child */
private Object children;
/**
* @see org.apache.wicket.Component#Component(String)
*/
public MarkupContainer(final String id)
{
this(id, null);
}
/**
* @see org.apache.wicket.Component#Component(String, IModel)
*/
public MarkupContainer(final String id, IModel<?> model)
{
super(id, model);
}
/**
* Adds a child component to this container.
*
* @param childs
* The child(s)
* @throws IllegalArgumentException
* Thrown if a child with the same id is replaced by the add operation.
* @return This
*/
public MarkupContainer add(final Component... childs)
{
for (Component child : childs)
{
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 " +
this.getClass().getSimpleName() + "(\"" + 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 to map
addedComponent(child);
if (put(child) != null)
{
throw new IllegalArgumentException(exceptionMessage("A child with id '" +
child.getId() + "' already exists"));
}
}
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 childs
* The child(s) to be added or replaced
* @return This
*/
public MarkupContainer addOrReplace(final Component... childs)
{
for (Component child : childs)
{
if (child == null)
{
throw new IllegalArgumentException("argument child must be not null");
}
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)
{
if (component == null)
{
throw new IllegalArgumentException("argument component may not be null");
}
// 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.
int index = children_indexOf(component);
if (index >= 0)
{
children_remove(index);
}
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)
{
if (component == null)
{
throw new IllegalArgumentException("argument component may not be null");
}
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.
*
* @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 childs markup
*
* @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 childs 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.
*
* @return The type of associated markup for this component (for example, "html", "wml" or
* "vxml"). 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). 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
addedComponent(child);
put(child);
}
/**
* @return Iterator that iterates through children in the order they were added
*/
public Iterator<Component> iterator()
{
return new Iterator<Component>()
{
int index = 0;
public boolean hasNext()
{
return index < children_size();
}
public Component next()
{
return children_get(index++);
}
public void remove()
{
final Component removed = children_remove(--index);
checkHierarchyChange(removed);
removedComponent(removed);
}
};
}
/**
* @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;
if (children == null)
{
sorted = Collections.emptyList();
}
else
{
if (children instanceof Component)
{
sorted = new ArrayList<Component>(1);
sorted.add((Component)children);
}
else
{
int size = children_size();
sorted = new ArrayList<Component>(size);
for (int i = 0; i < size; i++)
{
sorted.add(children_get(i));
}
}
}
Collections.sort(sorted, comparator);
return sorted.iterator();
}
/**
* @param component
* Component to remove from this container
* @return {@code this} for chaining
*/
public MarkupContainer remove(final Component component)
{
checkHierarchyChange(component);
if (component == null)
{
throw new IllegalArgumentException("argument component may not be null");
}
children_remove(component);
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)
{
if (id == null)
{
throw new IllegalArgumentException("argument id may not be null");
}
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();
// Loop through child components
int size = children_size();
for (int i = 0; i < size; i++)
{
Object childObject = children_get(i, false);
if (childObject instanceof Component)
{
// Get next child
final Component child = (Component)childObject;
// Do not call remove() because the state change would than be
// recorded twice.
child.internalOnRemove();
child.detach();
child.setParent(null);
}
}
children = 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 != null) && 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)
{
Class<?> klass = getClass();
while (klass.isAnonymousClass())
{
klass = klass.getSuperclass();
}
className = klass.getName();
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)
{
checkHierarchyChange(child);
if (child == null)
{
throw new IllegalArgumentException("argument child must be not null");
}
if (log.isDebugEnabled())
{
log.debug("Replacing " + child.getId() + " in " + this);
}
if (child.getParent() != this)
{
// Add to map
final Component replaced = 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;
}
/**
* @see org.apache.wicket.Component#setDefaultModel(org.apache.wicket.model.IModel)
*/
@Override
public MarkupContainer setDefaultModel(final IModel<?> model)
{
final IModel<?> previous = getModelImpl();
super.setDefaultModel(model);
if (previous instanceof IComponentInheritedModel)
{
visitChildren(new IVisitor<Component, Void>()
{
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();
}
/**
* @see org.apache.wicket.Component#toString()
*/
@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(this.getClass().getSimpleName()).append(' ');
buffer.append(super.toString(detailed));
if (detailed && children_size() != 0)
{
buffer.append(", children = ");
// Loop through child components
final int size = children_size();
for (int i = 0; i < size; i++)
{
// Get next child
final Component child = children_get(i);
if (i != 0)
{
buffer.append(' ');
}
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, or null to visit all children
* @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);
}
/**
* @return A iterator which iterators over all children and grand-children the Component
*/
public final ComponentHierarchyIterator visitChildren()
{
return new ComponentHierarchyIterator(this);
}
/**
* @param clazz
* Filter condition
* @return A iterator which iterators over all children and grand-children the Component,
* returning only components which implement (instanceof) the provided clazz.
*/
public final ComponentHierarchyIterator visitChildren(final Class<?> clazz)
{
return new ComponentHierarchyIterator(this).filterByClass(clazz);
}
/**
* @param child
* Component being added
*/
private final void addedComponent(final Component child)
{
// Check for degenerate case
Args.notNull(child, "child");
MarkupContainer parent = child.getParent();
if (parent != null)
{
parent.remove(child);
}
// Set child's parent
child.setParent(this);
final IDebugSettings debugSettings = Application.get().getDebugSettings();
if (debugSettings.isLinePreciseReportingOnAddComponentEnabled())
{
child.setMetaData(ADDED_AT_KEY,
ComponentStrings.toString(child, new MarkupException("added")));
}
final Page page = findPage();
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>()
{
public void component(final Component component, final IVisit<Void> visit)
{
component.fireInitialize();
}
});
}
/**
* @param child
* Child to add
*/
private final void children_add(final Component child)
{
if (children == null)
{
children = child;
}
else
{
if (!(children instanceof ChildList))
{
// Save new children
children = new ChildList(children);
}
((ChildList)children).add(child);
}
}
/**
* Returns child component at the specified index
*
* @param index
* @throws ArrayIndexOutOfBoundsException
* @return child component at the specified index
*/
public final Component get(int index)
{
return children_get(index);
}
/**
*
* @param index
* @return The child component
*/
private final Component children_get(int index)
{
return (Component)children_get(index, true);
}
/**
* If the given object is a {@link ComponentSourceEntry} instance and <code>reconstruct</code>
* is true, it reconstructs the component and returns it. Otherwise it just returns the object
* passed as parameter
*
* @param object
* @param reconstruct
* @param parent
* @param index
* @return The object directly or the reconstructed component
*/
private final Object postprocess(Object object, boolean reconstruct, MarkupContainer parent,
int index)
{
if (reconstruct && object instanceof ComponentSourceEntry)
{
object = ((ComponentSourceEntry)object).reconstruct(parent, index);
}
return object;
}
/**
*
* @param index
* @param reconstruct
* @return the child component
*/
private final Object children_get(int index, boolean reconstruct)
{
Object component = null;
if (children != null)
{
if (children instanceof Object[] == false && children instanceof ChildList == false)
{
if (index != 0)
{
throw new ArrayIndexOutOfBoundsException("index " + index +
" is greater then 0");
}
component = postprocess(children, reconstruct, this, 0);
if (children != component)
{
children = component;
}
}
else
{
Object[] children;
if (this.children instanceof ChildList)
{
// we have a list
children = ((ChildList)this.children).childs;
}
else
{
// we have a object array
children = (Object[])this.children;
}
component = postprocess(children[index], reconstruct, this, index);
if (children[index] != component)
{
children[index] = component;
}
}
}
return component;
}
/**
* Returns the wicket:id of the given object, that can be either a {@link Component} or a
* {@link ComponentSourceEntry}
*
* @param object
* @return The id of the object (object can be component or componentsourcentry)
*/
private final String getId(Object object)
{
if (object instanceof Component)
{
return ((Component)object).getId();
}
else if (object instanceof ComponentSourceEntry)
{
return ((ComponentSourceEntry)object).id;
}
else
{
throw new IllegalArgumentException("Unknown type of object " + object);
}
}
/**
*
* @param id
* @return The child component
*/
private final Component children_get(final String id)
{
if (children == null)
{
return null;
}
Component component = null;
if ((children instanceof Object[] == false) && (children instanceof List == false))
{
if (getId(children).equals(id))
{
component = (Component)postprocess(children, true, this, 0);
if (children != component)
{
children = component;
}
}
}
else
{
Object[] children;
int size = 0;
if (this.children instanceof ChildList)
{
children = ((ChildList)this.children).childs;
size = ((ChildList)this.children).size;
}
else
{
children = (Object[])this.children;
size = children.length;
}
for (int i = 0; i < size; i++)
{
if (getId(children[i]).equals(id))
{
component = (Component)postprocess(children[i], true, this, i);
if (children[i] != component)
{
children[i] = component;
}
break;
}
}
}
return component;
}
/**
*
* @param child
* @return The index of the given child component
*/
private final int children_indexOf(Component child)
{
if (children == null)
{
return -1;
}
if (children instanceof Object[] == false && children instanceof ChildList == false)
{
if (getId(children).equals(child.getId()))
{
return 0;
}
}
else
{
int size = 0;
Object[] children;
if (this.children instanceof Object[])
{
children = (Object[])this.children;
size = children.length;
}
else
{
children = ((ChildList)this.children).childs;
size = ((ChildList)this.children).size;
}
for (int i = 0; i < size; i++)
{
if (getId(children[i]).equals(child.getId()))
{
return i;
}
}
}
return -1;
}
/**
*
* @param component
* @return The component that is removed.
*/
private final Component children_remove(Component component)
{
int index = children_indexOf(component);
if (index != -1)
{
return children_remove(index);
}
return null;
}
/**
*
* @param index
* @return The component that is removed
*/
private final Component children_remove(int index)
{
if (children == null)
{
return null;
}
if (children instanceof Component || children instanceof ComponentSourceEntry)
{
if (index == 0)
{
final Component removed = (Component)postprocess(children, true, null, -1);
children = null;
return removed;
}
else
{
throw new IndexOutOfBoundsException();
}
}
else
{
if (children instanceof Object[])
{
Object[] c = ((Object[])children);
final Object removed = c[index];
if (c.length == 2)
{
if (index == 0)
{
children = c[1];
}
else if (index == 1)
{
children = c[0];
}
else
{
throw new IndexOutOfBoundsException();
}
return (Component)postprocess(removed, true, null, -1);
}
children = new ChildList(children);
}
ChildList lst = (ChildList)children;
Object removed = lst.remove(index);
if (lst.size == 1)
{
children = lst.get(0);
}
return (Component)postprocess(removed, true, null, -1);
}
}
/**
*
* @param index
* @param child
* @param reconstruct
* @return The replaced child
*/
private final Object children_set(int index, Object child, boolean reconstruct)
{
Object replaced;
if (index >= 0 && index < children_size())
{
if (children instanceof Component || children instanceof ComponentSourceEntry)
{
replaced = children;
children = child;
}
else
{
if (children instanceof ChildList)
{
replaced = ((ChildList)children).set(index, child);
}
else
{
final Object[] children = (Object[])this.children;
replaced = children[index];
children[index] = child;
}
}
}
else
{
throw new IndexOutOfBoundsException();
}
return postprocess(replaced, reconstruct, null, -1);
}
/**
*
* @param index
* @param child
* @return The component that is replaced
*/
private final Component children_set(int index, Component child)
{
return (Component)children_set(index, child, true);
}
/**
*
* @return The size of the children
*/
private final int children_size()
{
if (children == null)
{
return 0;
}
else
{
if (children instanceof Component || children instanceof ComponentSourceEntry)
{
return 1;
}
else if (children instanceof ChildList)
{
return ((ChildList)children).size;
}
return ((Object[])children).length;
}
}
/**
* Ensure that there is space in childForId map for a new entry before adding it.
*
* @param child
* The child to put into the map
* @return Any component that was replaced
*/
private final Component put(final Component child)
{
int index = children_indexOf(child);
if (index == -1)
{
children_add(child);
return null;
}
else
{
return children_set(index, child);
}
}
/**
* @param component
* Component being removed
*/
private final 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 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 final 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
getResponse().write(element.toCharSequence());
return true;
}
else
{
if (tag instanceof WicketTag)
{
if (((WicketTag)tag).isChildTag())
{
markupStream.throwMarkupException("Found " + tag.toString() +
" but no <wicket:extend>");
}
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 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.");
}
}
List<String> names = findSimilarComponents(id);
// No one was able to handle the component id
StringBuffer msg = new StringBuffer(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(".");
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());
}
}
else
{
// Render as raw markup
getResponse().write(element.toCharSequence());
return true;
}
return false;
}
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>()
{
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);
}
/**
* @see org.apache.wicket.Component#onRender()
*/
@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 final 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 == true)
{
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);
MarkupElement elem = markupStream.get();
if (rawMarkup)
{
markupStream.next();
}
else if (!markupStream.getTag().isClose())
{
markupStream.skipComponent();
}
else
{
throw new WicketRuntimeException("Ups. This should never happen. " +
markupStream.toString());
}
}
}
/**
*
*/
private static class ComponentSourceEntry extends org.apache.wicket.ComponentSourceEntry
{
private ComponentSourceEntry(MarkupContainer container, Component component,
IComponentSource componentSource)
{
super(container, component, componentSource);
}
private static final long serialVersionUID = 1L;
@Override
protected void setChild(MarkupContainer parent, int index, Component child)
{
parent.children_set(index, child, false);
}
}
/**
* @see org.apache.wicket.Component#removeChildren()
*/
@Override
void removeChildren()
{
super.removeChildren();
for (int i = children_size(); i-- > 0;)
{
Object child = children_get(i, false);
if (child instanceof Component)
{
Component component = (Component)child;
component.internalOnRemove();
}
}
}
@Override
void detachChildren()
{
super.detachChildren();
for (int i = children_size(); i-- > 0;)
{
Object child = children_get(i, false);
if (child instanceof Component)
{
Component component = (Component)child;
component.detach();
// We need to keep InlineEnclosures for Ajax request handling.
// TODO this is really ugly. Feature request for 1.5: change auto-component that
// they don't need to be removed anymore.
if (component.isAuto() && !(component instanceof InlineEnclosure))
{
children_remove(i);
}
}
}
if (children instanceof ChildList)
{
ChildList lst = (ChildList)children;
Object[] tmp = new Object[lst.size];
System.arraycopy(lst.childs, 0, tmp, 0, lst.size);
children = tmp;
}
}
/**
*
* @see org.apache.wicket.Component#internalMarkRendering()
*/
@Override
void internalMarkRendering(boolean setRenderingFlag)
{
super.internalMarkRendering(setRenderingFlag);
final int size = children_size();
for (int i = 0; i < size; i++)
{
final Component child = children_get(i);
child.internalMarkRendering(setRenderingFlag);
}
}
/**
* @return a copy of the children array.
*/
private Component[] copyChildren()
{
int size = children_size();
Component result[] = new Component[size];
for (int i = 0; i < size; ++i)
{
result[i] = children_get(i);
}
return result;
}
/**
*
* @see org.apache.wicket.Component#onBeforeRenderChildren()
*/
@Override
void onBeforeRenderChildren()
{
super.onBeforeRenderChildren();
// We need to copy the children list because the children components can
// modify the hierarchy in their onBeforeRender.
Component[] children = copyChildren();
try
{
// Loop through child components
for (final Component child : children)
{
// 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>()
{
public void component(Component component, IVisit<Void> visit)
{
component.clearEnabledInHierarchyCache();
}
});
}
@Override
void onVisibleStateChanged()
{
super.onVisibleStateChanged();
visitChildren(new IVisitor<Component, Void>()
{
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();
}
/**
* @return True if this markup container has associated markup
* @deprecated Use #getAssociatedMarkup() != null instead
*/
@Deprecated
public boolean hasAssociatedMarkup()
{
return getAssociatedMarkup() != null;
}
/**
*
*/
private static class ChildList extends AbstractList<Object> implements IClusterable
{
private static final long serialVersionUID = -7861580911447631127L;
private int size;
private Object[] childs;
/**
* Construct.
*
* @param children
*/
public ChildList(Object children)
{
if (children instanceof Object[])
{
childs = (Object[])children;
size = childs.length;
}
else
{
childs = new Object[3];
add(children);
}
}
@Override
public Object get(int index)
{
return childs[index];
}
@Override
public int size()
{
return size;
}
@Override
public boolean add(Object o)
{
ensureCapacity(size + 1);
childs[size++] = o;
return true;
}
@Override
public void add(int index, Object element)
{
if (index > size || index < 0)
{
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
ensureCapacity(size + 1);
System.arraycopy(childs, index, childs, index + 1, size - index);
childs[index] = element;
size++;
}
@Override
public Object set(int index, Object element)
{
if (index >= size)
{
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
Object oldValue = childs[index];
childs[index] = element;
return oldValue;
}
@Override
public Object remove(int index)
{
if (index >= size)
{
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
Object oldValue = childs[index];
int numMoved = size - index - 1;
if (numMoved > 0)
{
System.arraycopy(childs, index + 1, childs, index, numMoved);
}
childs[--size] = null; // Let gc do its work
return oldValue;
}
/**
* @param minCapacity
*/
public void ensureCapacity(int minCapacity)
{
int oldCapacity = childs.length;
if (minCapacity > oldCapacity)
{
Object oldData[] = childs;
int newCapacity = oldCapacity * 2;
if (newCapacity < minCapacity)
{
newCapacity = minCapacity;
}
childs = new Object[newCapacity];
System.arraycopy(oldData, 0, childs, 0, size);
}
}
}
/**
* Swaps position of children. This method is particularly useful for adjusting positions of
* repeater's items without rebuilding the component hierarchy
*
* @param idx1
* index of first component to be swapped
* @param idx2
* index of second component to be swapped
*/
public final void swap(int idx1, int idx2)
{
int size = children_size();
if (idx1 < 0 || idx1 >= size)
{
throw new IndexOutOfBoundsException("Argument idx is out of bounds: " + idx1 + "<>[0," +
size + ")");
}
if (idx2 < 0 || idx2 >= size)
{
throw new IndexOutOfBoundsException("Argument idx is out of bounds: " + idx2 + "<>[0," +
size + ")");
}
if (idx1 == idx2)
{
return;
}
if (children instanceof Object[])
{
final Object[] array = (Object[])children;
Object tmp = array[idx1];
array[idx1] = array[idx2];
array[idx2] = tmp;
}
else
{
ChildList list = (ChildList)children;
Object tmp = list.childs[idx1];
list.childs[idx1] = list.childs[idx2];
list.childs[idx2] = tmp;
}
}
/**
* @see org.apache.wicket.Component#onMarkupAttached()
*/
@Override
protected void onMarkupAttached()
{
super.onMarkupAttached();
// createAndAddComponentsForWicketTags();
}
/**
* Automatically create components for <wicket:xxx> tag.
*/
private void createAndAddComponentsForWicketTags()
{
// Markup must be available
IMarkupFragment markup = getMarkup();
if ((markup != null) && (markup.size() > 1))
{
MarkupStream stream = new MarkupStream(markup);
// Skip the first component tag which already belongs to 'this' container
if (stream.skipUntil(ComponentTag.class))
{
stream.next();
}
// Search for <wicket:xxx> in the remaining markup and try to resolve the component
while (stream.skipUntil(ComponentTag.class))
{
ComponentTag tag = stream.getTag();
if (tag.isOpen() || tag.isOpenClose())
{
if (tag instanceof WicketTag)
{
Component component = ComponentResolvers.resolve(this, stream, tag, null);
if ((component != null) && (component.getParent() == null))
{
if (component.getId().equals(tag.getId()) == false)
{
// make sure we are able to get() the component during rendering
tag.setId(component.getId());
tag.setModified(true);
}
add(component);
}
}
if (tag.isOpen())
{
stream.skipToMatchingCloseTag(tag);
}
}
stream.next();
}
}
}
}