package org.selfhtml.xslt; import net.sf.saxon.event.Emitter; import net.sf.saxon.charcode.UnicodeCharacterSet; import net.sf.saxon.charcode.UTF16; import net.sf.saxon.om.FastStringBuffer; import net.sf.saxon.sort.IntHashMap; import net.sf.saxon.tinytree.CharSlice; import net.sf.saxon.tinytree.CompressedWhitespace; import net.sf.saxon.trans.XPathException; import net.sf.saxon.value.Whitespace; import java.util.Stack; import java.util.Vector; import java.util.Map; /** * JSONEmitter is an Emitter that generates JSON output * to a specified destination. * * This is based (w.r.t. the character output semantics) on * XMLEmitter from Saxon 9 which is available under the Mozilla * Public License (MPL) 1.0, see http://saxon.sf.net/ * * Since MPL source code is used inside this file, this file also * has to be released under the Mozilla Public License, Version 1.0, * see http://www.mozilla.org/MPL/MPL-1.0.html * * Please note that code using this class may be under any license * as long as the MPL is fulfilled for this source code file. */ public class JSONEmitter extends Emitter { // to be used later on public final static String NAMESPACE = "urn:json-output"; // different types of elements public final static int OBJECT = 0; public final static int ARRAY = 1; public final static int NUMBER = 2; public final static int STRING = 3; public final static int FALSE = 4; public final static int TRUE = 5; public final static int NULL = 6; public static class JSONElement { /** * The name of the tag. This may be "object", "array", etc. */ public String name; /** * The value of the key attribute. */ public String key = null; }; protected boolean empty = true; protected boolean startTagOpen = false; protected JSONElement currentElement = null; protected Stack objectStack = new Stack (); protected Stack isNotFirst = new Stack (); StringBuffer currentText; public void open() throws XPathException {} public void startDocument(int properties) throws XPathException {} public void endDocument() throws XPathException { if (!objectStack.isEmpty()) { throw new IllegalStateException("Attempt to end document in serializer when elements are unclosed"); } } public void startElement(int nameCode, int typeCode, int locationId, int properties) throws XPathException { if (empty) { openDocument (); } String name = namePool.getLocalName (nameCode); if (!name.equals ("object") && !name.equals ("array") && !name.equals ("number") && !name.equals ("string") && !name.equals ("false") && !name.equals ("true") && !name.equals ("null")) { throw new XPathException("Element name not one of object, array, number, string, false, true or null!"); } if (startTagOpen) { closeStartTag (currentElement, false); } if (objectStack.empty ()) { if (!name.equals ("array") && !name.equals ("object")) { throw new XPathException ("Only objects and arrays are allowed as top-level elements!"); } } else { int type = objectStack.peek ().intValue (); if (type != OBJECT && type != ARRAY) { throw new XPathException ("Only objects and arrays may contain subelements!"); } } startTagOpen = true; JSONElement e = new JSONElement (); e.name = name; currentElement = e; if (name.equals ("string") || name.equals ("number")) { currentText = new StringBuffer (); } } public void namespace(int namespaceCode, int properties) throws XPathException {} public void attribute(int nameCode, int typeCode, CharSequence value, int locationId, int properties) throws XPathException { String name = namePool.getLocalName (nameCode); if (!name.equals ("key")) { throw new XPathException ("Only key is allowed as a possible attribute!"); } if (objectStack.peek ().intValue () != OBJECT) { throw new XPathException ("key attribute is only allowed for object members!"); } currentElement.key = value.toString (); } public void startContent() throws XPathException {} public void endElement() throws XPathException { try { if (startTagOpen) { closeStartTag (currentElement, true); } else { int type = objectStack.pop ().intValue (); isNotFirst.pop (); switch (type) { case OBJECT: writer.write ('}'); break; case ARRAY: writer.write (']'); break; case NUMBER: writeNumber (currentText.toString ()); break; case STRING: writer.write ('"'); writeString (currentText); writer.write ('"'); break; // TRUE, FALSE und NULL treten nicht auf } } } catch (java.io.IOException err) { throw new XPathException (err); } } public void characters(CharSequence chars, int locationId, int properties) throws XPathException { if (empty) { openDocument (); } if (startTagOpen) { closeStartTag (currentElement, false); } // ignore whitespaces outside if (Whitespace.isWhite (chars) && (objectStack.empty () || objectStack.peek ().intValue () != STRING)) { return; } if (objectStack.empty ()) { throw new XPathException ("Character data is not allowed outside and !"); } int type = objectStack.peek ().intValue (); if (type == NUMBER || type == STRING) { currentText.append (chars); } else { throw new XPathException ("Character data is not allowed outside and !"); } } public void processingInstruction(String name, CharSequence data, int locationId, int properties) throws XPathException {} public void comment(CharSequence content, int locationId, int properties) throws XPathException {} public void close() throws XPathException { try { if (writer != null) { writer.flush (); } } catch (java.io.IOException err) { throw new XPathException (err); } } protected void openDocument () throws XPathException { if (writer == null) { makeWriter (); } if (characterSet == null) { characterSet = UnicodeCharacterSet.getInstance (); } } protected void closeStartTag (JSONElement e, boolean empty) throws XPathException { try { if (startTagOpen) { if (!isNotFirst.empty () && isNotFirst.peek ().intValue () == 1) { writer.write (','); } else if (!isNotFirst.empty ()) { isNotFirst.pop (); isNotFirst.push (new Integer (1)); } if (e.key != null) { // sanity checking is done before e.key is set writer.write ('"'); writeString (e.key); writer.write ("\":"); } else if (e.key == null && !objectStack.empty () && objectStack.peek ().intValue () == OBJECT) { throw new XPathException ("key attribute for object member not given!"); } startTagOpen = false; int type; if (e.name.equals ("object")) { writer.write ('{'); if (empty) { writer.write ('}'); return; } type = OBJECT; } else if (e.name.equals ("array")) { writer.write ('['); if (empty) { writer.write (']'); return; } type = ARRAY; } else if (e.name.equals ("number")) { if (empty) { writer.write ('0'); return; } type = NUMBER; } else if (e.name.equals ("string")) { if (empty) { writer.write ('"'); writer.write ('"'); return; } type = STRING; } else if (e.name.equals ("true")) { if (!empty) { throw new XPathException ("true, false and null elements must be empty!"); } writer.write ("true"); return; } else if (e.name.equals ("false")) { if (!empty) { throw new XPathException ("true, false and null elements must be empty!"); } writer.write ("false"); return; } else if (e.name.equals ("null")) { if (!empty) { throw new XPathException ("true, false and null elements must be empty!"); } writer.write ("true"); return; } else { throw new XPathException ("Element name not one of object, array, number, string, false, true or null!"); } objectStack.push (new Integer (type)); isNotFirst.push (new Integer (0)); } } catch (java.io.IOException err) { throw new XPathException (err); } } static boolean[] specialChars = new boolean[128]; static { // 0..31 are control characters (including LF, CR, ...) for (char i = 0; i <= 31; i++) specialChars[i] = true; specialChars['"'] = true; } /** * Write contents of string to current writer, after escaping special characters. * Taken from net/sf/saxon/event/XMLEmitter.java and modified to acommodate JS * characters. Note that this does not write the string delimiters! */ protected void writeString (final CharSequence chars) throws java.io.IOException, XPathException { int segstart = 0; boolean disabled = false; // CompressedWhitespace check removed since we don't want NCRs but // backslash + uXXXX for Javascript! final int clength = chars.length(); while (segstart < clength) { int i = segstart; // find a maximal sequence of "ordinary" characters while (i < clength) { final char c = chars.charAt(i); if (c < 127) { if (specialChars[c]) { break; } else { i++; } } else if (c < 160) { break; } else if (c == 0x2028) { break; } else if (UTF16.isHighSurrogate(c)) { break; } else if (!characterSet.inCharset(c)) { break; } else { i++; } } // if this was the whole string write it out and exit if (i >= clength) { if (segstart == 0) { writeCharSequence(chars); } else { writeCharSequence(chars.subSequence(segstart, i)); } return; } // otherwise write out this sequence if (i > segstart) { writeCharSequence(chars.subSequence(segstart, i)); } // examine the special character that interrupted the scan final char c = chars.charAt(i); if (c==0) { // used to switch escaping on and off disabled = !disabled; } else if (disabled) { if (c > 127) { if (UTF16.isHighSurrogate(c)) { int cc = UTF16.combinePair(c, chars.charAt(i+1)); if (!characterSet.inCharset(cc)) { XPathException de = new XPathException("Character x" + Integer.toHexString(cc) + " is not available in the chosen encoding"); de.setErrorCode("SERE0008"); throw de; } } else if (!characterSet.inCharset(c)) { XPathException de = new XPathException("Character " + c + " (x" + Integer.toHexString((int)c) + ") is not available in the chosen encoding"); de.setErrorCode("SERE0008"); throw de; } } writer.write(c); } else if (c>=127 && c<160) { // XML 1.1 requires these characters to be written as character references outputCharacterReference(c); } else if (c>=160) { if (c==0x2028) { outputCharacterReference(c); } else if (UTF16.isHighSurrogate(c)) { char d = chars.charAt(++i); int charval = UTF16.combinePair(c, d); if (characterSet.inCharset(charval)) { writer.write(c); writer.write(d); } else { outputCharacterReference(charval); } } else { // process characters not available in the current encoding outputCharacterReference(c); } } else { // process special ASCII characters if (c=='\"') { writer.write("\\\""); } else if (c=='\n') { writer.write("\\n"); } else if (c=='\r') { writer.write("\\r"); } else if (c=='\t') { writer.write("\\t"); } else { // C0 control characters outputCharacterReference(c); } } segstart = ++i; } } protected void writeNumber (String s) throws java.io.IOException { // make sure value's numeric String res; try { double d = Double.parseDouble (s); res = new Double (d).toString (); } catch (Exception e) { res = "0"; } writer.write (res); } /** * Write a CharSequence (without any escaping of special characters): various implementations. * Taken from net/sf/saxon/event/XMLEmitter.java. * @param s the character sequence to be written */ public void writeCharSequence (CharSequence s) throws java.io.IOException { if (s instanceof String) { writer.write ((String)s); } else if (s instanceof CharSlice) { ((CharSlice) s).write (writer); } else if (s instanceof FastStringBuffer) { ((FastStringBuffer) s).write (writer); } else if (s instanceof CompressedWhitespace) { ((CompressedWhitespace) s).write (writer); } else { writer.write (s.toString ()); } } private char[] charref = new char[10]; protected void outputCharacterReference(int charval) throws java.io.IOException, XPathException { int o = 0; charref[o++]='\\'; charref[o++]='u'; String code = Integer.toHexString(charval); int len = code.length(); if (len > 4) { throw new XPathException ("Surrogates were not separated. Should not happen"); } // fill with zeroes for (int k = 0; k < 4 - len; k++) { charref[o++] = '0'; } for (int k = 0; k < len; k++) { charref[o++] = code.charAt(k); } writer.write(charref, 0, o); } }