/*
* Copyright (c) 2007 Mozilla Foundation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package nu.validator.json;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.ArrayList;
import java.util.List;
import org.xml.sax.SAXException;
public class Serializer implements JsonHandler {
private enum State {
INITIAL, DOCUMENT, ARRAY, OBJECT, VALUE, STRING
}
private final List<State> stack = new ArrayList<>();
private boolean hadCallback = false;
private boolean first = false;
private final Writer writer;
private static Writer newOutputStreamWriter(OutputStream out) {
CharsetEncoder enc = Charset.forName("UTF-8").newEncoder();
enc.onMalformedInput(CodingErrorAction.REPLACE);
enc.onUnmappableCharacter(CodingErrorAction.REPLACE);
return new OutputStreamWriter(out, enc);
}
public Serializer(OutputStream out) {
this.writer = newOutputStreamWriter(out);
push(State.INITIAL);
}
private void push(State state) {
stack.add(state);
}
private void pop() {
stack.remove(stack.size() - 1);
}
private State peek() {
int size = stack.size();
if (size == 0) {
return null;
} else {
return stack.get(size - 1);
}
}
@Override
public void bool(boolean bool) throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
if (!first) {
writer.write(',');
}
// fall thru
case DOCUMENT:
case VALUE:
writer.write(Boolean.toString(bool));
if (state == State.VALUE) {
pop();
}
first = false;
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
private void charactersImpl(char[] ch, int start, int length)
throws IOException {
int s = start;
int end = start + length;
for (int i = start; i < end; i++) {
char c = ch[i];
if (c <= '\u001F' || c == '\"' || c == '\\') {
if (s < i) {
writer.write(ch, s, i - s);
}
s = i + 1;
writer.write('\\');
switch (c) {
case '\"':
writer.write('\"');
break;
case '\\':
writer.write('\\');
break;
case '\u0008':
writer.write('b');
break;
case '\u000C':
writer.write('f');
break;
case '\n':
writer.write('n');
break;
case '\r':
writer.write('r');
break;
case '\t':
writer.write('t');
break;
default:
String hex = Integer.toHexString(c);
if (hex.length() == 1) {
writer.write("u000");
writer.write(hex);
} else {
writer.write("u00");
writer.write(hex);
}
break;
}
}
}
if (s < end) {
writer.write(ch, s, end - s);
}
}
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
try {
State state = peek();
switch (state) {
case STRING:
charactersImpl(ch, start, length);
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void endArray() throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
writer.write(']');
pop();
first = false;
if (peek() == State.VALUE) {
pop();
}
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void endDocument() throws SAXException {
try {
State state = peek();
switch (state) {
case DOCUMENT:
if (hadCallback) {
writer.write(')');
}
writer.write('\n');
writer.flush();
writer.close();
pop();
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void endObject() throws SAXException {
try {
State state = peek();
switch (state) {
case OBJECT:
writer.write('}');
pop();
first = false;
if (peek() == State.VALUE) {
pop();
}
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void endString() throws SAXException {
try {
State state = peek();
switch (state) {
case STRING:
writer.write('\"');
pop();
first = false;
if (peek() == State.VALUE) {
pop();
}
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void key(String key) throws SAXException {
try {
State state = peek();
switch (state) {
case OBJECT:
if (!first) {
writer.write(',');
}
writer.write('\"');
charactersImpl(key.toCharArray(), 0, key.length());
writer.write('\"');
writer.write(':');
push(State.VALUE);
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void number(int number) throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
if (!first) {
writer.write(',');
}
// fall thru
case DOCUMENT:
case VALUE:
writer.write(Integer.toString(number));
if (state == State.VALUE) {
pop();
}
first = false;
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void number(long number) throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
if (!first) {
writer.write(',');
}
// fall thru
case DOCUMENT:
case VALUE:
writer.write(Long.toString(number));
if (state == State.VALUE) {
pop();
}
first = false;
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void number(float number) throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
if (!first) {
writer.write(',');
}
// fall thru
case DOCUMENT:
case VALUE:
writer.write(Float.toString(number));
if (state == State.VALUE) {
pop();
}
first = false;
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void number(double number) throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
if (!first) {
writer.write(',');
}
// fall thru
case DOCUMENT:
case VALUE:
writer.write(Double.toString(number));
if (state == State.VALUE) {
pop();
}
first = false;
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void startArray() throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
if (!first) {
writer.write(',');
}
// fall thru
case DOCUMENT:
case VALUE:
writer.write('[');
push(State.ARRAY);
first = true;
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void startDocument(String callback) throws SAXException {
try {
State state = peek();
switch (state) {
case INITIAL:
if (callback == null) {
hadCallback = false;
} else {
hadCallback = true;
writer.write(callback);
writer.write('(');
}
push(State.DOCUMENT);
first = true;
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void startObject() throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
if (!first) {
writer.write(',');
}
// fall thru
case DOCUMENT:
case VALUE:
writer.write('{');
push(State.OBJECT);
first = true;
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void startString() throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
if (!first) {
writer.write(',');
}
// fall thru
case DOCUMENT:
case VALUE:
writer.write('\"');
push(State.STRING);
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
@Override
public void string(String string) throws SAXException {
try {
State state = peek();
switch (state) {
case ARRAY:
if (!first) {
writer.write(',');
}
// fall thru
case DOCUMENT:
case VALUE:
if (string == null) {
writer.write("null");
} else {
writer.write('\"');
charactersImpl(string.toCharArray(), 0, string.length());
writer.write('\"');
}
if (state == State.VALUE) {
pop();
}
first = false;
break;
default:
throw new SAXException("Illegal state for callback.");
}
} catch (IOException e) {
throw new SAXException(e.getMessage(), e);
}
}
}