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