All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.wicket.MarkupContainer Maven / Gradle / Ivy

Go to download

Wicket is a Java web application framework that takes simplicity, separation of concerns and ease of development to a whole new level. Wicket pages can be mocked up, previewed and later revised using standard WYSIWYG HTML design tools. Dynamic content processing and form handling is all handled in Java code using a first-class component model backed by POJO data beans that can easily be persisted using your favorite technology.

There is a newer version: 10.2.0
Show newest version
/*
 * 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.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

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.
 * 
    *
  • Children - 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.
  • * *
  • Markup Rendering - 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.
  • *
*

* 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. *

* 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. *

* 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. *

* 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 { 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> REMOVALS_KEY = new MetaDataKey<>() { 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. *

* 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. *

* 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 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 { private int indexInRemovalsSinceLastUpdate; private int expectedModCounter = -1; private Component currentComponent = null; private Iterator 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 (expectedModCounter >= modCounter) { // no new modifications return; } if (children == null) { internalIterator = Collections.emptyIterator(); } else if (children instanceof Component) { internalIterator = Collections.singleton((Component)children).iterator(); } else if (children instanceof List) { List childrenList = children(); internalIterator = childrenList.iterator(); } else { Map 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; 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 current) { if (current == null) { indexInRemovalsSinceLastUpdate = 0; } else { LinkedList removals = removals_get(); if (removals != null) { check_removed: while (current != null) { for (int i = indexInRemovalsSinceLastUpdate; i < removals.size(); i++) { RemovedChild removal = removals.get(i); if (removal.removedChild == current || removal.removedChild == null) { current = removal.previousSibling; // current was removed, use its sibling instead continue check_removed; } } // current wasn't removed, keep it break; } indexInRemovalsSinceLastUpdate = removals.size(); } } return current; } }; 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 iterator(Comparator comparator) { final List 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. *

* 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 * ignored * @deprecated * Use {@link #renderAssociatedMarkup(String)}. The {@code exceptionMessage} * parameter is ignored. */ @Deprecated(since = "9.10.0", forRemoval = true) public final void renderAssociatedMarkup(final String openTagName, final String exceptionMessage) { renderAssociatedMarkup(openTagName); } /** * 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 */ public final void renderAssociatedMarkup(final String openTagName) { // 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. Markup for a " + openTagName + " component must begin a tag like ''"); } // Check for required open tag name ComponentTag associatedMarkupOpenTag = (ComponentTag)elem; if (!(associatedMarkupOpenTag.isOpen() && (associatedMarkupOpenTag instanceof WicketTag))) { associatedMarkupStream.throwMarkupException("Markup for a " + openTagName + " component must begin a tag like ''"); } try { setIgnoreAttributeModifier(true); renderComponentTag(associatedMarkupOpenTag); associatedMarkupStream.next(); String className = null; final boolean outputClassName = getApplication().getDebugSettings() .isOutputMarkupContainerClassName(); if (outputClassName) { className = Classes.name(getClass()); getResponse().write(""); } renderComponentTagBody(associatedMarkupStream, associatedMarkupOpenTag); if (outputClassName) { getResponse().write(""); } 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() { @Override public void component(final Component component, final IVisit 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 * The type that goes into the Visitor.component() method. * @param * @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 R visitChildren(final Class clazz, final IVisitor 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 * @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 visitChildren(final IVisitor 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 (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() { @Override public void component(final Component component, final IVisit 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 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 kids = children(); for (Component child : kids) { if (child.getId().equals(childId)) { return child; } } return null; } Map 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 childrenList = children(); Iterator 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 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 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 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 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 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 removals_get() { return getRequestFlag(RFLAG_CONTAINER_HAS_REMOVALS) ? getMetaData(REMOVALS_KEY) : null; } /** * 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 removals) { setRequestFlag(RFLAG_CONTAINER_HAS_REMOVALS, removals != null); setMetaData(REMOVALS_KEY, removals); } /** * Removes the list of removals from the metadata. */ private void removals_clear() { if (getRequestFlag(RFLAG_CONTAINER_HAS_REMOVALS)) { removals_set(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 removals = removals_get(); if (removals == null) { removals = new LinkedList<>(); removals_set(removals); } removals.add(new RemovedChild(removedChild, prevSibling)); } /** * @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 or if ((element instanceof ComponentTag) && !markupStream.atCloseTag()) { // Get element as tag final ComponentTag tag = (ComponentTag)element; if (tag instanceof WicketTag && ((WicketTag)tag).isFragmentTag()){ return false; } // 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 . 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 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 findSimilarComponents(final String id) { final List names = Generics.newArrayList(); Page page = findPage(); if (page != null) { page.visitChildren(new IVisitor() { @Override public void component(Component component, IVisit visit) { if (Strings.getLevenshteinDistance(id.toLowerCase(Locale.ROOT), component.getId() .toLowerCase(Locale.ROOT)) < 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

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.isCurrentIndexInsideTheStream()) { // 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(); } } /** * * @see org.apache.wicket.Component#internalMarkRendering(boolean) */ @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 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)children); } else { return new ArrayList<>(((Map)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() { @Override public void component(Component component, IVisit visit) { component.clearEnabledInHierarchyCache(); } }); } @Override void onVisibleStateChanged() { super.onVisibleStateChanged(); visitChildren(new IVisitor() { @Override public void component(Component component, IVisit visit) { component.clearVisibleInHierarchyCache(); } }); } @Override protected void onDetach() { super.onDetach(); modCounter++; 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 queueRegion = (MarkupContainer)findParent(IQueueRegion.class); if (queueRegion == null) { return; } MarkupContainer anchestor = this; boolean hasQueuedChildren = !isQueueEmpty(); while (!hasQueuedChildren && anchestor != queueRegion) { anchestor = anchestor.getParent(); hasQueuedChildren = !anchestor.isQueueEmpty(); } if (hasQueuedChildren && !queueRegion.getRequestFlag(RFLAG_CONTAINER_DEQUEING)) { queueRegion.dequeue(); } } } @Override protected void onInitialize() { super.onInitialize(); 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 = findChildComponent(tag); 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); } } } /** * Search the child component for the given tag. * * @param tag * the component tag * @return the child component for the given tag or null if no child can not be found. */ protected Component findChildComponent(ComponentTag tag) { return get(tag.getId()); } /** * 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.BORDER) { Border childContainer = (Border)child; // propagate dequeuing to border's body MarkupContainer body = childContainer.getBodyContainer(); dequeueChildrenContainer(dequeue, body); } if (childType == ChildToDequeueType.MARKUP_CONTAINER) { // propagate dequeuing to containers MarkupContainer childContainer = (MarkupContainer)child; dequeueChildrenContainer(dequeue, childContainer); } 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)); } private void dequeueChildrenContainer(DequeueContext dequeue, MarkupContainer child) { dequeue.pushContainer(child); child.dequeue(dequeue); dequeue.popContainer(); } /** @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); } /** * Returns a sequential {@code Stream} with the direct children of this markup container as its * source. This stream doesn't traverse the component tree. * * @return a sequential {@code Stream} over the direct children of this markup container * @since 8.0 */ public Stream stream() { return StreamSupport.stream(spliterator(), false); } /** * Returns a sequential {@code Stream} with the all children of this markup container as its * source. This stream does traverse the component tree. * * @return a sequential {@code Stream} over the all children of this markup container * @since 8.0 */ @SuppressWarnings("unchecked") public Stream streamChildren() { class ChildrenIterator implements Iterator { private Iterator currentIterator; private Deque> iteratorStack = new ArrayDeque<>(); private ChildrenIterator(Iterator iterator) { currentIterator = iterator; } @Override public boolean hasNext() { while (!currentIterator.hasNext() && !iteratorStack.isEmpty()) { currentIterator = iteratorStack.pop(); } return currentIterator.hasNext(); } @Override public C next() { C child = currentIterator.next(); if (child instanceof Iterable) { iteratorStack.push(currentIterator); currentIterator = ((Iterable)child).iterator(); } return child; } } return StreamSupport.stream( Spliterators.spliteratorUnknownSize(new ChildrenIterator<>(iterator()), 0), false); } }