/*
* JBoss, Home of Professional Open Source
* Copyright 2013, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.richfaces.json;
import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import org.ajax4jsf.Messages;
import org.ajax4jsf.javascript.JSEncoder;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
/**
* @author shura SAX content handler for serialise events as JavaScript function.
*/
public class JSContentHandler implements ContentHandler, LexicalHandler {
public static final String DEFAULT_ENCODING = "ISO-8859-1";
private static final char C_COMMA = ',';
private static final char C_GT = '>';
private static final char C_LT = '<';
private static final char C_NSSEP = ':';
private static final char C_QUOTE = '\'';
private static final char C_SPACE = ' ';
private static final JSEncoder ENCODER = new JSEncoder();
private static final char[] S_TEXT_START = "new T(".toCharArray();
private static final char[] S_TEXT_END = ")".toCharArray();
private static final char[] S_PROCINSTR_START = "<?".toCharArray();
private static final char[] S_PROCINSTR_END = "?>".toCharArray();
private static final char[] S_OBJECT_START = "{".toCharArray();
private static final char[] S_OBJECT_END = "}".toCharArray();
private static final char[] S_EOL = System.getProperty("line.separator").toCharArray();
private static final char[] S_ELEMENT_START = "new E(".toCharArray();
private static final char[] S_ELEMENT_END_START_TAG = ",[".toCharArray();
private static final char[] S_ELEMENT_END = "])".toCharArray();
private static final char[] S_ELEMENT_CLOSE = ")".toCharArray();
private static final char[] S_DOCUMENT_START = "(".toCharArray();
private static final char[] S_DOCUMENT_ENF = ");".toCharArray();
private static final char[] S_DOCUMENT_3 = "\"?>".toCharArray();
private static final char[] S_COMMENT_START = "new C('".toCharArray();
private static final char[] S_COMMENT_END = "')".toCharArray();
private static final char[] S_CDATA_START = "new D('".toCharArray();
private static final char[] S_CDATA_END = "')".toCharArray();
private static final char[] S_ATTRIBUTES_START = ",{".toCharArray();
private static final char[] S_ATTRIBUTES_END = "}".toCharArray();
private static final boolean DEBUG = false;
// protected DocType doctype = null;
protected char[] indentBuffer;
protected int level;
protected Writer outputWriter;
private boolean hangingElement = false;
/**
* True if we are processing the prolog.
*/
private boolean beforeDocumentStart = true;
/**
* True if we are processing the DTD.
*/
private boolean processingDtd = false;
/**
* True if we are processing the DTD.
*/
private boolean processingCdata = false;
/* ====================================================================== */
/**
* The <code>DocType</code> instance representing the document.
*/
private Locator locator;
public JSContentHandler() {
}
/**
* @param outputWriter
*/
public JSContentHandler(Writer outputWriter) {
this.outputWriter = outputWriter;
}
/**
* @throws java.io.IOException
*/
public void close() throws IOException {
outputWriter.close();
}
// ContentHandler Methods
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#characters(char[], int, int)
*/
public void characters(char[] ch, int start, int length) throws SAXException {
if (this.beforeDocumentStart || (level < 0)) {
return;
}
try {
if ((level != 0) && !this.closeElement(false) && !this.processingCdata) {
this.outputWriter.write(C_COMMA);
}
if (DEBUG) {
this.outputWriter.write('[');
this.outputWriter.write(Integer.toString(level));
this.outputWriter.write(']');
}
if (!this.processingCdata) {
this.outputWriter.write(S_TEXT_START);
}
this.encodeText(ch, start, length);
if (!this.processingCdata) {
this.outputWriter.write(S_TEXT_END);
}
} catch (IOException e) {
throw new SAXException("Write error", e);
}
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#endDocument()
*/
public void endDocument() throws SAXException {
this.beforeDocumentStart = true;
if (this.level != 0) {
throw new SAXException(Messages.getMessage(Messages.OPEN_CLOSE_TAGS_DO_NOT_MATCH_ERROR));
}
// Write parameters after parsing and final function )
try {
this.outputWriter.write(S_EOL);
this.outputWriter.flush();
} catch (IOException e) {
throw new SAXException("Write error", e);
}
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
*/
public void endElement(String uri, String localName, String qName) throws SAXException {
this.level--;
if (closeElement(true)) {
return;
}
try {
this.outputWriter.write(S_ELEMENT_END); // [</]
if (DEBUG) {
this.outputWriter.write('[');
this.outputWriter.write(qName);
this.outputWriter.write('-');
this.outputWriter.write(uri);
this.outputWriter.write(']');
}
} catch (IOException e) {
throw new SAXException("Write error", e);
}
// this.outputWriter.write(qual);
// this.outputWriter.write(C_GT); // [>]
}
/**
* Write the end part of a start element (if necessary).
*
* @param endElement Whether this method was called because an element is being closed or not.
* @return <b>true </b> if this call successfully closed the element (and no further <code></element></code> is
* required.
*/
protected boolean closeElement(boolean endElement) throws SAXException {
if (!hangingElement) {
return false;
}
try {
if (endElement) {
this.outputWriter.write(S_ELEMENT_CLOSE); // [ />]
} else {
this.outputWriter.write(S_ELEMENT_END_START_TAG); // [>]
}
} catch (IOException e) {
throw new SAXException("Write error", e);
}
this.hangingElement = false;
return true;
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String)
*/
public void endPrefixMapping(String prefix) throws SAXException {
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int)
*/
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
// TODO Auto-generated method stub
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String)
*/
public void processingInstruction(String target, String data) throws SAXException {
// TODO Auto-generated method stub
}
/* ====================================================================== */
/**
* Receive an object for locating the origin of SAX document events.
*/
public final void setDocumentLocator(Locator locator) {
this.locator = locator;
}
/**
* Return the public identifier for the current document event.
*
* @return A <code>String</code> containing the public identifier, or <b>null</b> if none is available.
*/
public String getPublicId() {
return (this.locator == null) ? null : this.locator.getPublicId();
}
/**
* Return the system identifier for the current document event.
*
* @return A <code>String</code> containing the system identifier, or <b>null</b> if none is available.
*/
public String getSystemId() {
return (this.locator == null) ? null : this.locator.getSystemId();
}
/**
* Return the line number where the current document event ends.
*
* @return The line number, or -1 if none is available.
*/
public int getLineNumber() {
return (this.locator == null) ? -1 : this.locator.getLineNumber();
}
/**
* Return the column number where the current document event ends.
*
* @return The column number, or -1 if none is available.
*/
public int getColumnNumber() {
return (this.locator == null) ? -1 : this.locator.getColumnNumber();
}
/**
* Return a <code>String</code> describing the current location.
*/
protected String getLocation() {
if (this.locator == null) {
return "";
}
StringBuffer buf = new StringBuffer(" (");
if (this.getSystemId() != null) {
buf.append(this.getSystemId());
buf.append(' ');
}
buf.append("line " + this.getLineNumber());
buf.append(" col " + this.getColumnNumber());
buf.append(')');
return buf.toString();
}
/* ====================================================================== */
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#skippedEntity(java.lang.String)
*/
public void skippedEntity(String name) throws SAXException {
// TODO Auto-generated method stub
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#startDocument()
*/
public void startDocument() throws SAXException {
this.beforeDocumentStart = false;
this.processingCdata = false;
this.level = 0;
/* We have a document type. */
// if (this.doctype != null) {
//
// String root_name = this.doctype.getName();
// /* Check the DTD and the root element */
// if (!root_name.equals(qual)) {
// throw new SAXException("Root element name \"" + root_name
// + "\" declared by document type declaration differs "
// + "from actual root element name \"" + qual + "\"");
// }
/* Output the <!DOCTYPE ...> declaration. */
// this.outputWriter.write(this.doctype.toString());
// }
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String,
* org.xml.sax.Attributes)
*/
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
try {
if (!this.closeElement(false) && (this.level > 0)) {
this.outputWriter.write(C_COMMA);
}
this.outputWriter.write(S_ELEMENT_START); // [<]
if (DEBUG) {
this.outputWriter.write('[');
this.outputWriter.write(uri);
this.outputWriter.write(']');
}
this.outputWriter.write(C_QUOTE);
this.outputWriter.write(qName);
this.outputWriter.write(C_QUOTE);
// if (attributes.getLength() > 0) {
this.outputWriter.write(S_ATTRIBUTES_START);
// TODO - implementing namespaces !
// for (int x = 0; x < namespaces.length; x++) {
// this.outputWriter.write(S_ELEMENT_4); // [ xmlns]
// if (namespaces[x][Namespaces.NAMESPACE_PREFIX].length() > 0) {
// this.outputWriter.write(C_NSSEP); // [:]
// this.outputWriter.write(namespaces[x][Namespaces.NAMESPACE_PREFIX]);
// }
// this.outputWriter.write(S_ELEMENT_1); // [="]
// this.encode(namespaces[x][Namespaces.NAMESPACE_URI]);
// this.outputWriter.write(C_QUOTE); // ["]
// }
for (int x = 0; x < attributes.getLength(); x++) {
if (0 != x) {
this.outputWriter.write(C_COMMA);
this.outputWriter.write(C_SPACE); // [ ]
}
if (DEBUG) {
this.outputWriter.write('[');
this.outputWriter.write(attributes.getURI(x));
this.outputWriter.write(']');
}
String attrName = attributes.getQName(x);
// For JavaScript any attributes names illegal ...
// replate with correct names.
if (attrName.equalsIgnoreCase("class")) {
attrName = "className";
}
this.outputWriter.write(C_QUOTE); // [']
this.outputWriter.write(attrName);
this.outputWriter.write(C_QUOTE); // [']
this.outputWriter.write(C_NSSEP); // [:]
// TODO by nick - fix namespace handling - they shouldn't be encoded
this.encodeAttributeValue(attributes, x);
}
this.outputWriter.write(S_ATTRIBUTES_END);
// }
this.level++;
this.hangingElement = true;
} catch (IOException e) {
throw new SAXException("write error", e);
}
}
protected void encodeAttributeValue(Attributes attributes, int idx) throws SAXException, IOException {
this.outputWriter.write(C_QUOTE); // [']
this.encode(attributes.getValue(idx));
this.outputWriter.write(C_QUOTE); // [']
}
protected void encodeText(char[] chars, int start, int length) throws SAXException, IOException {
this.encode(chars, start, length);
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String)
*/
public void startPrefixMapping(String prefix, String uri) throws SAXException {
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ext.LexicalHandler#comment(char[], int, int)
*/
public void comment(char[] ch, int start, int length) throws SAXException {
if (this.beforeDocumentStart || (level < 0)) {
return;
}
try {
if (!this.closeElement(false) && (level != 0)) {
this.outputWriter.write(C_COMMA);
}
if (DEBUG) {
this.outputWriter.write('[');
this.outputWriter.write(Integer.toString(level));
this.outputWriter.write(']');
}
// this.outputWriter.write(C_QUOTE);
this.outputWriter.write(S_COMMENT_START);
this.encode(ch, start, length);
this.outputWriter.write(S_COMMENT_END);
// this.outputWriter.write(C_QUOTE);
} catch (IOException e) {
throw new SAXException("Write error", e);
}
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ext.LexicalHandler#endCDATA()
*/
public void endCDATA() throws SAXException {
if (this.beforeDocumentStart || (level < 0)) {
return;
}
try {
if (this.closeElement(false)) {
return;
}
if (DEBUG) {
this.outputWriter.write('[');
this.outputWriter.write(Integer.toString(level));
this.outputWriter.write(']');
}
this.outputWriter.write(S_CDATA_END);
this.processingCdata = false;
} catch (IOException e) {
throw new SAXException("Write error", e);
}
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ext.LexicalHandler#endDTD()
*/
public void endDTD() throws SAXException {
// TODO Auto-generated method stub
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ext.LexicalHandler#endEntity(java.lang.String)
*/
public void endEntity(String name) throws SAXException {
// TODO Auto-generated method stub
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ext.LexicalHandler#startCDATA()
*/
public void startCDATA() throws SAXException {
if (this.beforeDocumentStart || (level < 0)) {
return;
}
try {
if (!this.closeElement(false) && (level != 0)) {
this.outputWriter.write(C_COMMA);
}
if (DEBUG) {
this.outputWriter.write('[');
this.outputWriter.write(Integer.toString(level));
this.outputWriter.write(']');
}
this.outputWriter.write(S_CDATA_START);
this.processingCdata = true;
} catch (IOException e) {
throw new SAXException("Write error", e);
}
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ext.LexicalHandler#startDTD(java.lang.String, java.lang.String, java.lang.String)
*/
public void startDTD(String name, String publicId, String systemId) throws SAXException {
// TODO Auto-generated method stub
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ext.LexicalHandler#startEntity(java.lang.String)
*/
public void startEntity(String name) throws SAXException {
// TODO Auto-generated method stub
}
/*
* (non-Javadoc)
*
* @see org.apache.cocoon.components.serializers.EncodingSerializer#writeIndent(int)
*/
protected void writeIndent(int indent) throws SAXException {
try {
this.outputWriter.write("\n".toCharArray(), 0, 1);
if (indent > 0) {
this.outputWriter.write(assureIndentBuffer(indent), 0, indent);
}
} catch (IOException e) {
throw new SAXException("Write error", e);
}
}
private char[] assureIndentBuffer(int size) {
if ((indentBuffer == null) || (indentBuffer.length < size)) {
indentBuffer = new char[size];
Arrays.fill(indentBuffer, ' ');
}
return indentBuffer;
}
/**
* Encode and write a <code>String</code>
*/
protected void encode(String data) throws SAXException {
char[] array = data.toCharArray();
this.encode(array, 0, array.length);
}
/**
* Encode and write an array of characters.
*/
protected void encode(char[] data) throws SAXException {
this.encode(data, 0, data.length);
}
/**
* Encode and write a specific part of an array of characters.
*/
protected void encode(char[] data, int start, int length) throws SAXException {
int end = start + length;
if (data == null) {
throw new NullPointerException("Null data");
}
if ((start < 0) || (start > data.length) || (length < 0) || (end > data.length) || (end < 0)) {
throw new IndexOutOfBoundsException("Invalid data");
}
if (length == 0) {
return;
}
try {
for (int x = start; x < end; x++) {
char c = data[x];
if (JSContentHandler.ENCODER.compile(c)) {
continue;
}
if (start != x) {
this.outputWriter.write(data, start, x - start);
}
this.outputWriter.write(JSContentHandler.ENCODER.encode(c));
start = x + 1;
continue;
}
if (start != end) {
this.outputWriter.write(data, start, end - start);
}
} catch (IOException e) {
throw new SAXException("Write error", e);
}
}
protected boolean isProcessingCdata() {
return processingCdata;
}
protected boolean isBeforeDocumentStart() {
return beforeDocumentStart;
}
}