001: /* ****************************************************************************
002: * DataEncoder.java
003: *
004: * Compile XML directly to SWF bytecodes.
005: * ****************************************************************************/
006:
007: /* J_LZ_COPYRIGHT_BEGIN *******************************************************
008: * Copyright 2001-2004 Laszlo Systems, Inc. All Rights Reserved. *
009: * Use is subject to license terms. *
010: * J_LZ_COPYRIGHT_END *********************************************************/
011:
012: package org.openlaszlo.xml;
013:
014: import java.io.*;
015: import java.util.*;
016:
017: import org.xml.sax.*;
018:
019: import org.openlaszlo.xml.internal.*;
020: import org.openlaszlo.iv.flash.util.*;
021: import org.openlaszlo.iv.flash.api.action.*;
022: import org.openlaszlo.iv.flash.api.*;
023: import org.openlaszlo.compiler.CompilationError;
024: import org.openlaszlo.utils.ChainedException;
025: import org.openlaszlo.utils.FileUtils;
026: import org.openlaszlo.utils.HashIntTable;
027:
028: import org.jdom.input.SAXBuilder;
029: import org.jdom.output.SAXOutputter;
030: import org.jdom.JDOMException;
031: import org.jdom.Document;
032: import org.jdom.Attribute;
033: import org.jdom.Element;
034: import org.xml.sax.Attributes;
035: import org.xml.sax.helpers.AttributesImpl;
036:
037: import org.apache.log4j.*;
038:
039: /**
040: * API to compile XML-like data sets to SWF bytecode format.
041: *<p>
042: * This class implements the SAX ContentHandler API, and could possibly be used
043: * directly with a SAX Parser, but is designed to be used manually as shown below.
044: * <p>
045: * Example usage:
046: * <pre>
047: * import org.xml.sax.helpers.AttributesImpl;
048: *
049: * // send simple row data as node with attributes
050: * InputStream getDataInputStream(Request req, Response resp) {
051: * Connection conn = DriverManager.getConnection(ConnectionPoolManager.URL_PREFIX + alias, null, null);
052: * Statement stmt = conn.createStatement();
053: * ResultSet resultset = stmt.executeQuery("SELECT NAME, AGE, ID FROM EMPLOYEES");
054: * AttributesImpl emptyAttr = new AttributesImpl();
055: *
056: *
057: * DataEncoder db = new DataEncoder();
058: * db.startDocument();
059: * db.startElement("results", emptyAttr);
060: *
061: * while (resultset.next()) {
062: * AttributesImpl attrs = new AttributesImpl();
063: * //Output the values as attributes
064: * attrs.addAttribute("", "name", "", "CDATA", resultset.getString("name"));
065: * attrs.addAttribute("", "age", "", "CDATA", resultset.getString("age"));
066: * attrs.addAttribute("", "id", "", "CDATA", resultset.getString("id"));
067: * db.startElement("row", attrs);
068: * db.endElement();
069: * }
070: * db.endElement();
071: * db.endDocument();
072: *
073: * OutputStream out = resp.getOutputStream();
074: * return db.getInputStream();
075: * }
076: * </pre>
077: */
078: public class DataEncoder implements org.xml.sax.ContentHandler {
079:
080: /* Logger */
081: private static Logger mLogger = Logger.getLogger(DataEncoder.class);
082:
083: /** Hint to allocate buffer size large enough to hold output. */
084: private int initsize = 0;
085: private static final int DEFAULT_BUFFER_SIZE = 4096;
086:
087: /**
088: * The SWF file
089: */
090: private FlashOutput mSWF = null;
091: /**
092: * Size of the SWF file.
093: */
094: private long mSize = -1;
095:
096: // Target version of Flash to compile to
097: int mFlashVersion = 6;
098:
099: /**
100: * Constructs an empty DataEncoder.
101: */
102: public DataEncoder() {
103: }
104:
105: /**
106: * Constructs a DataEncoder with a buffer allocation size hint.
107: * @param initsize hint to allocate buffer size large enough to hold output.
108: */
109: public DataEncoder(int initsize) {
110: this .initsize = initsize;
111: }
112:
113: //============================================================
114: // SAX API
115: //============================================================
116:
117: /**
118: * Receive notification of character data.
119: *
120: * @param ch the characters from the XML document.
121: * @param start the start position in the array.
122: * @param length the number of characters to read from the array.
123: *
124: * @see #characters(String) characters(String)
125: */
126: public void characters(char[] ch, int start, int length) {
127: String text = new String(ch, start, length);
128: characters(text);
129: }
130:
131: /**
132: * Receive notification of string data.
133: *
134: * @param text the string from the XML document.
135: *
136: * @see #characters(char[], int, int) characters(char[], int, int)
137: */
138: public void characters(String text) {
139: // makeTextNode = function (text, parent)
140: // dup pointer to parent (who is at top of stack)
141: // DUP
142: body.writeByte(Actions.PushDuplicate);
143: // Push text
144:
145: body.writeByte(Actions.PushData);
146: // Leave a two byte space for the PUSH length
147: // Mark where we are, so we can rewrite this with correct length later.
148: int push_bufferpos = body.getPos();
149: body.writeWord(0); // placeholder 16-bit length field
150:
151: DataCommon.pushMergedStringData(text, body, dc);
152: // Set up argsnum and function name
153: // PUSH 2, _tdn
154: body.writeByte(0x07); // INT type
155: body.writeDWord(2); // '2' integer constant ; number of args to function
156: body.writeByte(0x08); // SHORT DICTIONARY LOOKUP
157: body.writeByte(textnode_idx); // push function name: index of "_t" string constant
158:
159: // Now go back and fix up the size arg to the PUSH instruction
160: int total_size = body.getPos() - (push_bufferpos + 2);
161: //System.out.println("pos="+body.getPos()+ " total_size = "+total_size+" push_bufferpos="+push_bufferpos+" nattrs="+nattrs);
162: body.writeWordAt(total_size, push_bufferpos);
163:
164: body.writeByte(Actions.CallFunction);
165: // Pop the node, because there will be no end tag for it, and it has no children.
166: body.writeByte(Actions.Pop);
167: }
168:
169: /**
170: * Receive notification of the end of an element. This method is equivalent
171: * to calling {@link #endElement() endElement()} -- the input parameters are
172: * ignored.
173: *
174: * @param uri the Namespace URI, or the empty string if the element has no
175: * Namespace URI or if Namespace processing is not being performed.
176: * @param localName the local name (without prefix), or the empty string if
177: * Namespace processing is not being performed.
178: * @param qName the qualified XML 1.0 name (with prefix), or the empty
179: * string if qualified names are not available.
180: * @see #endElement() endElement()
181: */
182: public void endElement(java.lang.String uri,
183: java.lang.String localName, java.lang.String qName) {
184: // Pop the node off the stack.
185: body.writeByte(Actions.Pop);
186: }
187:
188: /**
189: * Receive notification of the end of an element.
190: *
191: * @see #endElement(String,String,String) endElement(String,String,String)
192: */
193: public void endElement() {
194: // Pop the node off the stack.
195: body.writeByte(Actions.Pop);
196: }
197:
198: /**
199: * End the scope of a prefix-URI mapping. This method is unimplemented.
200: *
201: * @param prefix the prefix that was being mapped.
202: */
203: public void endPrefixMapping(java.lang.String prefix) {
204: }
205:
206: /**
207: * Receive notification of ignorable whitespace in element content. This
208: * method is unimplemented.
209: *
210: * @param ch the characters from the XML document.
211: * @param start the start position in the array.
212: * @param length the number of characters to read from the array.
213: */
214: public void ignorableWhitespace(char[] ch, int start, int length) {
215: }
216:
217: /**
218: * Receive notification of a processing instruction. This method is
219: * unimplemented.
220: *
221: * @param target the processing instruction target.
222: * @param data the processing instruction data, or null if none was
223: * supplied. The data does not include any whitespace separating it from the
224: * target.
225: */
226: public void processingInstruction(java.lang.String target,
227: java.lang.String data) {
228: }
229:
230: /**
231: * Receive an object for locating the origin of SAX document events. This
232: * method is unimplemented.
233: *
234: * @param locator an object that can return the location of any SAX document
235: * event.
236: */
237: public void setDocumentLocator(Locator locator) {
238: }
239:
240: /**
241: * Receive notification of a skipped entity. This method is unimplemented.
242: *
243: * @param name the name of the skipped entity. If it is a parameter entity,
244: * the name will begin with '%', and if it is the external DTD subset, it
245: * will be the string "[dtd]".
246: */
247: public void skippedEntity(java.lang.String name) {
248: }
249:
250: /** State vars */
251: DataContext dc;
252: // The node constructor function name.
253: private byte constructor_idx;
254: private byte textnode_idx;
255:
256: FlashBuffer body;
257: Program program;
258: Program resultProgram;
259: FlashBuffer out;
260:
261: /**
262: * Receive notification of the beginning of a document.
263: *
264: * @see #endDocument() endDocument()
265: */
266: public void startDocument() {
267: dc = new DataContext(mFlashVersion);
268: if (mFlashVersion == 5) {
269: dc.setEncoding("Cp1252");
270: } else {
271: dc.setEncoding("UTF-8");
272: }
273: constructor_idx = (byte) (DataCommon.addStringConstant(
274: DataCommon.NODE_INSTANTIATOR_FN, dc) & 0xFF);
275: textnode_idx = (byte) (DataCommon.addStringConstant(
276: DataCommon.TEXT_INSTANTIATOR_FN, dc) & 0xFF);
277:
278: // Room at the end of the buffer for maybe some callback code to the runtime to say we're done.
279: // Allocate enough room to hold the data nodes and strings ; it should be < input XML filesize
280: body = new FlashBuffer(initsize == 0 ? DEFAULT_BUFFER_SIZE
281: : initsize);
282: program = new Program(body);
283:
284: // Bind the node creation functions to some short local names:
285: // element nodes: _root._m => _m
286: program.push(new Object[] { "_m", "_root" });
287: program.getVar();
288: program.push("_m");
289: body.writeByte(Actions.GetMember);
290: program.setVar();
291:
292: // text nodes: _root._t => _t
293: program.push(new Object[] { "_t", "_root" });
294: program.getVar();
295: program.push("_t");
296: body.writeByte(Actions.GetMember);
297: program.setVar();
298:
299: // Build a root node by calling the runtime's root node instantiator
300: //
301: // The root node will have $n = 0, and whatever other initial conditions are needed.
302: program.push(0); // Root node creator function takes no args.
303: program.push("_root");
304: program.getVar();
305: program.push(DataCommon.ROOT_NODE_INSTANTIATOR_FN);
306: program.callMethod();
307: // The root node is now on the stack.
308: // Build data. Invariant is that it leaves the stack the way it found it.
309:
310: AttributesImpl emptyAttr = new AttributesImpl();
311: startElement("resultset", emptyAttr);
312: startElement("body", emptyAttr);
313: }
314:
315: /**
316: * Receive notification of the end of a document.
317: *
318: * @see #startDocument() startDocument()
319: */
320: public void endDocument() {
321:
322: endElement();
323: // NOTE: [2003-07-17 bloch] This is where headers/meta-data would be inserted.
324: endElement();
325:
326: // The root node is sitting on the stack.
327: // Finalize; bind the variable "root" to the node that the lfc expects.
328: program.push(1);
329: program.push("_root");
330: program.getVar();
331: program.push(DataCommon.ROOT_NODE_FINAL_FN);
332: program.callMethod();
333:
334: FlashBuffer body = program.body();
335:
336: // Bind "root" global to the top node
337: body.writeByte(Actions.PushDuplicate);
338: program.push("__lztmproot");
339: body.writeByte(Actions.StackSwap);
340: program.setVar();
341:
342: // Call into viewsystem
343: program.push("_parent");
344: program.getVar();
345: program.push(2);
346: program.push("_parent");
347: program.getVar();
348: program.push("loader");
349: body.writeByte(Actions.GetMember);
350: program.push("returnData");
351: program.callMethod();
352: program.pop();
353:
354: // Collect the string dictionary data
355: byte pooldata[] = DataCommon.makeStringPool(dc);
356: // 'out' is the main FlashBuffer for composing the output file
357: final int MISC = 64;
358: out = new FlashBuffer(body.getSize() + pooldata.length + MISC);
359: // Write out string constant pool
360: out.writeByte(Actions.ConstantPool);
361: out.writeWord(pooldata.length + 2); // number of bytes in pool data + int (# strings)
362: out.writeWord(dc.cpool.size()); // number of strings in pool
363: out.writeArray(pooldata, 0, pooldata.length); // copy the data
364: // Write out the code to build nodes
365: out.writeArray(body.getBuf(), 0, body.getSize());
366: resultProgram = new Program(out);
367: }
368:
369: /**
370: * @return the Flash program to build the data set.
371: */
372: private Program getProgram() {
373: return resultProgram;
374: }
375:
376: /**
377: * Make SWF
378: *
379: * @return FlashFile containing entire SWF with header
380: */
381: private FlashFile makeSWFFile() {
382: // Create FlashFile object nd include action bytes
383: FlashFile file = FlashFile.newFlashFile();
384: Script s = new Script(1);
385: file.setMainScript(s);
386: file.setVersion(5);
387: Frame frame = s.newFrame();
388: frame.addFlashObject(new DoAction(resultProgram));
389: return file;
390: }
391:
392: /*
393: public int writeSWF(OutputStream w, boolean closeStream)
394: throws IOException {
395: // Get inputstream and write to outputstream
396: InputStream input;
397: int i;
398: try {
399: FlashFile file = makeSWFFile();
400: input = file.generate().getInputStream();
401: i = FileUtils.send(input, w);
402: w.flush();
403: } catch (IVException ex) {
404: throw new ChainedException(ex);
405: } finally {
406: if (closeStream) {
407: w.close();
408: }
409: }
410: return i;
411: }
412: */
413:
414: /**
415: * Get the compiled data swf program byte codes. Only call this after you
416: * have called {@link #endDocument endDocument()}.
417: *
418: * @return input stream containing the compiled SWF data program; only valid
419: * after {@link #endDocument endDocument()} has been called. Must not be called
420: * before {@link #endDocument endDocument()}.
421: */
422: public InputStream getInputStream() throws IOException {
423:
424: generate();
425: return mSWF.getInputStream();
426: }
427:
428: /**
429: * Return the size of the output object; only valid after endDocument
430: * {@link #endDocument endDocument()} has been called. Must not be called
431: * before {@link #endDocument endDocument()}.
432: *
433: * @return long representing the size
434: */
435: public long getSize() throws IOException {
436:
437: generate();
438: return mSize;
439: }
440:
441: /**
442: * Generate the SWF file
443: */
444: private void generate() throws IOException {
445:
446: if (mSWF == null) {
447: try {
448: InputStream input;
449: FlashFile file = makeSWFFile();
450: mSWF = file.generate();
451: mSize = mSWF.pos;
452: } catch (IVException ex) {
453: throw new ChainedException(ex);
454: }
455: }
456: }
457:
458: /**
459: * A lower level call than startElement(); attributes must be supplied by
460: * individual calls to addAttribute(). This method is unimplemented.
461: * @param localName the element name.
462: */
463: public void _startElement(String localName) {
464:
465: }
466:
467: /**
468: * A low level call to add an attribute, must be preceded by call to
469: * _startElement() for a given element. This method is unimplemented.
470: */
471: public void addAttribute(String attrName, String attrVal) {
472:
473: }
474:
475: /**
476: * Receive notification of the beginning of an element. This method is
477: * equivalent to calling {@link #startElement(String, Attributes)
478: * startElement(String, Attributes)} -- the uri and qName parameters are
479: * ignored.
480: *
481: * @param uri the Namespace URI, or the empty string if the element has no
482: * Namespace URI or if Namespace processing is not being performed.
483: * @param localName the local name (without prefix), or the empty string if
484: * Namespace processing is not being performed.
485: * @param qName the qualified name (with prefix), or the empty string if
486: * qualified names are not available.
487: * @param atts the attributes attached to the element. If there are no
488: * attributes, it shall be an empty Attributes object.
489: *
490: * @see #startElement(String, Attributes) startElement(String, Attributes)
491: */
492: public void startElement(java.lang.String uri,
493: java.lang.String localName, java.lang.String qName,
494: Attributes atts) {
495: startElement(localName, atts);
496: }
497:
498: /**
499: * Receive notification of the beginning of an element.
500: *
501: * @param localName the local name (without prefix), or the empty string if
502: * Namespace processing is not being performed.
503: * @param atts the attributes attached to the element. If there are no
504: * attributes, it shall be an empty Attributes object.
505: *
506: * @see #startElement(String, String, String, Attributes)
507: * startElement(String, String, String, Attributes)
508: */
509: public void startElement(java.lang.String localName, Attributes atts) {
510: int idx; // tmp var to hold a string pool index
511: // makeNodeNoText = function (attrs, name, parent)
512: // dup pointer to PARENT (who is at top of stack)
513: // DUP
514: body.writeByte(Actions.PushDuplicate);
515:
516: // We're really squeezing things down, so we are going to merge
517: // the PUSH of the element name with the PUSH of the attribute key/value
518: // data and the attribute count. So the stack will look like
519: // [eltname attrname1 attrval1 attrname2 attrval2 ... ... nattrs]
520: // when we're done
521: body.writeByte(Actions.PushData);
522: // Leave a two byte space for the PUSH length
523: // Mark where we are, so we can rewrite this with correct length later.
524: int push_bufferpos = body.getPos();
525: body.writeWord(0); // placeholder 16-bit length field
526:
527: // Push element NAME
528: String eltname = localName;
529: DataCommon.pushMergedStringDataSymbol(eltname, body, dc);
530:
531: // Fold all the attribute key/value pairs into a single PUSH
532: // Build ATTRIBUTE object
533: int nattrs = atts.getLength();
534:
535: // PUSH := {0x96, lenlo, lenhi, 0x00, char, char, char, ...0x00, }
536: for (int i = 0; i < nattrs; i++) {
537: String attrname = atts.getLocalName(i);
538: //System.out.print("Attr " + attrname);
539: DataCommon.pushMergedStringDataSymbol(attrname, body, dc);
540:
541: String attrval = atts.getValue(i);
542: //System.out.println("= " + attrval);
543: DataCommon.pushMergedStringData(attrval, body, dc);
544: }
545: // create the attrs object; push the attr count
546: body.writeByte(0x07); // INT type
547: body.writeDWord(nattrs);
548: // Now go back and fix up the size arg to the PUSH instruction
549: int total_size = body.getPos() - (push_bufferpos + 2);
550: //System.out.println("pos="+body.getPos()+ " total_size = "+total_size+" push_bufferpos="+push_bufferpos+" nattrs="+nattrs);
551: body.writeWordAt(total_size, push_bufferpos);
552: body.writeByte(Actions.InitObject);
553:
554: // stack now has [parent, name, attrs]
555: // Push # of args and node-instantiator-function name
556: // PUSH 3, _mdn
557: // [PUSHDATA, 7, 0, 0x07, 03 00 00 00 0x08, constructor_idx]
558: body.writeByte(Actions.PushData);
559: body.writeWord(7);
560: body.writeByte(0x07); // INT type
561: body.writeDWord(3); // '3' integer constant , number of args to node constructor fn
562: body.writeByte(0x08); // SHORT DICTIONARY LOOKUP type
563: body.writeByte(constructor_idx); // index of "_m" string constant
564: body.writeByte(Actions.CallFunction);
565: // We leave the new node on the stack, so we can reference it as the parent
566: // Stack => [parentnode newnode]
567: }
568:
569: /**
570: * Begin the scope of a prefix-URI Namespace mapping. This method is
571: * unimplemented.
572: *
573: * @param prefix the Namespace prefix being declared.
574: * @param uri the Namespace URI the prefix is mapped to.
575: */
576: public void startPrefixMapping(java.lang.String prefix,
577: java.lang.String uri) {
578: }
579:
580: //============================================================
581: // End SAX ContentHandler Compatibility
582: //============================================================
583:
584: /**
585: * Parse a DOM Document and pass it to DataEncoder via JDOM SAXOutputter
586: *
587: * @param doc DOM Document to build.
588: * @throws DataCompilerException if there was a problem compiling the Document.
589: */
590: public void buildFromDocument(Document doc)
591: throws DataEncoderException {
592: try {
593: SAXOutputter saxout = new SAXOutputter(this );
594: saxout.output(doc);
595: } catch (JDOMException e) {
596: throw new DataEncoderException(
597: /* (non-Javadoc)
598: * @i18n.test
599: * @org-mes="Error compiling XML from Document: " + p[0]
600: */
601: org.openlaszlo.i18n.LaszloMessages.getMessage(
602: DataEncoder.class.getName(), "051018-604",
603: new Object[] { e.getMessage() }));
604: }
605: }
606:
607: /**
608: * Build from a DOM Element.
609: *
610: * @param e DOM Element.
611: */
612: public void buildFromElement(Element e) {
613: startDocument();
614: traverseDOM(e);
615: endDocument();
616: }
617:
618: String getQualifiedName(Attribute attr) {
619: // AttributesImpl expects a full qualified name or the empty string, if
620: // qualified name is not available.
621:
622: if (attr.getNamespacePrefix().equals(""))
623: return "";
624: return attr.getQualifiedName();
625: }
626:
627: String getAttributeType(Attribute attr) {
628: // Not really sure if returning an "ENUMERATED" string is the right
629: // thing to pass back for enumerated types.
630:
631: int type = attr.getAttributeType();
632: if (type == Attribute.CDATA_TYPE) {
633: return "CDATA";
634: } else if (type == Attribute.ID_TYPE) {
635: return "ID";
636: } else if (type == Attribute.IDREF_TYPE) {
637: return "IDREF";
638: } else if (type == Attribute.IDREFS_TYPE) {
639: return "IDREFS";
640: } else if (type == Attribute.ENTITY_TYPE) {
641: return "ENTITY";
642: } else if (type == Attribute.ENTITIES_TYPE) {
643: return "ENTITIES";
644: } else if (type == Attribute.NMTOKEN_TYPE) {
645: return "NMTOKEN";
646: } else if (type == Attribute.NMTOKENS_TYPE) {
647: return "NMTOKENS";
648: } else if (type == Attribute.NOTATION_TYPE) {
649: return "NOTATION";
650: } else if (type == Attribute.ENUMERATED_TYPE) {
651: return "ENUMERATED";
652: } else {
653: return "";
654: }
655:
656: }
657:
658: private void traverseDOM(Element el) {
659: List attrList = el.getAttributes();
660: AttributesImpl attrs = new AttributesImpl();
661: for (int i = 0; i < attrList.size(); i++) {
662: Attribute attr = (Attribute) attrList.get(i);
663: attrs.addAttribute(attr.getNamespaceURI(), attr.getName(),
664: getQualifiedName(attr), getAttributeType(attr),
665: attr.getValue());
666: }
667:
668: startElement(el.getName(), attrs);
669:
670: // Add text node
671: String text = el.getTextTrim();
672: if (text != null && text.length() != 0)
673: characters(text);
674:
675: List children = el.getChildren();
676: for (int i = 0; i < children.size(); i++)
677: traverseDOM((Element) children.get(i));
678:
679: endElement();
680:
681: }
682:
683: }
|