/** * Copyright 2011 The nanojson Authors * * 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 com.grack.nanojson; import java.io.IOException; import java.lang.reflect.Array; import java.util.Collection; import java.util.Map; import java.util.Stack; /** * Internal class that handles emitting to an {@link Appendable}. Users only see the public subclasses, * {@link JsonStringWriter} and {@link JsonAppendableWriter}. * * @param <SELF> * A subclass of {@link JsonWriterBase}. */ class JsonWriterBase<SELF extends JsonWriterBase<SELF>> implements JsonSink<SELF> { protected final Appendable appendable; private Stack<Boolean> states = new Stack<Boolean>(); private boolean first = true; private boolean inObject; JsonWriterBase(Appendable appendable) { this.appendable = appendable; } /** * This is guaranteed to be safe as the type of "this" will always be the type of "SELF". */ @SuppressWarnings("unchecked") private SELF castThis() { return (SELF)this; } @Override public SELF array(Collection<?> c) { return array(null, c); } @Override public SELF array(String key, Collection<?> c) { if (key == null) array(); else array(key); for (Object o : c) { value(o); } return end(); } @Override public SELF object(Map<?, ?> map) { return object(null, map); } @Override public SELF object(String key, Map<?, ?> map) { if (key == null) object(); else object(key); for (Map.Entry<?, ?> entry : map.entrySet()) { Object o = entry.getValue(); if (!(entry.getKey() instanceof String)) throw new JsonWriterException("Invalid key type for map: " + (entry.getKey() == null ? "null" : entry.getKey().getClass())); String k = (String)entry.getKey(); value(k, o); } return end(); } @Override public SELF nul() { preValue(); raw("null"); return castThis(); } @Override public SELF nul(String key) { preValue(key); raw("null"); return castThis(); } @Override public SELF value(Object o) { if (o == null) return nul(); else if (o instanceof String) return value((String)o); else if (o instanceof Number) return value(((Number)o)); else if (o instanceof Boolean) return value((boolean)(Boolean)o); else if (o instanceof Collection) return array((Collection<?>)o); else if (o instanceof Map) return object((Map<?, ?>)o); else if (o.getClass().isArray()) { int length = Array.getLength(o); array(); for (int i = 0; i < length; i++) value(Array.get(o, i)); return end(); } else throw new JsonWriterException("Unable to handle type: " + o.getClass()); } @Override public SELF value(String key, Object o) { if (o == null) return nul(key); else if (o instanceof String) return value(key, (String)o); else if (o instanceof Number) return value(key, (Number)o); else if (o instanceof Boolean) return value(key, (boolean)(Boolean)o); else if (o instanceof Collection) return array(key, (Collection<?>)o); else if (o instanceof Map) return object(key, (Map<?, ?>)o); else if (o.getClass().isArray()) { int length = Array.getLength(o); array(key); for (int i = 0; i < length; i++) value(Array.get(o, i)); return end(); } else throw new JsonWriterException("Unable to handle type: " + o.getClass()); } @Override public SELF value(String s) { if (s == null) return nul(); preValue(); emitStringValue(s); return castThis(); } @Override public SELF value(int i) { preValue(); raw(Integer.toString(i)); return castThis(); } @Override public SELF value(long l) { preValue(); raw(Long.toString(l)); return castThis(); } @Override public SELF value(boolean b) { preValue(); raw(Boolean.toString(b)); return castThis(); } @Override public SELF value(double d) { preValue(); raw(Double.toString(d)); return castThis(); } @Override public SELF value(float d) { preValue(); raw(Float.toString(d)); return castThis(); } @Override public SELF value(Number n) { preValue(); if (n == null) raw("null"); else raw(n.toString()); return castThis(); } @Override public SELF value(String key, String s) { if (s == null) return nul(key); preValue(key); emitStringValue(s); return castThis(); } @Override public SELF value(String key, int i) { preValue(key); raw(Integer.toString(i)); return castThis(); } @Override public SELF value(String key, long l) { preValue(key); raw(Long.toString(l)); return castThis(); } @Override public SELF value(String key, boolean b) { preValue(key); raw(Boolean.toString(b)); return castThis(); } @Override public SELF value(String key, double d) { preValue(key); raw(Double.toString(d)); return castThis(); } @Override public SELF value(String key, float d) { preValue(key); raw(Float.toString(d)); return castThis(); } @Override public SELF value(String key, Number n) { if (n == null) return nul(key); preValue(key); raw(n.toString()); return castThis(); } @Override public SELF array() { preValue(); states.push(inObject); inObject = false; first = true; raw('['); return castThis(); } @Override public SELF object() { preValue(); states.push(inObject); inObject = true; first = true; raw('{'); return castThis(); } @Override public SELF array(String key) { preValue(key); states.push(inObject); inObject = false; first = true; raw('['); return castThis(); } @Override public SELF object(String key) { preValue(key); states.push(inObject); inObject = true; first = true; raw('{'); return castThis(); } @Override public SELF end() { if (states.size() == 0) throw new JsonWriterException("Invalid call to end()"); if (inObject) { raw('}'); } else { raw(']'); } first = false; inObject = states.pop(); return castThis(); } /** * Ensures that the object is in the finished state. * * @throws JsonWriterException * if the written JSON is not properly balanced, ie: all arrays and objects that were started have been * properly ended. */ protected void doneInternal() { if (states.size() > 0) throw new JsonWriterException("Unclosed JSON objects and/or arrays when closing writer"); if (first) throw new JsonWriterException("Nothing was written to the JSON writer"); } private void raw(String s) { try { appendable.append(s); } catch (IOException e) { throw new JsonWriterException(e); } } private void raw(char c) { try { appendable.append(c); } catch (IOException e) { throw new JsonWriterException(e); } } private void pre() { if (first) { first = false; } else { if (states.size() == 0) throw new JsonWriterException("Invalid call to emit a value in a finished JSON writer"); raw(','); } } private void preValue() { if (inObject) throw new JsonWriterException("Invalid call to emit a keyless value while writing an object"); pre(); } private void preValue(String key) { if (!inObject) throw new JsonWriterException("Invalid call to emit a key value while not writing an object"); pre(); emitStringValue(key); raw(':'); } /** * Emits a quoted string value, escaping characters that are required to be escaped. */ private void emitStringValue(String s) { raw('"'); char b = 0, c = 0; for (int i = 0; i < s.length(); i++) { b = c; c = s.charAt(i); switch (c) { case '\\': case '"': raw('\\'); raw(c); break; case '/': // Special case to ensure that </script> doesn't appear in JSON // output if (b == '<') raw('\\'); raw(c); break; case '\b': raw("\\b"); break; case '\t': raw("\\t"); break; case '\n': raw("\\n"); break; case '\f': raw("\\f"); break; case '\r': raw("\\r"); break; default: if (shouldBeEscaped(c)) { String t = "000" + Integer.toHexString(c); raw("\\u" + t.substring(t.length() - "0000".length())); } else { raw(c); } } } raw('"'); } /** * json.org spec says that all control characters must be escaped. */ private boolean shouldBeEscaped(char c) { return c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100'); } }