001: package uk.org.ponder.saxalizer;
002:
003: import java.io.ByteArrayOutputStream;
004: import java.io.OutputStream;
005: import java.util.ArrayList;
006: import java.util.Enumeration;
007: import java.util.Iterator;
008: import java.util.List;
009: import java.util.Map;
010:
011: import uk.org.ponder.arrayutil.ListUtil;
012: import uk.org.ponder.util.EnumerationConverter;
013: import uk.org.ponder.util.Logger;
014: import uk.org.ponder.util.UniversalRuntimeException;
015: import uk.org.ponder.xml.XMLWriter;
016:
017: /**
018: * The DeSAXalizer takes a tree of Java objects and writes them to an XML
019: * stream. Architectural note - this used to be a stateless class with only
020: * static methods. This resulted in a slightly cleaner design since all state
021: * alterations were transparent, but in the end the number of arguments to
022: * internal methods began to snowball. Constructing a DeSAXalizer costs
023: * virtually nothing in the modern world.
024: */
025: public class DeSAXalizer {
026: // one of these objects is allocated in a stack for each tag level.
027: static class SerialContext {
028: Object object; // The object being serialised
029: MethodAnalyser ma;
030: // the current index through the get methods
031: SAMIterator getenum;
032: // enumeration for a vector tag, while enum is active, currentgetindex
033: // progress
034: // is suspended. enum will be set to null when it expires.
035: Enumeration enumm;
036: // The (original) tag name of the enum in progress
037: // String enumtagname;
038: String stashedclosingtag;
039: boolean writtenchild = false;
040:
041: SerialContext(Object object,
042: SAXalizerMappingContext mappingcontext) {
043: this .object = object;
044: this .ma = mappingcontext.getAnalyser(object.getClass());
045: // for generic objects, currentgetindex has an honorary value of -1
046: // representing
047: // getChildEnum()
048: // currentgetindex = object instanceof GenericSAX ? -1 : 0;
049: stashedclosingtag = null;
050: getenum = ma.tagmethods.getGetEnumeration();
051: }
052: }
053:
054: // These five members constitute the state of the DeSAXalizer.
055: // This is a Stack of serialcontexts.
056: private List desaxingobjects;
057:
058: public SerialContext getDeSAXingObject() {
059: return (SerialContext) ListUtil.peek(desaxingobjects);
060: }
061:
062: private XMLWriter xmlw;
063: private SAXalizerMappingContext mappingcontext;
064: private DeSAXalizerForbidder forbidder = null;
065:
066: public DeSAXalizer() {
067: this (SAXalizerMappingContext.instance());
068: }
069:
070: public DeSAXalizer(SAXalizerMappingContext mappingcontext) {
071: this .mappingcontext = mappingcontext;
072: }
073:
074: private int indentlevel;
075:
076: public int getIndent() {
077: return indentlevel == COMPACT_MODE ? COMPACT_MODE : indentlevel
078: + desaxingobjects.size();
079: }
080:
081: public void setForbidder(DeSAXalizerForbidder forbidder) {
082: this .forbidder = forbidder;
083: }
084:
085: private void blastState() {
086: xmlw = null;
087: desaxingobjects.clear();
088: forbidder = null;
089: }
090:
091: /**
092: * Render a <code>DeSAXalizable</code> object as a string.
093: *
094: * @param root The object to be rendered as a string.
095: * @param roottag The root tag of the rendered tree
096: * @return The supplied object rendered as a <code>String</code> of XML
097: * tags, minus declaration.
098: */
099:
100: public String toString(Object root, String roottag,
101: boolean compactmode) {
102: ByteArrayOutputStream baos = new ByteArrayOutputStream();
103: if (root == null)
104: return "null";
105: try {
106: serializeSubtree(root, roottag, baos,
107: compactmode ? COMPACT_MODE : 0);
108: baos.close();
109: return baos.toString(XMLWriter.DEFAULT_ENCODING);
110: } catch (Throwable t) {
111: throw UniversalRuntimeException.accumulate(t,
112: "Error writing object for " + roottag);
113: }
114: }
115:
116: /**
117: * Write a serialized representation of an object subtree as XML to an output
118: * stream. This method DOES NOT close the supplied output stream.
119: *
120: * @param root The root object of the tree to be serialized. Must implement
121: * the <code>DeSAXalizable</code> interface, as must all
122: * descendents, or have types registered with the
123: * <code>SAXLeafParser</code>.
124: * @param roottag The tag to be supplied to the root object (all subobjects
125: * have their tags supplied through the <code>DeSAXalizable</code>
126: * interface.
127: * @param os The output stream to receive the serialized data. The data will
128: * be written in UTF-8 format.
129: */
130:
131: public void serializeSubtree(Object root, String roottag,
132: OutputStream os) {
133: serializeSubtree(root, roottag, os, 0);
134: }
135:
136: private void appendAttr(String attrname, Object attrvalue) {
137: xmlw.writeRaw(" ");
138: xmlw.writeRaw(attrname); // attribute names may not contain escapes
139: xmlw.writeRaw("=\"");
140: xmlw.write(mappingcontext.saxleafparser.render(attrvalue));
141: xmlw.writeRaw("\"");
142: }
143:
144: private void renderAttrs(Object torender, SAMIterator getattrenum) {
145: for (; getattrenum.valid(); getattrenum.next()) {
146: SAXAccessMethod getattr = getattrenum.get();
147: Object attrvalue = getattr.getChildObject(torender);
148: Logger.println("Attr " + getattr.tagname
149: + ": returned object " + attrvalue,
150: Logger.DEBUG_SUBATOMIC);
151: if (attrvalue != null) {
152: appendAttr(getattr.tagname, attrvalue);
153: }
154: }
155: // now find and write any extra attributes
156: if (torender instanceof SAXalizableExtraAttrs) {
157: Map extraattrs = ((SAXalizableExtraAttrs) torender)
158: .getAttributes();
159: if (extraattrs != null) {
160: Iterator attrs = extraattrs.keySet().iterator();
161: while (attrs.hasNext()) {
162: String attributename = (String) attrs.next();
163: String attributevalue = (String) extraattrs
164: .get(attributename);
165: appendAttr(attributename, attributevalue);
166: }
167: }
168: }
169: }
170:
171: private SerialContext writeOpeningTag(Object child,
172: String childtagname, SAXAccessMethod topgetmethod) {
173: SerialContext top = null;
174: try {
175: SerialContext oldtop = getDeSAXingObject();
176: if (oldtop != null && !oldtop.writtenchild) {
177: xmlw.writeRaw(">");
178: if (indentlevel != COMPACT_MODE) {
179: xmlw.writeRaw("\n");
180: }
181: oldtop.writtenchild = true;
182: }
183:
184: xmlw.writeRaw("<" + childtagname, getIndent());
185: String genericdata = null;
186: if (child instanceof GenericSAX) {
187: GenericSAX generic = (GenericSAX) child;
188: SAXAccessMethodSpec[] getmethods = generic
189: .getSAXGetMethods();
190: // QQQQQ There may be getmethods defined by other means.
191: // Generic needs review!
192: if (generic.size() == 0
193: && (getmethods == null || getmethods.length == 0))
194: genericdata = generic.getData();
195: }
196: boolean isleaf = mappingcontext.saxleafparser
197: .isLeafType(child.getClass());
198: // TODO: It may be a leaf as a result of a parent class (ViewParameters)
199: top = isleaf ? null : new SerialContext(child,
200: mappingcontext);
201: // leaf is NOT DeSAXalizable OR we found some valid text
202: boolean closenow = (isleaf || genericdata != null);
203: Logger.println("Got generic data |" + genericdata + "|",
204: Logger.DEBUG_SUBATOMIC);
205:
206: if (closenow) {
207: // it is a leaf object
208: xmlw.writeRaw(">", 0);
209: // NB - 10/11/05 - final end of support for "Generic" objects. Check SVN
210: // should it ever need to come back.
211: // use leafparser to render it into text
212: if (genericdata == null) {
213: xmlw.write(mappingcontext.saxleafparser
214: .render(child));
215: }
216: // use writeRaw so that < is not deentitised
217: xmlw.writeRaw("</" + childtagname + ">\n", 0);
218: top = null;
219: } else { // it is not a leaf object. writing it will require another pass
220: // around
221: Logger.println("Pushed", Logger.DEBUG_EXTRA_INFO);
222: String polynick = mappingcontext.classnamemanager
223: .getClassName(child.getClass());
224: if (polynick != null && topgetmethod != null
225: && topgetmethod.ispolymorphic) {
226: appendAttr(Constants.TYPE_ATTRIBUTE_NAME, polynick);
227: }
228: SAMIterator getattrenum = top.ma.attrmethods
229: .getGetEnumeration();
230: if (getattrenum.valid()
231: || child instanceof SAXalizableExtraAttrs) {
232: Logger.println("Child has attributes",
233: Logger.DEBUG_SUBATOMIC);
234: // renderinto.clear();
235: renderAttrs(child, getattrenum);
236: // xmlw.write(renderinto.storage, renderinto.offset, renderinto.size);
237: }
238:
239: // xmlw.writeRaw(">\n", 0);
240: desaxingobjects.add(top);
241: } // else not a leaf object
242: } catch (Throwable t) {
243: throw UniversalRuntimeException.accumulate(t,
244: "Error while writing tag " + childtagname);
245: }
246: return top;
247: }
248:
249: /**
250: * A value for indentlevel that will suppress writing of the XML declaration.
251: */
252: public static final int COMPACT_MODE = -1;
253:
254: /**
255: * Write a serialized representation of an object subtree as XML to an output
256: * stream. This method DOES NOT close the supplied output stream.
257: *
258: * @param root The root object of the tree to be serialized. Must implement
259: * the <code>DeSAXalizable</code> interface, as must all
260: * descendents, or have types registered with the
261: * <code>SAXLeafParser</code>.
262: * @param roottag The tag to be supplied to the root object (all subobjects
263: * have their tags supplied through the <code>DeSAXalizable</code>
264: * interface.
265: * @param os The output stream to receive the serialized data. The data will
266: * be written in UTF-8 format.
267: * @param forbidder An interface through which clients may countermand the
268: * serialization of a particular subobject of the root. Each object
269: * and tag will be supplied to this interface before serialization.
270: * <code>null</code> if all objects to be serialized without
271: * restriction.
272: * @param maxdepth If this is not -1, subobjects below this depth from the
273: * root will not be serialized.
274: * @param indentlevel The initial indent level in the XML output file to be
275: * applied to the tag representing the root object. If this is not 0,
276: * an XML declaration will not be written to the file. If this is
277: * COMPACT_MODE, newlines and indentation will not be written.
278: */
279:
280: public void serializeSubtree(Object root, String roottagname,
281: OutputStream os, int indentlevel) {
282: // Store the stack of desaxing objects locally, to avoid having to allocate
283: // thread-local
284: // desaxalizers. This is a stack of SerialContexts.
285: desaxingobjects = new ArrayList();
286: this .indentlevel = indentlevel;
287:
288: try {
289: this .xmlw = new XMLWriter(os);
290: if (indentlevel == 0) {
291: xmlw.writeDeclaration();
292: }
293: SerialContext top = writeOpeningTag(root, roottagname, null);
294: SAXAccessMethod topgetmethod = null;
295:
296: // ONE ITERATION ROUND THE FOLLOWING LOOP CORRESPONDS TO ONE COMPLETE XML
297: // TAG
298: // at the top of this loop, the opening tag for the object on top of the
299: // stack has just been written.
300: Object child = null;
301: String childtagname = null;
302: while (true) {
303: // traverse downwards
304: if (Logger.passDebugLevel(Logger.DEBUG_EXTRA_INFO)) {
305: Logger.println("At top", Logger.DEBUG_EXTRA_INFO);
306: }
307: if (top.enumm == null && !top.getenum.valid()) {
308: // we have reached the last child node. pop the stack and write
309: // closing tag.
310: Logger.println("Popped", Logger.DEBUG_EXTRA_INFO);
311: boolean writtenchild = top.writtenchild;
312: ListUtil.pop(desaxingobjects);
313:
314: if (desaxingobjects.isEmpty())
315: break; // GLOBAL EXIT POINT
316: top = getDeSAXingObject();
317: xmlw.closeTag(top.stashedclosingtag, getIndent(),
318: writtenchild);
319: continue;
320: } // end if pop required
321:
322: // Step 2: get the child object that the index on top of stack refers to
323: child = null;
324: topgetmethod = top.getenum.get();
325: childtagname = topgetmethod.tagname;
326:
327: // so long as the top object is not in an enum, get the enclosed object
328: if (top.enumm == null) {
329: child = topgetmethod.getChildObject(top.object);
330:
331: Logger.println("Object " + child
332: + " delivered for tag "
333: + topgetmethod.tagname,
334: Logger.DEBUG_EXTRA_INFO);
335:
336: // if we discover it is an enum, begin chewing on it.
337: if (child != null && topgetmethod.ismultiple) {
338: top.enumm = EnumerationConverter
339: .getEnumeration(child);
340: } else {
341: top.getenum.next();
342: }
343: }
344: Logger.println("About to check enum: childtagname is "
345: + childtagname, Logger.DEBUG_EXTRA_INFO);
346: // if the top object IS within an enum, see whether it has another
347: // element
348: if (top.enumm != null) {
349: if (top.enumm.hasMoreElements()) {
350: child = top.enumm.nextElement();
351: // if it is a GenericSAX enum, the tagname must be initialised with
352: // the value reported by the GenericSAX
353: if (child instanceof GenericSAX) {
354: childtagname = ((GenericSAX) child)
355: .getTag();
356: }
357: // if it is a polymorphic enum, the tagname must be replaced with
358: // the actual object class
359: else if (topgetmethod.tagname.equals("*")) {
360: childtagname = child.getClass().getName();
361: }
362: // otherwise, the value set from
363: // top.getmethods[top.currentgetindex].tagname is correct
364: Logger
365: .println("Got next child " + child
366: + " from enum",
367: Logger.DEBUG_EXTRA_INFO);
368: } else { // the enum is finished, leave child null so that following
369: // branch will pass on
370: Logger.println("enum finished",
371: Logger.DEBUG_EXTRA_INFO);
372: top.enumm = null;
373: top.getenum.next();
374: child = null; // this line deals with the case of 0-element
375: // enumerations
376: }
377: }
378: // Step 3: We have made all attempts to get a child object on this
379: // iteration - child will be null if there is simply no object to be
380: // delivered or an enum just finished. Otherwise, we have a child
381: // object and so write out its opening tag.
382: // If the child is a leaf object, we also write out the closing tag
383: // here.
384: if (child != null) {
385: top.stashedclosingtag = childtagname;
386: Logger.println("Tag name determined to be "
387: + childtagname, Logger.DEBUG_EXTRA_INFO);
388: if ((forbidder == null || forbidder
389: .permitSerialization(childtagname, child))
390: // maxdepth parameter removed - forbidders should do everything AMB
391: // 1/10/04
392: // && (maxdepth == -1 || desaxingobjects.size() <= maxdepth)
393: ) {
394: // Once we have got the child object, we can write the opening tag
395: // name. If it is a leaf, write closing tag as well.
396: SerialContext returnedtop = writeOpeningTag(
397: child, childtagname, topgetmethod);
398: if (returnedtop != null)
399: top = returnedtop;
400: // prettySpaces(desaxingobjects.size(), w);
401: } // end if the writing was not forbidden
402: else
403: Logger.println("Tag writing forbidden",
404: Logger.DEBUG_EXTRA_INFO);
405: } // end if there was a child to write
406: } // end enormous loop
407:
408: xmlw.closeTag(roottagname, indentlevel, top.writtenchild);
409: } finally {
410: if (xmlw != null) {
411: xmlw.close();
412: }
413: blastState();
414: }
415: } // end method serializeSubtree
416: }
|