/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.wicket.markup; import org.apache.wicket.Component; import org.apache.wicket.util.lang.Args; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.string.Strings; /** * A stream of {@link org.apache.wicket.markup.MarkupElement}s, subclasses of which are * {@link org.apache.wicket.markup.ComponentTag} and {@link org.apache.wicket.markup.RawMarkup}. A * markup stream has a current index in the list of markup elements. The next markup element can be * retrieved and the index advanced by calling next(). If the index hits the end, hasMore() will * return false. * <p> * The current markup element can be accessed with get() and as a ComponentTag with getTag(). * <p> * The stream can be sought to a particular location with setCurrentIndex(). * <p> * Convenience methods also exist to skip component tags (and any potentially nested markup) or raw * markup. * <p> * Several boolean methods of the form at*() return true if the markup stream is positioned at a tag * with a given set of characteristics. * <p> * The resource from which the markup was loaded can be retrieved with getResource(). * * @author Jonathan Locke */ public class MarkupStream { /** Element at currentIndex */ private MarkupElement current; /** Current index in markup stream */ private int currentIndex = 0; /** The markup element list */ private final IMarkupFragment markup; /** * Constructor * * @param markup * List of markup elements */ public MarkupStream(final IMarkupFragment markup) { Args.notNull(markup, "markup"); this.markup = markup; if (markup.size() > 0) { current = get(currentIndex); } } /** * @return True if current markup element is a close tag */ public boolean atCloseTag() { return atTag() && getTag().isClose(); } /** * @return True if current markup element is an openclose tag */ public boolean atOpenCloseTag() { return atTag() && getTag().isOpenClose(); } /** * @param componentId * Required component name attribute * @return True if the current markup element is an openclose tag with the given component name */ public boolean atOpenCloseTag(final String componentId) { return atOpenCloseTag() && componentId.equals(getTag().getId()); } /** * @return True if current markup element is an open tag */ public boolean atOpenTag() { return atTag() && getTag().isOpen(); } /** * @param id * Required component id attribute * @return True if the current markup element is an open tag with the given component name */ public boolean atOpenTag(final String id) { return atOpenTag() && id.equals(getTag().getId()); } /** * @return True if current markup element is a tag */ public boolean atTag() { return current instanceof ComponentTag; } /** * Compare this markup stream with another one * * @param that * The other markup stream * @return True if each MarkupElement in this matches each element in that */ public boolean equalTo(final MarkupStream that) { // While a has more markup elements while (isCurrentIndexInsideTheStream()) { // Get an element from each final MarkupElement thisElement = this.get(); final MarkupElement thatElement = that.get(); // and if the elements are not equal if (thisElement != null && thatElement != null) { if (!thisElement.equalTo(thatElement)) { // fail the comparison return false; } } else { // If one element is null, if (!(thisElement == null && thatElement == null)) { // fail the comparison return false; } } next(); that.next(); } // If we've run out of markup elements in b if (!that.isCurrentIndexInsideTheStream()) { // then the two streams match perfectly return true; } // Stream b had extra elements return false; } /** * True, if associate markup is the same. It will change e.g. if the markup file has been * re-loaded or the locale has been changed. * * @param markupStream * The markup stream to compare with. * @return true, if markup has not changed */ public final boolean equalMarkup(final MarkupStream markupStream) { if (markupStream == null) { return false; } return markup == markupStream.markup; } /** * @return The current markup element */ public MarkupElement get() { return current; } /** * @param index * The index of a markup element * @return The MarkupElement element */ public MarkupElement get(final int index) { return markup.get(index); } /** * Get the component/container's Class which is directly associated with the stream. * * @return The component's class */ public final Class<? extends Component> getContainerClass() { return markup.getMarkupResourceStream().getMarkupClass(); } /** * @return Current index in markup stream */ public int getCurrentIndex() { return currentIndex; } /** * Gets the markup encoding. A markup encoding may be specified in a markup file with an XML * encoding specifier of the form <?xml ... encoding="..." ?>. * * @return The encoding, or null if not found */ public final String getEncoding() { return markup.getMarkupResourceStream().getEncoding(); } /** * @return The resource where this markup stream came from */ public IResourceStream getResource() { return markup.getMarkupResourceStream().getResource(); } /** * @return The current markup element as a markup tag */ public ComponentTag getTag() { if (current instanceof ComponentTag) { return (ComponentTag)current; } throwMarkupException("Tag expected"); return null; } /** * Get the wicket namespace valid for this specific markup * * @return wicket namespace */ public final String getWicketNamespace() { return markup.getMarkupResourceStream().getWicketNamespace(); } /** * @return True if this markup stream is moved to a MarkupElement element */ public boolean isCurrentIndexInsideTheStream() { return currentIndex < markup.size(); } /** * @return True if this markup stream has more MarkupElement elements */ public boolean hasMore() { return currentIndex < (markup.size() - 1); } /** * * @return true, if underlying markup has been merged (inheritance) */ public final boolean isMergedMarkup() { return markup instanceof MergedMarkup; } /** * Note: * * @return The next markup element in the stream */ public MarkupElement next() { if (++currentIndex < markup.size()) { return current = get(currentIndex); } return null; } /** * Note: * * @return The next markup element in the stream */ public MarkupElement nextOpenTag() { while (next() != null) { MarkupElement elem = get(); if (elem instanceof ComponentTag) { ComponentTag tag = (ComponentTag)elem; if (tag.isOpen() || tag.isOpenClose()) { return current = get(currentIndex); } } } return null; } /** * @param currentIndex * New current index in the stream * @return this */ public MarkupStream setCurrentIndex(final int currentIndex) { current = get(currentIndex); this.currentIndex = currentIndex; return this; } /** * Skips this component and all nested components */ public final void skipComponent() { // Get start tag final ComponentTag startTag = getTag(); if (startTag.isOpen()) { // With HTML not all tags require a close tag which // must have been detected by the HtmlHandler earlier on. if (startTag.hasNoCloseTag() == false) { // Skip <tag> next(); // Skip nested components skipToMatchingCloseTag(startTag); } // Skip </tag> next(); } else if (startTag.isOpenClose()) { // Skip <tag/> next(); } else { // We were something other than <tag> or <tag/> throwMarkupException("Skip component called on bad markup element " + startTag); } } /** * Skips any raw markup at the current position */ public void skipRawMarkup() { while (true) { if (current instanceof RawMarkup) { if (next() != null) { continue; } } else if ((current instanceof ComponentTag) && !(current instanceof WicketTag)) { ComponentTag tag = (ComponentTag)current; if (tag.isAutoComponentTag()) { if (next() != null) { continue; } } else if (tag.isClose() && tag.getOpenTag().isAutoComponentTag()) { if (next() != null) { continue; } } } break; } } /** * Skip until an element of type 'clazz' is found * * @param clazz * @return true if found */ public boolean skipUntil(final Class<? extends MarkupElement> clazz) { while (isCurrentIndexInsideTheStream()) { if (clazz.isInstance(current)) { return true; } next(); } return false; } /** * Skips any markup at the current position until the wicket tag name is found. * * @param wicketTagName * wicket tag name to seek */ public void skipUntil(final String wicketTagName) { while (true) { if ((current instanceof WicketTag) && ((WicketTag)current).getName().equals(wicketTagName)) { return; } // go on until we reach the end if (next() == null) { return; } } } /** * Renders markup until a closing tag for openTag is reached. * * @param openTag * The open tag */ public void skipToMatchingCloseTag(final ComponentTag openTag) { // Loop through the markup in this container while (isCurrentIndexInsideTheStream()) { // If the current markup tag closes the openTag if (get().closes(openTag)) { // Done! return; } // Skip element next(); } throwMarkupException("Expected close tag for " + openTag); } /** * @return A markup fragment starting at the current position */ public final IMarkupFragment getMarkupFragment() { return new MarkupFragment(markup, currentIndex); } /** * Gets the attribute with 'name' for the tag at the current position * * @param name * @param withWicketNamespace * @return null, if not found */ public final String getTagAttribute(final String name, final boolean withWicketNamespace) { String attr = (withWicketNamespace ? attr = getWicketNamespace() + ":" + name : name); return getTag().getAttributes().getString(attr); } /** * Sometime its necessary to get the previous markup element versus the current one. * * @return The previous element (currentIndex - 1) */ public final ComponentTag getPreviousTag() { MarkupElement elem = get(currentIndex - 1); if ((elem instanceof ComponentTag) == false) { throwMarkupException("Tag expected"); } return (ComponentTag)elem; } /** * Throws a new markup exception * * @param message * The exception message * @throws MarkupException */ public void throwMarkupException(final String message) { throw new MarkupException(this, message); } /** * @return An HTML string highlighting the current position in the markup stream */ public String toHtmlDebugString() { final StringBuilder buffer = new StringBuilder(); for (int i = 0; i < markup.size(); i++) { if (i == currentIndex) { buffer.append("<font color = \"red\">"); } final MarkupElement element = markup.get(i); buffer.append(Strings.escapeMarkup(element.toString(), true).toString()); if (i == currentIndex) { buffer.append("</font>"); } } return buffer.toString(); } /** * @return String representation of markup stream */ @Override public String toString() { return "[markup = " + String.valueOf(markup) + ", index = " + currentIndex + ", current = " + ((current == null) ? "null" : current.toUserDebugString()) + "]"; } }