/* * 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.Iterator; import java.util.List; import java.util.ListIterator; 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.Marker; import org.apache.fop.fo.flow.table.TableCell; 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 extensionAttachments = null; /** The map of foreign attributes, null if none */ private Map foreignAttributes = null; /** 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 = false; /** Markers added to this element. */ private Map markers; private int bidiLevel = -1; // The value of properties relevant for all fo objects private String id; private String layer; // End of property values /** * 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 unknowns = propertyList.getUnknownPropertyValues(); for (Entry 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); } } /** * 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 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 { 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 ListIterator 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.nextNode().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 (Iterator iter = getChildNodes(); iter.hasNext();) { FONode node = (FONode) 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(); } 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 getMarkers() { return markers; } /** {@inheritDoc} */ protected String getContextInfoAlt() { StringBuffer sb = new StringBuffer(); 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 { ListIterator iter = getChildNodes(); if (iter == null) { return null; } StringBuffer sb = new StringBuffer(); while (iter.hasNext()) { FONode node = (FONode) 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 & 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 & 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) || "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 & 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 instanceof TableCell && (ancestorID == FO_TABLE_HEADER || ancestorID == FO_TABLE_FOOTER)) { // note that if the retrieve-table-marker is not in a table-header/footer an exception is // thrown, so no need to reset this flag in that case ((TableCell) temp).flagAsHavingRetrieveTableMarker(); } 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 (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(); } 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/**/ 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(); } 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 + "]"); } /** 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 parentNode() { return parentNode; } /** {@inheritDoc} */ public Object 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 Object 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(Object o) { if ((flags & F_SET_ALLOWED) == F_SET_ALLOWED) { FONode newNode = (FONode) o; 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(Object o) { FONode newNode = (FONode) o; 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((FONode) o, currentNode.siblings[1]); } FONode.attachSiblings(currentNode, (FONode) o); 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 lastNode() { while (currentNode != null && currentNode.siblings != null && currentNode.siblings[1] != null) { currentNode = currentNode.siblings[1]; currentIndex++; } return currentNode; } /** {@inheritDoc} */ public FONode firstNode() { currentNode = parentNode.firstChild; currentIndex = 0; return currentNode; } /** {@inheritDoc} */ public FONode nextNode() { return (FONode) next(); } /** {@inheritDoc} */ public FONode previousNode() { return (FONode) previous(); } } }