/*
* #!
* Ontopia Engine
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* !#
*/
package net.ontopia.topicmaps.utils.jtm;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* PUBLIC: A JSON serializer. Take a look at the <a
* href="http://www.json.org/">JSON Homepage</a> for a complete specification of
* the format.
*
* The JSONWriter object provides <code>key</code>, <code>value</code> and
* <code>pair</code> methods to append JSON key/value pairs. With the
* <code>array</code> and <code>endArray</code> methods you can create bound
* array values, and <code>object</code> and <code>endObject</code> methods
* allows you to create and bound objects. All of these methods return the
* JSONWriter instance, permitting a cascade style. For example,
*
* <pre>
* new JSONWriter(myWriter).object().key("JSON").value("Hello, World!")
* .endObject();
* </pre>
*
* which writes
*
* <pre>
* {"JSON":"Hello, World!"}
* </pre>
*
* <b>Note:</b> a default instance of a JSONWriter will prettify the output,
* i.e. include whitespace and newlines as appropriate. To disable this
* behaviour, use <a href="#setPrettify">setPrettify</a>.
*
* @since 5.1
*/
public class JSONWriter {
static Logger log = LoggerFactory.getLogger(JSONWriter.class.getName());
private final static int INDENT = 2;
private Writer out;
private boolean prettify;
private int depth;
private boolean comma;
private boolean startOfDocument;
/**
* PUBLIC: Create an JSONWriter that writes to a given OutputStream in UTF-8.
*
* @param stream Where the output should be written.
*/
public JSONWriter(OutputStream stream) throws IOException {
this(stream, "utf-8");
}
/**
* PUBLIC: Create an JSONWriter that writes to a given OutputStream in the
* given encoding.
*
* @param stream Where the output should be written.
* @param encoding The desired character encoding.
*/
public JSONWriter(OutputStream stream, String encoding) throws IOException {
this(new OutputStreamWriter(stream, encoding));
}
/**
* PUBLIC: Create an JSONWriter that writes to a given Writer.
*
* @param out Where the output should be written.
*/
public JSONWriter(Writer out) {
this.out = out;
this.prettify = true;
this.depth = -1;
this.comma = false;
this.startOfDocument = true;
}
/**
* PUBLIC: Returns whether the output from the JSON serializer is being
* prettified, e.g. contains newlines and indentation.
*
* @return <tt>true</tt> if the JSON output should be prettified,
* <tt>false</tt> otherwise.
*/
public boolean isPrettify() {
return prettify;
}
/**
* PUBLIC: Sets the prettify behaviour of the JSON serializer.
*
* @param prettify <tt>true</tt> to enable prettifying of the JSON output,
* <tt>false</tt> to disable it.
*/
public void setPrettify(boolean prettify) {
this.prettify = prettify;
}
/**
* PUBLIC: Finish the serialization process, flushes the underlying stream.
*/
public void finish() throws IOException {
if (prettify) {
out.write('\n');
}
out.flush();
}
/**
* PUBLIC: Begin to append a new object.
*
* @return this
*/
public JSONWriter object() throws IOException {
if (comma) {
out.write(',');
}
depth++;
indent();
out.write('{');
comma = false;
startOfDocument = false;
return this;
}
/**
* PUBLIC: Finish of an JSON object.
*
* @return this
*/
public JSONWriter endObject() throws IOException {
out.write('}');
depth--;
comma = true;
return this;
}
/**
* PUBLIC: Begin a new JSON array.
*
* @return this
*/
public JSONWriter array() throws IOException {
out.write('[');
depth++;
comma = false;
return this;
}
/**
* PUBLIC: Finish an JSON array.
*
* @return this
*/
public JSONWriter endArray() throws IOException {
out.write(']');
depth--;
comma = true;
return this;
}
/**
* PUBLIC: Write out the given key. The key is quoted and escaped according to
* the JSON specification.
*
* @param key The key to be written.
* @return this
*/
public JSONWriter key(String key) throws IOException {
if (comma) {
out.write(',');
indent();
out.write(' ');
}
out.write(quote(key));
out.write(':');
comma = false;
return this;
}
/**
* Write out the given value. The value is quoted and escaped according to the
* JSON specification.
*
* @param value The value to be written.
* @return this
*/
public JSONWriter value(String value) throws IOException {
if (comma) {
out.write(',');
}
out.write(quote(value));
comma = true;
return this;
}
/**
* Write a complete JSON key/value pair to the stream.
* This method is just for convenience, it does the same as
*
* <pre>
* writer.key("mykey").value("myvalue");
* </pre>
*
* @param key The key to be written.
* @param value The value to be written.
* @return this
*/
public JSONWriter pair(String key, String value) throws IOException {
return this.key(key).value(value);
}
private void indent() throws IOException {
if (prettify) {
if (depth >= 0 && !startOfDocument) {
out.write('\n');
}
if (depth > 0) {
int indentation = depth * INDENT;
out.write(String.format("%1$" + indentation + "s", ' '));
}
}
}
/**
* Produce a string in double quotes with backslash sequences in all the right
* places. A backslash will be inserted within </, allowing JSON text to be
* delivered in HTML. In JSON text, a string cannot contain a control
* character or an unescaped quote or backslash.
*
* @param string A String
* @return A String correctly formatted for insertion in a JSON text.
* @author JSON.org
*/
private static String quote(String string) {
if (string == null || string.length() == 0) {
return "\"\"";
}
char b;
char c = 0;
int i;
int len = string.length();
StringBuilder sb = new StringBuilder(len + 4);
String t;
sb.append('"');
for (i = 0; i < len; i += 1) {
b = c;
c = string.charAt(i);
switch (c) {
case '\\':
case '"':
sb.append('\\');
sb.append(c);
break;
case '/':
if (b == '<') {
sb.append('\\');
}
sb.append(c);
break;
case '\b':
sb.append("\\b");
break;
case '\t':
sb.append("\\t");
break;
case '\n':
sb.append("\\n");
break;
case '\f':
sb.append("\\f");
break;
case '\r':
sb.append("\\r");
break;
default:
if (c < ' ' || (c >= '\u0080' && c < '\u00a0')
|| (c >= '\u2000' && c < '\u2100')) {
t = "000" + Integer.toHexString(c);
sb.append("\\u" + t.substring(t.length() - 4));
} else {
sb.append(c);
}
}
}
sb.append('"');
return sb.toString();
}
}