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);
}
}