001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: /* $Id: FObj.java 554104 2007-07-07 01:07:10Z adelmelle $ */
019:
020: package org.apache.fop.fo;
021:
022: import java.util.Collections;
023: import java.util.Iterator;
024: import java.util.List;
025: import java.util.ListIterator;
026: import java.util.Map;
027: import java.util.NoSuchElementException;
028: import java.util.Set;
029:
030: import org.apache.fop.apps.FOPException;
031: import org.apache.fop.fo.extensions.ExtensionAttachment;
032: import org.apache.fop.fo.flow.Marker;
033: import org.apache.fop.fo.properties.PropertyMaker;
034: import org.apache.fop.util.QName;
035: import org.xml.sax.Attributes;
036: import org.xml.sax.Locator;
037:
038: /**
039: * Base class for representation of formatting objects and their processing.
040: */
041: public abstract class FObj extends FONode implements Constants {
042:
043: /** the list of property makers */
044: private static PropertyMaker[] propertyListTable = FOPropertyMapping
045: .getGenericMappings();
046:
047: /**
048: * pointer to the descendant subtree
049: */
050: protected FONode firstChild;
051:
052: /** The list of extension attachments, null if none */
053: private List extensionAttachments = null;
054:
055: /** The map of foreign attributes, null if none */
056: private Map foreignAttributes = null;
057:
058: /** Used to indicate if this FO is either an Out Of Line FO (see rec)
059: * or a descendant of one. Used during FO validation.
060: */
061: private boolean isOutOfLineFODescendant = false;
062:
063: /** Markers added to this element. */
064: private Map markers = null;
065:
066: // The value of properties relevant for all fo objects
067: private String id = null;
068:
069: // End of property values
070:
071: /**
072: * Create a new formatting object.
073: * All formatting object classes extend this class.
074: *
075: * @param parent the parent node
076: */
077: public FObj(FONode parent) {
078: super (parent);
079:
080: // determine if isOutOfLineFODescendant should be set
081: if (parent != null && parent instanceof FObj) {
082: if (((FObj) parent).getIsOutOfLineFODescendant()) {
083: isOutOfLineFODescendant = true;
084: } else {
085: int foID = getNameId();
086: if (foID == FO_FLOAT || foID == FO_FOOTNOTE
087: || foID == FO_FOOTNOTE_BODY) {
088: isOutOfLineFODescendant = true;
089: }
090: }
091: }
092: }
093:
094: /**
095: * @see org.apache.fop.fo.FONode#clone(FONode, boolean)
096: */
097: public FONode clone(FONode parent, boolean removeChildren)
098: throws FOPException {
099: FObj fobj = (FObj) super .clone(parent, removeChildren);
100: if (removeChildren) {
101: fobj.firstChild = null;
102: }
103: return fobj;
104: }
105:
106: /**
107: * Returns the PropertyMaker for a given property ID.
108: * @param propId the property ID
109: * @return the requested Property Maker
110: */
111: public static PropertyMaker getPropertyMakerFor(int propId) {
112: return propertyListTable[propId];
113: }
114:
115: /**
116: * @see org.apache.fop.fo.FONode#processNode
117: */
118: public void processNode(String elementName, Locator locator,
119: Attributes attlist, PropertyList pList) throws FOPException {
120: setLocator(locator);
121: pList.addAttributesToList(attlist);
122: if (!inMarker() || "marker".equals(elementName)) {
123: pList.setWritingMode();
124: bind(pList);
125: }
126: }
127:
128: /**
129: * Create a default property list for this element.
130: * @see org.apache.fop.fo.FONode
131: */
132: protected PropertyList createPropertyList(PropertyList parent,
133: FOEventHandler foEventHandler) throws FOPException {
134: return foEventHandler.getPropertyListMaker().make(this , parent);
135: }
136:
137: /**
138: * Bind property values from the property list to the FO node.
139: * Must be overridden in all FObj subclasses that have properties
140: * applying to it.
141: * @param pList the PropertyList where the properties can be found.
142: * @throws FOPException if there is a problem binding the values
143: */
144: public void bind(PropertyList pList) throws FOPException {
145: id = pList.get(PR_ID).getString();
146: }
147:
148: /**
149: * @see org.apache.fop.fo.FONode#startOfNode
150: * @throws FOPException FOP Exception
151: */
152: protected void startOfNode() throws FOPException {
153: if (id != null) {
154: checkId(id);
155: }
156: }
157:
158: /**
159: * Setup the id for this formatting object.
160: * Most formatting objects can have an id that can be referenced.
161: * This methods checks that the id isn't already used by another FO
162: *
163: * @param id the id to check
164: * @throws ValidationException if the ID is already defined elsewhere
165: * (strict validation only)
166: */
167: private void checkId(String id) throws ValidationException {
168: if (!inMarker() && !id.equals("")) {
169: Set idrefs = getFOEventHandler().getIDReferences();
170: if (!idrefs.contains(id)) {
171: idrefs.add(id);
172: } else {
173: if (getUserAgent().validateStrictly()) {
174: throw new ValidationException(
175: "Property id \""
176: + id
177: + "\" previously used; id values must be unique"
178: + " in document.", locator);
179: } else {
180: if (log.isWarnEnabled()) {
181: StringBuffer msg = new StringBuffer();
182: msg.append("Found non-unique id on ").append(
183: getName());
184: if (locator.getLineNumber() != -1) {
185: msg.append(" (at ").append(
186: locator.getLineNumber())
187: .append("/").append(
188: locator.getColumnNumber())
189: .append(")");
190: }
191: msg
192: .append("\nAny reference to it will be considered "
193: + "a reference to the first occurrence "
194: + "in the document.");
195: log.warn(msg);
196: }
197: }
198: }
199: }
200: }
201:
202: /**
203: * Returns Out Of Line FO Descendant indicator.
204: * @return true if Out of Line FO or Out Of Line descendant, false otherwise
205: */
206: public boolean getIsOutOfLineFODescendant() {
207: return isOutOfLineFODescendant;
208: }
209:
210: /**
211: * @see org.apache.fop.fo.FONode#addChildNode(FONode)
212: */
213: protected void addChildNode(FONode child) throws FOPException {
214: if (canHaveMarkers() && child.getNameId() == FO_MARKER) {
215: addMarker((Marker) child);
216: } else {
217: ExtensionAttachment attachment = child
218: .getExtensionAttachment();
219: if (attachment != null) {
220: /* This removes the element from the normal children,
221: * so no layout manager is being created for them
222: * as they are only additional information.
223: */
224: addExtensionAttachment(attachment);
225: } else {
226: if (firstChild == null) {
227: firstChild = child;
228: } else {
229: FONode prevChild = firstChild;
230: while (prevChild.siblings != null
231: && prevChild.siblings[1] != null) {
232: prevChild = prevChild.siblings[1];
233: }
234: FONode.attachSiblings(prevChild, child);
235: }
236: }
237: }
238: }
239:
240: /**
241: * Used by RetrieveMarker during Marker-subtree cloning
242: * @param child the (cloned) child node
243: * @param parent the (cloned) parent node
244: * @throws FOPException when the child could not be added to the parent
245: */
246: protected static void addChildTo(FONode child, FObj parent)
247: throws FOPException {
248: parent.addChildNode(child);
249: }
250:
251: /** @see org.apache.fop.fo.FONode#removeChild(org.apache.fop.fo.FONode) */
252: public void removeChild(FONode child) {
253: FONode nextChild = null;
254: if (child.siblings != null) {
255: nextChild = child.siblings[1];
256: }
257: if (child == firstChild) {
258: firstChild = nextChild;
259: if (firstChild != null) {
260: firstChild.siblings[0] = null;
261: }
262: } else {
263: FONode prevChild = child.siblings[0];
264: prevChild.siblings[1] = nextChild;
265: if (nextChild != null) {
266: nextChild.siblings[0] = prevChild;
267: }
268: }
269: }
270:
271: /**
272: * Find the nearest parent, grandparent, etc. FONode that is also an FObj
273: * @return FObj the nearest ancestor FONode that is an FObj
274: */
275: public FObj findNearestAncestorFObj() {
276: FONode par = parent;
277: while (par != null && !(par instanceof FObj)) {
278: par = par.parent;
279: }
280: return (FObj) par;
281: }
282:
283: /**
284: * Check if this formatting object generates reference areas.
285: * @return true if generates reference areas
286: * @todo see if needed
287: */
288: public boolean generatesReferenceAreas() {
289: return false;
290: }
291:
292: /**
293: * @see org.apache.fop.fo.FONode#getChildNodes()
294: */
295: public FONodeIterator getChildNodes() {
296: if (firstChild != null) {
297: return new FObjIterator(this );
298: }
299: return null;
300: }
301:
302: /**
303: * Return an iterator over the object's childNodes starting
304: * at the passed-in node (= first call to iterator.next() will
305: * return childNode)
306: * @param childNode First node in the iterator
307: * @return A ListIterator or null if childNode isn't a child of
308: * this FObj.
309: */
310: public FONodeIterator getChildNodes(FONode childNode) {
311: FONodeIterator it = getChildNodes();
312: if (it != null) {
313: if (firstChild == childNode) {
314: return it;
315: } else {
316: while (it.hasNext()
317: && it.nextNode().siblings[1] != childNode) {
318: //nop
319: }
320: if (it.hasNext()) {
321: return it;
322: } else {
323: return null;
324: }
325: }
326: }
327: return null;
328: }
329:
330: /**
331: * Notifies a FObj that one of it's children is removed.
332: * This method is subclassed by Block to clear the
333: * firstInlineChild variable in case it doesn't generate
334: * any areas (see addMarker()).
335: * @param node the node that was removed
336: */
337: protected void notifyChildRemoval(FONode node) {
338: //nop
339: }
340:
341: /**
342: * Add the marker to this formatting object.
343: * If this object can contain markers it checks that the marker
344: * has a unique class-name for this object and that it is
345: * the first child.
346: * @param marker Marker to add.
347: */
348: protected void addMarker(Marker marker) {
349: String mcname = marker.getMarkerClassName();
350: if (firstChild != null) {
351: // check for empty childNodes
352: for (Iterator iter = getChildNodes(); iter.hasNext();) {
353: FONode node = (FONode) iter.next();
354: if (node instanceof FObj
355: || (node instanceof FOText && ((FOText) node)
356: .willCreateArea())) {
357: log.error("fo:marker must be an initial child: "
358: + mcname);
359: return;
360: } else if (node instanceof FOText) {
361: iter.remove();
362: notifyChildRemoval(node);
363: }
364: }
365: }
366: if (markers == null) {
367: markers = new java.util.HashMap();
368: }
369: if (!markers.containsKey(mcname)) {
370: markers.put(mcname, marker);
371: } else {
372: log.error("fo:marker 'marker-class-name' "
373: + "must be unique for same parent: " + mcname);
374: }
375: }
376:
377: /**
378: * @return true if there are any Markers attached to this object
379: */
380: public boolean hasMarkers() {
381: return markers != null && !markers.isEmpty();
382: }
383:
384: /**
385: * @return the collection of Markers attached to this object
386: */
387: public Map getMarkers() {
388: return markers;
389: }
390:
391: /** @see org.apache.fop.fo.FONode#gatherContextInfo() */
392: protected String gatherContextInfo() {
393: if (getLocator() != null) {
394: return super .gatherContextInfo();
395: } else {
396: ListIterator iter = getChildNodes();
397: if (iter == null) {
398: return null;
399: }
400: StringBuffer sb = new StringBuffer();
401: while (iter.hasNext()) {
402: FONode node = (FONode) iter.next();
403: String s = node.gatherContextInfo();
404: if (s != null) {
405: if (sb.length() > 0) {
406: sb.append(", ");
407: }
408: sb.append(s);
409: }
410: }
411: return (sb.length() > 0 ? sb.toString() : null);
412: }
413: }
414:
415: /**
416: * Convenience method for validity checking. Checks if the
417: * incoming node is a member of the "%block;" parameter entity
418: * as defined in Sect. 6.2 of the XSL 1.0 & 1.1 Recommendations
419: * @param nsURI namespace URI of incoming node
420: * @param lName local name (i.e., no prefix) of incoming node
421: * @return true if a member, false if not
422: */
423: protected boolean isBlockItem(String nsURI, String lName) {
424: return (FO_URI.equals(nsURI) && (lName.equals("block")
425: || lName.equals("table")
426: || lName.equals("table-and-caption")
427: || lName.equals("block-container")
428: || lName.equals("list-block") || lName.equals("float") || isNeutralItem(
429: nsURI, lName)));
430: }
431:
432: /**
433: * Convenience method for validity checking. Checks if the
434: * incoming node is a member of the "%inline;" parameter entity
435: * as defined in Sect. 6.2 of the XSL 1.0 & 1.1 Recommendations
436: * @param nsURI namespace URI of incoming node
437: * @param lName local name (i.e., no prefix) of incoming node
438: * @return true if a member, false if not
439: */
440: protected boolean isInlineItem(String nsURI, String lName) {
441: return (FO_URI.equals(nsURI) && (lName.equals("bidi-override")
442: || lName.equals("character")
443: || lName.equals("external-graphic")
444: || lName.equals("instream-foreign-object")
445: || lName.equals("inline")
446: || lName.equals("inline-container")
447: || lName.equals("leader")
448: || lName.equals("page-number")
449: || lName.equals("page-number-citation")
450: || lName.equals("page-number-citation-last")
451: || lName.equals("basic-link")
452: || (lName.equals("multi-toggle") && (getNameId() == FO_MULTI_CASE || findAncestor(FO_MULTI_CASE) > 0))
453: || (lName.equals("footnote") && !isOutOfLineFODescendant) || isNeutralItem(
454: nsURI, lName)));
455: }
456:
457: /**
458: * Convenience method for validity checking. Checks if the
459: * incoming node is a member of the "%block;" parameter entity
460: * or "%inline;" parameter entity
461: * @param nsURI namespace URI of incoming node
462: * @param lName local name (i.e., no prefix) of incoming node
463: * @return true if a member, false if not
464: */
465: protected boolean isBlockOrInlineItem(String nsURI, String lName) {
466: return (isBlockItem(nsURI, lName) || isInlineItem(nsURI, lName));
467: }
468:
469: /**
470: * Convenience method for validity checking. Checks if the
471: * incoming node is a member of the neutral item list
472: * as defined in Sect. 6.2 of the XSL 1.0 & 1.1 Recommendations
473: * @param nsURI namespace URI of incoming node
474: * @param lName local name (i.e., no prefix) of incoming node
475: * @return true if a member, false if not
476: */
477: protected boolean isNeutralItem(String nsURI, String lName) {
478: return (FO_URI.equals(nsURI) && (lName.equals("multi-switch")
479: || lName.equals("multi-properties")
480: || lName.equals("wrapper")
481: || (!isOutOfLineFODescendant && lName.equals("float")) || lName
482: .equals("retrieve-marker")));
483: }
484:
485: /**
486: * Convenience method for validity checking. Checks if the
487: * current node has an ancestor of a given name.
488: * @param ancestorID ID of node name to check for (e.g., FO_ROOT)
489: * @return number of levels above FO where ancestor exists,
490: * -1 if not found
491: */
492: protected int findAncestor(int ancestorID) {
493: int found = 1;
494: FONode temp = getParent();
495: while (temp != null) {
496: if (temp.getNameId() == ancestorID) {
497: return found;
498: }
499: found += 1;
500: temp = temp.getParent();
501: }
502: return -1;
503: }
504:
505: /** @return the "id" property. */
506: public String getId() {
507: return id;
508: }
509:
510: /** @return whether this object has an id set */
511: public boolean hasId() {
512: return id != null && id.length() > 0;
513: }
514:
515: /** @see org.apache.fop.fo.FONode#getNamespaceURI() */
516: public String getNamespaceURI() {
517: return FOElementMapping.URI;
518: }
519:
520: /** @see org.apache.fop.fo.FONode#getNormalNamespacePrefix() */
521: public String getNormalNamespacePrefix() {
522: return "fo";
523: }
524:
525: /**
526: * Add a new extension attachment to this FObj.
527: * (see org.apache.fop.fo.FONode for details)
528: *
529: * @param attachment the attachment to add.
530: */
531: public void addExtensionAttachment(ExtensionAttachment attachment) {
532: if (attachment == null) {
533: throw new NullPointerException(
534: "Parameter attachment must not be null");
535: }
536: if (extensionAttachments == null) {
537: extensionAttachments = new java.util.ArrayList();
538: }
539: if (log.isDebugEnabled()) {
540: log.debug("ExtensionAttachment of category "
541: + attachment.getCategory() + " added to "
542: + getName() + ": " + attachment);
543: }
544: extensionAttachments.add(attachment);
545: }
546:
547: /** @return the extension attachments of this FObj. */
548: public List getExtensionAttachments() {
549: if (extensionAttachments == null) {
550: return Collections.EMPTY_LIST;
551: } else {
552: return extensionAttachments;
553: }
554: }
555:
556: /**
557: * Adds a foreign attribute to this FObj.
558: * @param attributeName the attribute name as a QName instance
559: * @param value the attribute value
560: */
561: public void addForeignAttribute(QName attributeName, String value) {
562: /* TODO: Handle this over FOP's property mechanism so we can use
563: * inheritance.
564: */
565: if (attributeName == null) {
566: throw new NullPointerException(
567: "Parameter attributeName must not be null");
568: }
569: if (foreignAttributes == null) {
570: foreignAttributes = new java.util.HashMap();
571: }
572: foreignAttributes.put(attributeName, value);
573: }
574:
575: /** @return the map of foreign attributes */
576: public Map getForeignAttributes() {
577: if (foreignAttributes == null) {
578: return Collections.EMPTY_MAP;
579: } else {
580: return foreignAttributes;
581: }
582: }
583:
584: public class FObjIterator implements FONodeIterator {
585:
586: private static final int F_NONE_ALLOWED = 0;
587: private static final int F_SET_ALLOWED = 1;
588: private static final int F_REMOVE_ALLOWED = 2;
589:
590: private FONode currentNode;
591: private FObj parentNode;
592: private int currentIndex;
593: private int flags = F_NONE_ALLOWED;
594:
595: protected FObjIterator(FObj parent) {
596: this .parentNode = parent;
597: this .currentNode = parent.firstChild;
598: this .currentIndex = 0;
599: this .flags = F_NONE_ALLOWED;
600: }
601:
602: /**
603: * @see FONodeIterator#parentNode()
604: */
605: public FObj parentNode() {
606: return parentNode;
607: }
608:
609: /**
610: * @see java.util.ListIterator#next()
611: */
612: public Object next() {
613: if (currentNode != null) {
614: if (currentIndex != 0) {
615: if (currentNode.siblings != null
616: && currentNode.siblings[1] != null) {
617: currentNode = currentNode.siblings[1];
618: } else {
619: throw new NoSuchElementException();
620: }
621: }
622: currentIndex++;
623: flags |= (F_SET_ALLOWED | F_REMOVE_ALLOWED);
624: return currentNode;
625: } else {
626: throw new NoSuchElementException();
627: }
628: }
629:
630: /**
631: * @see java.util.ListIterator#previous()
632: */
633: public Object previous() {
634: if (currentNode.siblings != null
635: && currentNode.siblings[0] != null) {
636: currentIndex--;
637: currentNode = currentNode.siblings[0];
638: flags |= (F_SET_ALLOWED | F_REMOVE_ALLOWED);
639: return currentNode;
640: } else {
641: throw new NoSuchElementException();
642: }
643: }
644:
645: /**
646: * @see java.util.ListIterator#set(Object)
647: */
648: public void set(Object o) {
649: if ((flags & F_SET_ALLOWED) == F_SET_ALLOWED) {
650: FONode newNode = (FONode) o;
651: if (currentNode == parentNode.firstChild) {
652: parentNode.firstChild = newNode;
653: } else {
654: FONode.attachSiblings(currentNode.siblings[0],
655: newNode);
656: }
657: if (currentNode.siblings != null
658: && currentNode.siblings[1] != null) {
659: FONode.attachSiblings(newNode,
660: currentNode.siblings[1]);
661: }
662: } else {
663: throw new IllegalStateException();
664: }
665: }
666:
667: /**
668: * @see java.util.ListIterator#add(Object)
669: */
670: public void add(Object o) {
671: FONode newNode = (FONode) o;
672: if (currentIndex == -1) {
673: if (currentNode != null) {
674: FONode.attachSiblings(newNode, currentNode);
675: }
676: parentNode.firstChild = newNode;
677: currentIndex = 0;
678: currentNode = newNode;
679: } else {
680: if (currentNode.siblings != null
681: && currentNode.siblings[1] != null) {
682: FONode.attachSiblings((FONode) o,
683: currentNode.siblings[1]);
684: }
685: FONode.attachSiblings(currentNode, (FONode) o);
686: }
687: flags &= F_NONE_ALLOWED;
688: }
689:
690: /**
691: * @see java.util.ListIterator#hasNext()
692: */
693: public boolean hasNext() {
694: return (currentNode != null)
695: && ((currentIndex == 0) || (currentNode.siblings != null && currentNode.siblings[1] != null));
696: }
697:
698: /**
699: * @see java.util.ListIterator#hasPrevious()
700: */
701: public boolean hasPrevious() {
702: return (currentIndex != 0)
703: || (currentNode.siblings != null && currentNode.siblings[0] != null);
704: }
705:
706: /**
707: * @see java.util.ListIterator#nextIndex()
708: */
709: public int nextIndex() {
710: return currentIndex + 1;
711: }
712:
713: /**
714: * @see java.util.ListIterator#previousIndex()
715: */
716: public int previousIndex() {
717: return currentIndex - 1;
718: }
719:
720: /**
721: * @see java.util.ListIterator#remove()
722: */
723: public void remove() {
724: if ((flags & F_REMOVE_ALLOWED) == F_REMOVE_ALLOWED) {
725: parentNode.removeChild(currentNode);
726: if (currentIndex == 0) {
727: //first node removed
728: currentNode = parentNode.firstChild;
729: } else if (currentNode.siblings != null
730: && currentNode.siblings[0] != null) {
731: currentNode = currentNode.siblings[0];
732: currentIndex--;
733: } else {
734: currentNode = null;
735: }
736: flags &= F_NONE_ALLOWED;
737: } else {
738: throw new IllegalStateException();
739: }
740: }
741:
742: public FONode lastNode() {
743: while (currentNode != null && currentNode.siblings != null
744: && currentNode.siblings[1] != null) {
745: currentNode = currentNode.siblings[1];
746: currentIndex++;
747: }
748: return currentNode;
749: }
750:
751: public FONode firstNode() {
752: currentNode = parentNode.firstChild;
753: currentIndex = 0;
754: return currentNode;
755: }
756:
757: public FONode nextNode() {
758: return (FONode) next();
759: }
760:
761: public FONode previousNode() {
762: return (FONode) previous();
763: }
764: }
765:
766: }
|