/* Copyright 2013 The jeo project. All rights reserved.
*
* 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 io.jeo.json.encoder;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayDeque;
import io.jeo.json.JSONValue;
public class JSONEncoder {
/**
* output
*/
final Writer out;
/**
* object stack
*/
final ArrayDeque<Thing> stack = new ArrayDeque<Thing>();
String indent;
String space;
String newline;
/**
* Creates a new encoder.
*
* @param out Writer to output to.
*/
public JSONEncoder(Writer out) {
this(out, 0);
}
/**
* Creates a new encoder with formatting.
*
* @param out Writer to output to.
* @param indentSize The number of spaces to use when indenting.
*/
public JSONEncoder(Writer out, int indentSize) {
this.out = out;
indent = spaces(indentSize);
space = indentSize > 0 ? " " : "";
newline = indentSize > 0 ? "\n" : "";
}
/**
* The underlying writer.
*/
public Writer getWriter() {
return out;
}
/*
* Helper to generate an indentation chunk.
*/
static String spaces(int indentSize) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < indentSize; i++) {
sb.append(" ");
}
return sb.toString();
}
/**
* Starts a new JSON object.
*
* @return This encoder.
*/
public JSONEncoder object() throws IOException {
Thing t = peek();
if (t != null) {
if (t instanceof Arr) {
Arr a = (Arr) t;
if (a.size > 0) {
out.write(',');
}
}
else {
Obj o = (Obj) t;
if (!o.key) {
throw new IllegalArgumentException("no key for object");
}
}
t.size++;
}
stack.push(new Obj());
out.write('{');
return this;
}
/**
* Starts a new JSON array.
*
* @return This encoder.
*/
public JSONEncoder array() throws IOException {
Thing t = peek();
if (t != null) {
if (t instanceof Arr) {
Arr a = (Arr) t;
if (a.size > 0) {
out.write(',');
}
}
else if (t instanceof Obj) {
Obj o = (Obj) t;
if (!o.key) {
throw new IllegalStateException("no key");
}
}
t.size++;
}
stack.push(new Arr());
out.write('[');
return this;
}
/**
* Starts an object property.
*
* @param key The key/name of the property.
*
* @return This encoder.
*/
public JSONEncoder key(String key) throws IOException {
Thing t = peek();
if (!(t instanceof Obj)) {
throw new IllegalStateException("no object");
}
Obj o = (Obj) t;
if (o.key) {
throw new IllegalStateException("value required");
}
o.key = true;
if (o.size > 0) {
out.write(',');
}
newline();
out.write("\"");
out.write(JSONValue.escape(key));
out.write("\":");
out.write(space);
return this;
}
/**
* Specifies a numeric value for an object property.
* <p>
* You must call the {@link #key(String)} method before calling this method.
* </p>
* @param value The value.
* @return This encoder.
*/
public JSONEncoder value(Number value) throws IOException {
if (value != null) {
// check for double nan/infinte
if (value instanceof Double || value instanceof Float) {
Double val = value.doubleValue();
if (val.isInfinite() || val.isNaN()) {
value = null;
}
}
}
return doValue(value != null ? value.toString() : null);
}
public JSONEncoder value(double value) throws IOException {
return doValue(Double.toString(value));
}
public JSONEncoder value(long value) throws IOException {
return doValue(Long.toString(value));
}
/**
* Specifies a numeric value for an object property.
*
* @param value The value.
*
* @return This encoder.
*/
public JSONEncoder value(Object value) throws IOException {
if (value == null) {
return nul();
}
if (value instanceof Number) {
return value((Number)value);
}
else {
return value(value.toString());
}
}
/**
* Specified a null value for an object property.
* @return This encoder.
*/
public JSONEncoder nul() throws IOException {
return value((String)null);
}
/**
* Specifies a string value for an object property.
* <p>
* You must call the {@link #key(String)} method before calling this method.
* </p>
* @return This encoder.
*/
public JSONEncoder value(String value) throws IOException {
return doValue(value != null ? "\""+JSONValue.escape(value)+"\"" : null);
}
/*
* Helper to write out an already encoded value.
*/
JSONEncoder doValue(String encoded) throws IOException {
Thing t = peek();
if (t == null) {
throw new IllegalStateException("no object");
}
if (t instanceof Arr) {
Arr a = (Arr) t;
if (a.size > 0) {
out.write(',');
}
newline();
a.size++;
}
else {
Obj o = (Obj) t;
if (!o.key) {
throw new IllegalStateException("no key");
}
o.key = false;
o.size++;
}
if (encoded == null) {
encoded = "null";
}
out.write(encoded);
return this;
}
/**
* Ends the current JSON object.
*
* @return This encoder.
*/
public JSONEncoder endObject() throws IOException {
Thing t = peek();
if (!(t instanceof Obj)) {
throw new IllegalStateException("no object");
}
Obj o = (Obj) t;
if (o.key) {
throw new IllegalStateException("open key");
}
stack.pop();
if (o.size > 0) {
newline();
}
t = peek();
if (t instanceof Obj) {
o = (Obj) t;
if (!o.key) {
throw new IllegalStateException("no key");
}
o.key = false;
}
out.write('}');
return this;
}
/**
* Ends the current JSON array.
*
* @return This encoder.
*/
public JSONEncoder endArray() throws IOException {
Thing t = peek();
if (!(t instanceof Arr)) {
throw new IllegalStateException("no array");
}
Arr a = (Arr) t;
stack.pop();
if (a.size > 0) {
newline();
}
t = peek();
if (t instanceof Obj) {
Obj o = (Obj) t;
if (!o.key) {
throw new IllegalStateException("no key");
}
o.key = false;
}
out.write("]");
return this;
}
public JSONEncoder flush() throws IOException {
out.flush();
return this;
}
/*
* Moves output to the next line and indents. A no-op if formatting not active.
*/
void newline() throws IOException {
out.write(newline);
if (!"".equals(indent)) {
for (int i = 0; i < stack.size(); i++) {
out.write(indent);
}
}
}
/*
* Does a "safe" peek of the stack, returning null if empty.
*/
Thing peek() {
return stack.isEmpty() ? null : stack.peek();
}
static abstract class Thing {
protected int size = 0;
}
static class Obj extends Thing {
protected boolean key = false;
}
static class Arr extends Thing {
}
}