| /* |
| * 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.Iterator; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| 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.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[] propertyListTable |
| = FOPropertyMapping.getGenericMappings(); |
| |
| /** |
| * pointer to the descendant subtree |
| */ |
| protected FONode firstChild; |
| |
| /** The list of extension attachments, null if none */ |
| private List/*<ExtensionAttachment>*/ extensionAttachments = null; |
| |
| /** The map of foreign attributes, null if none */ |
| private Map/*<QName,String>*/ 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 = null; |
| |
| // The value of properties relevant for all fo objects |
| private String id = null; |
| // 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 propertyListTable[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)) { |
| pList.setWritingMode(); |
| bind(pList); |
| } |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * @throws FOPException FOP Exception |
| */ |
| protected 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; |
| } else { |
| FONode prevChild = firstChild; |
| while (prevChild.siblings != null |
| && prevChild.siblings[1] != null) { |
| prevChild = prevChild.siblings[1]; |
| } |
| FONode.attachSiblings(prevChild, 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; |
| } |
| } |
| } |
| |
| /** |
| * 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 java.util.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 |
| */ |
| 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; |
| } |
| |
| /** {@inheritDoc} */ |
| public String getNamespaceURI() { |
| return FOElementMapping.URI; |
| } |
| |
| /** {@inheritDoc} */ |
| public String getNormalNamespacePrefix() { |
| return "fo"; |
| } |
| |
| /** |
| * 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 + "]"); |
| } |
| |
| /** Basic {@link FONodeIterator} implementation */ |
| public 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]); |
| } |
| } 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; |
| } else { |
| if (currentNode.siblings != null |
| && currentNode.siblings[1] != null) { |
| FONode.attachSiblings((FONode) o, currentNode.siblings[1]); |
| } |
| FONode.attachSiblings(currentNode, (FONode) o); |
| } |
| 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(); |
| } |
| } |
| } |