blob: d938eded03e3089e2c71d5bde8c0b41ccb0dd297 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id$ */
package org.apache.fop.fo;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.apache.xmlgraphics.util.QName;
import org.apache.fop.apps.FOPException;
import org.apache.fop.fo.extensions.ExtensionAttachment;
import org.apache.fop.fo.flow.ChangeBar;
import org.apache.fop.fo.flow.Marker;
import org.apache.fop.fo.pagination.PageSequence;
import org.apache.fop.fo.properties.Property;
import org.apache.fop.fo.properties.PropertyMaker;
/**
* Base class for representation of formatting objects and their processing.
* All standard formatting object classes extend this class.
*/
public abstract class FObj extends FONode implements Constants {
/** the list of property makers */
private static final PropertyMaker[] PROPERTY_LIST_TABLE
= FOPropertyMapping.getGenericMappings();
/** pointer to the descendant subtree */
protected FONode firstChild;
/** pointer to the end of the descendant subtree */
protected FONode lastChild;
/** The list of extension attachments, null if none */
private List<ExtensionAttachment> extensionAttachments;
/** The map of foreign attributes, null if none */
private Map<QName, String> foreignAttributes;
/** Used to indicate if this FO is either an Out Of Line FO (see rec)
* or a descendant of one. Used during FO validation.
*/
private boolean isOutOfLineFODescendant;
/** Markers added to this element. */
private Map<String, Marker> markers;
private int bidiLevel = -1;
// The value of properties relevant for all fo objects
private String id;
private String layer;
// End of property values
private boolean forceKeepTogether;
/**
* Create a new formatting object.
*
* @param parent the parent node
*/
public FObj(FONode parent) {
super(parent);
// determine if isOutOfLineFODescendant should be set
if (parent != null && parent instanceof FObj) {
if (((FObj) parent).getIsOutOfLineFODescendant()) {
isOutOfLineFODescendant = true;
} else {
int foID = getNameId();
if (foID == FO_FLOAT || foID == FO_FOOTNOTE
|| foID == FO_FOOTNOTE_BODY) {
isOutOfLineFODescendant = true;
}
}
}
}
/** {@inheritDoc} */
public FONode clone(FONode parent, boolean removeChildren)
throws FOPException {
FObj fobj = (FObj) super.clone(parent, removeChildren);
if (removeChildren) {
fobj.firstChild = null;
}
return fobj;
}
/**
* Returns the PropertyMaker for a given property ID.
* @param propId the property ID
* @return the requested Property Maker
*/
public static PropertyMaker getPropertyMakerFor(int propId) {
return PROPERTY_LIST_TABLE[propId];
}
/** {@inheritDoc} */
public void processNode(String elementName, Locator locator,
Attributes attlist, PropertyList pList)
throws FOPException {
setLocator(locator);
pList.addAttributesToList(attlist);
if (!inMarker() || "marker".equals(elementName)) {
bind(pList);
}
warnOnUnknownProperties(attlist, elementName, pList);
}
private void warnOnUnknownProperties(Attributes attlist, String objName, PropertyList propertyList)
throws FOPException {
Map<String, Property> unknowns = propertyList.getUnknownPropertyValues();
for (Entry<String, Property> entry : unknowns.entrySet()) {
FOValidationEventProducer producer = FOValidationEventProducer.Provider.get(getUserAgent()
.getEventBroadcaster());
producer.warnOnInvalidPropertyValue(this, objName,
getAttributeNameForValue(attlist, entry.getValue(), propertyList), entry.getKey(), null,
getLocator());
}
}
private String getAttributeNameForValue(Attributes attList, Property value, PropertyList propertyList)
throws FOPException {
for (int i = 0; i < attList.getLength(); i++) {
String attributeName = attList.getQName(i);
String attributeValue = attList.getValue(i);
Property prop = propertyList.getPropertyForAttribute(attList, attributeName, attributeValue);
if (prop != null && prop.equals(value)) {
return attributeName;
}
}
return "unknown";
}
/**
* Create a default property list for this element.
* {@inheritDoc}
*/
protected PropertyList createPropertyList(PropertyList parent,
FOEventHandler foEventHandler) throws FOPException {
return getBuilderContext().getPropertyListMaker().make(this, parent);
}
/**
* Bind property values from the property list to the FO node.
* Must be overridden in all FObj subclasses that have properties
* applying to it.
* @param pList the PropertyList where the properties can be found.
* @throws FOPException if there is a problem binding the values
*/
public void bind(PropertyList pList) throws FOPException {
id = pList.get(PR_ID).getString();
layer = pList.get(PR_X_LAYER).getString();
}
/**
* {@inheritDoc}
* @throws FOPException FOP Exception
*/
public void startOfNode() throws FOPException {
if (id != null) {
checkId(id);
}
PageSequence pageSequence = getRoot().getLastPageSequence();
if (pageSequence != null && pageSequence.hasChangeBars()) {
startOfNodeChangeBarList = pageSequence.getClonedChangeBarList();
}
}
/**
* {@inheritDoc}
* @throws FOPException FOP Exception
*/
public void endOfNode() throws FOPException {
List<ChangeBar> endOfNodeChangeBarList = null;
PageSequence pageSequence = getRoot().getLastPageSequence();
if (pageSequence != null) {
endOfNodeChangeBarList = pageSequence.getClonedChangeBarList();
}
if (startOfNodeChangeBarList != null && endOfNodeChangeBarList != null) {
nodeChangeBarList = new LinkedList<ChangeBar>(endOfNodeChangeBarList);
nodeChangeBarList.retainAll(startOfNodeChangeBarList);
if (nodeChangeBarList.isEmpty()) {
nodeChangeBarList = null;
}
startOfNodeChangeBarList = null;
}
super.endOfNode();
}
/**
* Setup the id for this formatting object.
* Most formatting objects can have an id that can be referenced.
* This methods checks that the id isn't already used by another FO
*
* @param id the id to check
* @throws ValidationException if the ID is already defined elsewhere
* (strict validation only)
*/
private void checkId(String id) throws ValidationException {
if (!inMarker() && !id.equals("")) {
Set<String> idrefs = getBuilderContext().getIDReferences();
if (!idrefs.contains(id)) {
idrefs.add(id);
} else {
getFOValidationEventProducer().idNotUnique(this, getName(), id, true, locator);
}
}
}
/**
* Returns Out Of Line FO Descendant indicator.
* @return true if Out of Line FO or Out Of Line descendant, false otherwise
*/
boolean getIsOutOfLineFODescendant() {
return isOutOfLineFODescendant;
}
/** {@inheritDoc}*/
protected void addChildNode(FONode child) throws FOPException {
if (child.getNameId() == FO_MARKER) {
addMarker((Marker) child);
} else {
ExtensionAttachment attachment = child.getExtensionAttachment();
if (attachment != null) {
/* This removes the element from the normal children,
* so no layout manager is being created for them
* as they are only additional information.
*/
addExtensionAttachment(attachment);
} else {
if (firstChild == null) {
firstChild = child;
lastChild = child;
} else {
if (lastChild == null) {
FONode prevChild = firstChild;
while (prevChild.siblings != null
&& prevChild.siblings[1] != null) {
prevChild = prevChild.siblings[1];
}
FONode.attachSiblings(prevChild, child);
} else {
FONode.attachSiblings(lastChild, child);
lastChild = child;
}
}
}
}
}
/**
* Used by RetrieveMarker during Marker-subtree cloning
* @param child the (cloned) child node
* @param parent the (cloned) parent node
* @throws FOPException when the child could not be added to the parent
*/
protected static void addChildTo(FONode child, FONode parent)
throws FOPException {
parent.addChildNode(child);
}
/** {@inheritDoc} */
public void removeChild(FONode child) {
FONode nextChild = null;
if (child.siblings != null) {
nextChild = child.siblings[1];
}
if (child == firstChild) {
firstChild = nextChild;
if (firstChild != null) {
firstChild.siblings[0] = null;
}
} else if (child.siblings != null) {
FONode prevChild = child.siblings[0];
prevChild.siblings[1] = nextChild;
if (nextChild != null) {
nextChild.siblings[0] = prevChild;
}
}
if (child == lastChild) {
if (child.siblings != null) {
lastChild = siblings[0];
} else {
lastChild = null;
}
}
}
/**
* Find the nearest parent, grandparent, etc. FONode that is also an FObj
* @return FObj the nearest ancestor FONode that is an FObj
*/
public FObj findNearestAncestorFObj() {
FONode par = parent;
while (par != null && !(par instanceof FObj)) {
par = par.parent;
}
return (FObj) par;
}
/**
* Check if this formatting object generates reference areas.
* @return true if generates reference areas
* TODO see if needed
*/
public boolean generatesReferenceAreas() {
return false;
}
/** {@inheritDoc} */
public FONodeIterator getChildNodes() {
if (hasChildren()) {
return new FObjIterator(this);
}
return null;
}
/**
* Indicates whether this formatting object has children.
* @return true if there are children
*/
public boolean hasChildren() {
return this.firstChild != null;
}
/**
* Return an iterator over the object's childNodes starting
* at the passed-in node (= first call to iterator.next() will
* return childNode)
* @param childNode First node in the iterator
* @return A FONodeIterator or null if childNode isn't a child of
* this FObj.
*/
public FONodeIterator getChildNodes(FONode childNode) {
FONodeIterator it = getChildNodes();
if (it != null) {
if (firstChild == childNode) {
return it;
} else {
while (it.hasNext()
&& it.next().siblings[1] != childNode) {
//nop
}
if (it.hasNext()) {
return it;
} else {
return null;
}
}
}
return null;
}
/**
* Notifies a FObj that one of it's children is removed.
* This method is subclassed by Block to clear the
* firstInlineChild variable in case it doesn't generate
* any areas (see addMarker()).
* @param node the node that was removed
*/
void notifyChildRemoval(FONode node) {
//nop
}
/**
* Add the marker to this formatting object.
* If this object can contain markers it checks that the marker
* has a unique class-name for this object and that it is
* the first child.
* @param marker Marker to add.
*/
protected void addMarker(Marker marker) {
String mcname = marker.getMarkerClassName();
if (firstChild != null) {
// check for empty childNodes
for (FONodeIterator iter = getChildNodes(); iter.hasNext();) {
FONode node = iter.next();
if (node instanceof FObj
|| (node instanceof FOText
&& ((FOText) node).willCreateArea())) {
getFOValidationEventProducer().markerNotInitialChild(this, getName(),
mcname, locator);
return;
} else if (node instanceof FOText) {
iter.remove();
notifyChildRemoval(node);
}
}
}
if (markers == null) {
markers = new HashMap<String, Marker>();
}
if (!markers.containsKey(mcname)) {
markers.put(mcname, marker);
} else {
getFOValidationEventProducer().markerNotUniqueForSameParent(this, getName(),
mcname, locator);
}
}
/**
* @return true if there are any Markers attached to this object
*/
public boolean hasMarkers() {
return markers != null && !markers.isEmpty();
}
/**
* @return the collection of Markers attached to this object
*/
public Map<String, Marker> getMarkers() {
return markers;
}
/** {@inheritDoc} */
protected String getContextInfoAlt() {
StringBuilder sb = new StringBuilder();
if (getLocalName() != null) {
sb.append(getName());
sb.append(", ");
}
if (hasId()) {
sb.append("id=").append(getId());
return sb.toString();
}
String s = gatherContextInfo();
if (s != null) {
sb.append("\"");
if (s.length() < 32) {
sb.append(s);
} else {
sb.append(s.substring(0, 32));
sb.append("...");
}
sb.append("\"");
return sb.toString();
} else {
return null;
}
}
/** {@inheritDoc} */
protected String gatherContextInfo() {
if (getLocator() != null) {
return super.gatherContextInfo();
} else {
FONodeIterator iter = getChildNodes();
if (iter == null) {
return null;
}
StringBuilder sb = new StringBuilder();
while (iter.hasNext()) {
FONode node = iter.next();
String s = node.gatherContextInfo();
if (s != null) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(s);
}
}
return (sb.length() > 0 ? sb.toString() : null);
}
}
/**
* Convenience method for validity checking. Checks if the
* incoming node is a member of the "%block;" parameter entity
* as defined in Sect. 6.2 of the XSL 1.0 &amp; 1.1 Recommendations
*
* @param nsURI namespace URI of incoming node
* @param lName local name (i.e., no prefix) of incoming node
* @return true if a member, false if not
*/
protected boolean isBlockItem(String nsURI, String lName) {
return (FO_URI.equals(nsURI)
&& ("block".equals(lName)
|| "table".equals(lName)
|| "table-and-caption".equals(lName)
|| "block-container".equals(lName)
|| "list-block".equals(lName)
|| "float".equals(lName)
|| isNeutralItem(nsURI, lName)));
}
/**
* Convenience method for validity checking. Checks if the
* incoming node is a member of the "%inline;" parameter entity
* as defined in Sect. 6.2 of the XSL 1.0 &amp; 1.1 Recommendations
*
* @param nsURI namespace URI of incoming node
* @param lName local name (i.e., no prefix) of incoming node
* @return true if a member, false if not
*/
protected boolean isInlineItem(String nsURI, String lName) {
return (FO_URI.equals(nsURI)
&& ("bidi-override".equals(lName)
|| "change-bar-begin".equals(lName)
|| "change-bar-end".equals(lName)
|| "character".equals(lName)
|| "external-graphic".equals(lName)
|| "instream-foreign-object".equals(lName)
|| "inline".equals(lName)
|| "inline-container".equals(lName)
|| "leader".equals(lName)
|| "page-number".equals(lName)
|| "page-number-citation".equals(lName)
|| "page-number-citation-last".equals(lName)
|| "basic-link".equals(lName)
|| ("multi-toggle".equals(lName)
&& (getNameId() == FO_MULTI_CASE
|| findAncestor(FO_MULTI_CASE) > 0))
|| ("footnote".equals(lName)
&& !isOutOfLineFODescendant)
|| isNeutralItem(nsURI, lName)));
}
/**
* Convenience method for validity checking. Checks if the
* incoming node is a member of the "%block;" parameter entity
* or "%inline;" parameter entity
* @param nsURI namespace URI of incoming node
* @param lName local name (i.e., no prefix) of incoming node
* @return true if a member, false if not
*/
protected boolean isBlockOrInlineItem(String nsURI, String lName) {
return (isBlockItem(nsURI, lName) || isInlineItem(nsURI, lName));
}
/**
* Convenience method for validity checking. Checks if the
* incoming node is a member of the neutral item list
* as defined in Sect. 6.2 of the XSL 1.0 &amp; 1.1 Recommendations
* @param nsURI namespace URI of incoming node
* @param lName local name (i.e., no prefix) of incoming node
* @return true if a member, false if not
*/
protected boolean isNeutralItem(String nsURI, String lName) {
return (FO_URI.equals(nsURI)
&& ("multi-switch".equals(lName)
|| "multi-properties".equals(lName)
|| "wrapper".equals(lName)
|| (!isOutOfLineFODescendant && "float".equals(lName))
|| "retrieve-marker".equals(lName)
|| "retrieve-table-marker".equals(lName)));
}
/**
* Convenience method for validity checking. Checks if the
* current node has an ancestor of a given name.
* @param ancestorID ID of node name to check for (e.g., FO_ROOT)
* @return number of levels above FO where ancestor exists,
* -1 if not found
*/
protected int findAncestor(int ancestorID) {
int found = 1;
FONode temp = getParent();
while (temp != null) {
if (temp.getNameId() == ancestorID) {
return found;
}
found += 1;
temp = temp.getParent();
}
return -1;
}
/**
* Clears the list of child nodes.
*/
public void clearChildNodes() {
this.firstChild = null;
}
/** @return the "id" property. */
public String getId() {
return id;
}
/** @return whether this object has an id set */
public boolean hasId() {
return (id != null && id.length() > 0);
}
/** @return the "layer" property. */
public String getLayer() {
return layer;
}
/** @return whether this object has an layer set */
public boolean hasLayer() {
return (layer != null && layer.length() > 0);
}
/** {@inheritDoc} */
public String getNamespaceURI() {
return FOElementMapping.URI;
}
/** {@inheritDoc} */
public String getNormalNamespacePrefix() {
return "fo";
}
/** {@inheritDoc} */
public boolean isBidiRangeBlockItem() {
String ns = getNamespaceURI();
String ln = getLocalName();
return !isNeutralItem(ns, ln) && isBlockItem(ns, ln);
}
/**
* Recursively set resolved bidirectional level of FO (and its ancestors) if
* and only if it is non-negative and if either the current value is reset (-1)
* or the new value is less than the current value.
* @param bidiLevel a non-negative bidi embedding level
*/
public void setBidiLevel(int bidiLevel) {
assert bidiLevel >= 0;
if ((this.bidiLevel < 0) || (bidiLevel < this.bidiLevel)) {
this.bidiLevel = bidiLevel;
if ((parent != null) && !isBidiPropagationBoundary()) {
FObj foParent = (FObj) parent;
int parentBidiLevel = foParent.getBidiLevel();
if ((parentBidiLevel < 0) || (bidiLevel < parentBidiLevel)) {
foParent.setBidiLevel(bidiLevel);
}
}
}
}
/**
* Obtain resolved bidirectional level of FO.
* @return either a non-negative bidi embedding level or -1
* in case no bidi levels have been assigned
*/
public int getBidiLevel() {
return bidiLevel;
}
/**
* Obtain resolved bidirectional level of FO or nearest FO
* ancestor that has a resolved level.
* @return either a non-negative bidi embedding level or -1
* in case no bidi levels have been assigned to this FO or
* any ancestor
*/
public int getBidiLevelRecursive() {
for (FONode fn = this; fn != null; fn = fn.getParent()) {
if (fn instanceof FObj) {
int level = ((FObj) fn).getBidiLevel();
if (level >= 0) {
return level;
}
}
if (isBidiInheritanceBoundary()) {
break;
}
}
return -1;
}
protected boolean isBidiBoundary(boolean propagate) {
return false;
}
private boolean isBidiInheritanceBoundary() {
return isBidiBoundary(false);
}
private boolean isBidiPropagationBoundary() {
return isBidiBoundary(true);
}
/**
* Add a new extension attachment to this FObj.
* (see org.apache.fop.fo.FONode for details)
*
* @param attachment the attachment to add.
*/
void addExtensionAttachment(ExtensionAttachment attachment) {
if (attachment == null) {
throw new NullPointerException(
"Parameter attachment must not be null");
}
if (extensionAttachments == null) {
extensionAttachments = new java.util.ArrayList<ExtensionAttachment>();
}
if (log.isDebugEnabled()) {
log.debug("ExtensionAttachment of category "
+ attachment.getCategory() + " added to "
+ getName() + ": " + attachment);
}
extensionAttachments.add(attachment);
}
/** @return the extension attachments of this FObj. */
public List<ExtensionAttachment> getExtensionAttachments() {
if (extensionAttachments == null) {
return Collections.EMPTY_LIST;
} else {
return extensionAttachments;
}
}
/** @return true if this FObj has extension attachments */
public boolean hasExtensionAttachments() {
return extensionAttachments != null;
}
/**
* Adds a foreign attribute to this FObj.
* @param attributeName the attribute name as a QName instance
* @param value the attribute value
*/
public void addForeignAttribute(QName attributeName, String value) {
/* TODO: Handle this over FOP's property mechanism so we can use
* inheritance.
*/
if (attributeName == null) {
throw new NullPointerException("Parameter attributeName must not be null");
}
if (foreignAttributes == null) {
foreignAttributes = new java.util.HashMap<QName, String>();
}
foreignAttributes.put(attributeName, value);
}
/** @return the map of foreign attributes */
public Map getForeignAttributes() {
if (foreignAttributes == null) {
return Collections.EMPTY_MAP;
} else {
return foreignAttributes;
}
}
/** {@inheritDoc} */
public String toString() {
return (super.toString() + "[@id=" + this.id + "]");
}
public boolean isForceKeepTogether() {
return forceKeepTogether;
}
public void setForceKeepTogether(boolean b) {
forceKeepTogether = b;
}
/** Basic {@link FONode.FONodeIterator} implementation */
public static class FObjIterator implements FONodeIterator {
private static final int F_NONE_ALLOWED = 0;
private static final int F_SET_ALLOWED = 1;
private static final int F_REMOVE_ALLOWED = 2;
private FONode currentNode;
private final FObj parentNode;
private int currentIndex;
private int flags = F_NONE_ALLOWED;
FObjIterator(FObj parent) {
this.parentNode = parent;
this.currentNode = parent.firstChild;
this.currentIndex = 0;
this.flags = F_NONE_ALLOWED;
}
/** {@inheritDoc} */
public FObj parent() {
return parentNode;
}
/** {@inheritDoc} */
public FONode next() {
if (currentNode != null) {
if (currentIndex != 0) {
if (currentNode.siblings != null
&& currentNode.siblings[1] != null) {
currentNode = currentNode.siblings[1];
} else {
throw new NoSuchElementException();
}
}
currentIndex++;
flags |= (F_SET_ALLOWED | F_REMOVE_ALLOWED);
return currentNode;
} else {
throw new NoSuchElementException();
}
}
/** {@inheritDoc} */
public FONode previous() {
if (currentNode.siblings != null
&& currentNode.siblings[0] != null) {
currentIndex--;
currentNode = currentNode.siblings[0];
flags |= (F_SET_ALLOWED | F_REMOVE_ALLOWED);
return currentNode;
} else {
throw new NoSuchElementException();
}
}
/** {@inheritDoc} */
public void set(FONode newNode) {
if ((flags & F_SET_ALLOWED) == F_SET_ALLOWED) {
if (currentNode == parentNode.firstChild) {
parentNode.firstChild = newNode;
} else {
FONode.attachSiblings(currentNode.siblings[0], newNode);
}
if (currentNode.siblings != null
&& currentNode.siblings[1] != null) {
FONode.attachSiblings(newNode, currentNode.siblings[1]);
}
if (currentNode == parentNode.lastChild) {
parentNode.lastChild = newNode;
}
} else {
throw new IllegalStateException();
}
}
/** {@inheritDoc} */
public void add(FONode newNode) {
if (currentIndex == -1) {
if (currentNode != null) {
FONode.attachSiblings(newNode, currentNode);
}
parentNode.firstChild = newNode;
currentIndex = 0;
currentNode = newNode;
if (parentNode.lastChild == null) {
parentNode.lastChild = newNode;
}
} else {
if (currentNode.siblings != null
&& currentNode.siblings[1] != null) {
FONode.attachSiblings(newNode, currentNode.siblings[1]);
}
FONode.attachSiblings(currentNode, newNode);
if (currentNode == parentNode.lastChild) {
parentNode.lastChild = newNode;
}
}
flags &= F_NONE_ALLOWED;
}
/** {@inheritDoc} */
public boolean hasNext() {
return (currentNode != null)
&& ((currentIndex == 0)
|| (currentNode.siblings != null
&& currentNode.siblings[1] != null));
}
/** {@inheritDoc} */
public boolean hasPrevious() {
return (currentIndex != 0)
|| (currentNode.siblings != null
&& currentNode.siblings[0] != null);
}
/** {@inheritDoc} */
public int nextIndex() {
return currentIndex + 1;
}
/** {@inheritDoc} */
public int previousIndex() {
return currentIndex - 1;
}
/** {@inheritDoc} */
public void remove() {
if ((flags & F_REMOVE_ALLOWED) == F_REMOVE_ALLOWED) {
parentNode.removeChild(currentNode);
if (currentIndex == 0) {
//first node removed
currentNode = parentNode.firstChild;
} else if (currentNode.siblings != null
&& currentNode.siblings[0] != null) {
currentNode = currentNode.siblings[0];
currentIndex--;
} else {
currentNode = null;
}
flags &= F_NONE_ALLOWED;
} else {
throw new IllegalStateException();
}
}
/** {@inheritDoc} */
public FONode last() {
while (currentNode != null
&& currentNode.siblings != null
&& currentNode.siblings[1] != null) {
currentNode = currentNode.siblings[1];
currentIndex++;
}
return currentNode;
}
/** {@inheritDoc} */
public FONode first() {
currentNode = parentNode.firstChild;
currentIndex = 0;
return currentNode;
}
}
}