/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2012-2013 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.json; import org.glassfish.json.api.BufferPool; import javax.json.*; import javax.json.stream.JsonGenerationException; import javax.json.stream.JsonGenerator; import java.io.*; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.Charset; import java.util.ArrayDeque; import java.util.Deque; import java.util.Map; /** * @author Jitendra Kotamraju */ class JsonGeneratorImpl implements JsonGenerator { private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final char[] INT_MIN_VALUE_CHARS = "-2147483648".toCharArray(); private static final int[] INT_CHARS_SIZE_TABLE = { 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE }; private static final char [] DIGIT_TENS = { '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2', '3', '3', '3', '3', '3', '3', '3', '3', '3', '3', '4', '4', '4', '4', '4', '4', '4', '4', '4', '4', '5', '5', '5', '5', '5', '5', '5', '5', '5', '5', '6', '6', '6', '6', '6', '6', '6', '6', '6', '6', '7', '7', '7', '7', '7', '7', '7', '7', '7', '7', '8', '8', '8', '8', '8', '8', '8', '8', '8', '8', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', } ; private static final char [] DIGIT_ONES = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', } ; /** * All possible chars for representing a number as a String */ private static final char[] DIGITS = { '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' }; private static enum Scope { IN_NONE, IN_OBJECT, IN_ARRAY } private final BufferPool bufferPool; private final Writer writer; private Context currentContext = new Context(Scope.IN_NONE); private final Deque<Context> stack = new ArrayDeque<Context>(); // Using own buffering mechanism as JDK's BufferedWriter uses synchronized // methods. Also, flushBuffer() is useful when you don't want to actually // flush the underlying output source private final char buf[]; // capacity >= INT_MIN_VALUE_CHARS.length private int len = 0; JsonGeneratorImpl(Writer writer, BufferPool bufferPool) { this.writer = writer; this.bufferPool = bufferPool; this.buf = bufferPool.take(); } JsonGeneratorImpl(OutputStream out, BufferPool bufferPool) { this(out, UTF_8, bufferPool); } JsonGeneratorImpl(OutputStream out, Charset encoding, BufferPool bufferPool) { this(new OutputStreamWriter(out, encoding), bufferPool); } @Override public void flush() { flushBuffer(); try { writer.flush(); } catch (IOException ioe) { throw new JsonException(JsonMessages.GENERATOR_FLUSH_IO_ERR(), ioe); } } @Override public JsonGenerator writeStartObject() { if (currentContext.scope == Scope.IN_OBJECT) { throw new JsonGenerationException(JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } if (currentContext.scope == Scope.IN_NONE && !currentContext.first) { throw new JsonGenerationException(JsonMessages.GENERATOR_ILLEGAL_MULTIPLE_TEXT()); } writeComma(); writeChar('{'); stack.push(currentContext); currentContext = new Context(Scope.IN_OBJECT); return this; } @Override public JsonGenerator writeStartObject(String name) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeName(name); writeChar('{'); stack.push(currentContext); currentContext = new Context(Scope.IN_OBJECT); return this; } private JsonGenerator writeName(String name) { writeComma(); writeEscapedString(name); writeChar(':'); return this; } @Override public JsonGenerator write(String name, String fieldValue) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeName(name); writeEscapedString(fieldValue); return this; } @Override public JsonGenerator write(String name, int value) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeName(name); writeInt(value); return this; } @Override public JsonGenerator write(String name, long value) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeName(name); writeString(String.valueOf(value)); return this; } @Override public JsonGenerator write(String name, double value) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } if (Double.isInfinite(value) || Double.isNaN(value)) { throw new NumberFormatException(JsonMessages.GENERATOR_DOUBLE_INFINITE_NAN()); } writeName(name); writeString(String.valueOf(value)); return this; } @Override public JsonGenerator write(String name, BigInteger value) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeName(name); writeString(String.valueOf(value)); return this; } @Override public JsonGenerator write(String name, BigDecimal value) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeName(name); writeString(String.valueOf(value)); return this; } @Override public JsonGenerator write(String name, boolean value) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeName(name); writeString(value? "true" : "false"); return this; } @Override public JsonGenerator writeNull(String name) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeName(name); writeString("null"); return this; } @Override public JsonGenerator write(JsonValue value) { if (currentContext.scope != Scope.IN_ARRAY) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } switch (value.getValueType()) { case ARRAY: JsonArray array = (JsonArray)value; writeStartArray(); for(JsonValue child: array) { write(child); } writeEnd(); break; case OBJECT: JsonObject object = (JsonObject)value; writeStartObject(); for(Map.Entry<String, JsonValue> member: object.entrySet()) { write(member.getKey(), member.getValue()); } writeEnd(); break; case STRING: JsonString str = (JsonString)value; write(str.getString()); break; case NUMBER: JsonNumber number = (JsonNumber)value; writeValue(number.toString()); break; case TRUE: write(true); break; case FALSE: write(false); break; case NULL: writeNull(); break; } return this; } @Override public JsonGenerator writeStartArray() { if (currentContext.scope == Scope.IN_OBJECT) { throw new JsonGenerationException(JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } if (currentContext.scope == Scope.IN_NONE && !currentContext.first) { throw new JsonGenerationException(JsonMessages.GENERATOR_ILLEGAL_MULTIPLE_TEXT()); } writeComma(); writeChar('['); stack.push(currentContext); currentContext = new Context(Scope.IN_ARRAY); return this; } @Override public JsonGenerator writeStartArray(String name) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeName(name); writeChar('['); stack.push(currentContext); currentContext = new Context(Scope.IN_ARRAY); return this; } @Override public JsonGenerator write(String name, JsonValue value) { if (currentContext.scope != Scope.IN_OBJECT) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } switch (value.getValueType()) { case ARRAY: JsonArray array = (JsonArray)value; writeStartArray(name); for(JsonValue child: array) { write(child); } writeEnd(); break; case OBJECT: JsonObject object = (JsonObject)value; writeStartObject(name); for(Map.Entry<String, JsonValue> member: object.entrySet()) { write(member.getKey(), member.getValue()); } writeEnd(); break; case STRING: JsonString str = (JsonString)value; write(name, str.getString()); break; case NUMBER: JsonNumber number = (JsonNumber)value; writeValue(name, number.toString()); break; case TRUE: write(name, true); break; case FALSE: write(name, false); break; case NULL: writeNull(name); break; } return this; } public JsonGenerator write(String value) { if (currentContext.scope != Scope.IN_ARRAY) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeComma(); writeEscapedString(value); return this; } public JsonGenerator write(int value) { if (currentContext.scope != Scope.IN_ARRAY) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeComma(); writeInt(value); return this; } @Override public JsonGenerator write(long value) { if (currentContext.scope != Scope.IN_ARRAY) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeValue(String.valueOf(value)); return this; } @Override public JsonGenerator write(double value) { if (currentContext.scope != Scope.IN_ARRAY) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } if (Double.isInfinite(value) || Double.isNaN(value)) { throw new NumberFormatException(JsonMessages.GENERATOR_DOUBLE_INFINITE_NAN()); } writeValue(String.valueOf(value)); return this; } @Override public JsonGenerator write(BigInteger value) { if (currentContext.scope != Scope.IN_ARRAY) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeValue(value.toString()); return this; } @Override public JsonGenerator write(BigDecimal value) { if (currentContext.scope != Scope.IN_ARRAY) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeValue(value.toString()); return this; } public JsonGenerator write(boolean value) { if (currentContext.scope != Scope.IN_ARRAY) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeComma(); writeString(value ? "true" : "false"); return this; } public JsonGenerator writeNull() { if (currentContext.scope != Scope.IN_ARRAY) { throw new JsonGenerationException( JsonMessages.GENERATOR_ILLEGAL_METHOD(currentContext.scope)); } writeComma(); writeString("null"); return this; } private void writeValue(String value) { writeComma(); writeString(value); } private void writeValue(String name, String value) { writeComma(); writeEscapedString(name); writeChar(':'); writeString(value); } @Override public JsonGenerator writeEnd() { if (currentContext.scope == Scope.IN_NONE) { throw new JsonGenerationException("writeEnd() cannot be called in no context"); } writeChar(currentContext.scope == Scope.IN_ARRAY ? ']' : '}'); currentContext = stack.pop(); return this; } protected void writeComma() { if (!currentContext.first) { writeChar(','); } currentContext.first = false; } private static class Context { boolean first = true; final Scope scope; Context(Scope scope) { this.scope = scope; } } public void close() { if (currentContext.scope != Scope.IN_NONE || currentContext.first) { throw new JsonGenerationException(JsonMessages.GENERATOR_INCOMPLETE_JSON()); } flushBuffer(); try { writer.close(); } catch (IOException ioe) { throw new JsonException(JsonMessages.GENERATOR_CLOSE_IO_ERR(), ioe); } bufferPool.recycle(buf); } // begin, end-1 indexes represent characters that need not // be escaped // // XXXssssssssssssXXXXXXXXXXXXXXXXXXXXXXrrrrrrrrrrrrrrXXXXXX // ^ ^ ^ ^ // | | | | // begin end begin end void writeEscapedString(String string) { writeChar('"'); int len = string.length(); for(int i = 0; i < len; i++) { int begin = i, end = i; char c = string.charAt(i); // find all the characters that need not be escaped // unescaped = %x20-21 | %x23-5B | %x5D-10FFFF while(c >= 0x20 && c <= 0x10ffff && c != 0x22 && c != 0x5c) { i++; end = i; if (i < len) { c = string.charAt(i); } else { break; } } // Write characters without escaping if (begin < end) { writeString(string, begin, end); if (i == len) { break; } } switch (c) { case '"': case '\\': writeChar('\\'); writeChar(c); break; case '\b': writeChar('\\'); writeChar('b'); break; case '\f': writeChar('\\'); writeChar('f'); break; case '\n': writeChar('\\'); writeChar('n'); break; case '\r': writeChar('\\'); writeChar('r'); break; case '\t': writeChar('\\'); writeChar('t'); break; default: String hex = "000" + Integer.toHexString(c); writeString("\\u" + hex.substring(hex.length() - 4)); } } writeChar('"'); } void writeString(String str, int begin, int end) { while (begin < end) { // source begin and end indexes int no = Math.min(buf.length - len, end - begin); str.getChars(begin, begin + no, buf, len); begin += no; // Increment source index len += no; // Increment dest index if (len >= buf.length) { flushBuffer(); } } } void writeString(String str) { writeString(str, 0, str.length()); } void writeChar(char c) { if (len >= buf.length) { flushBuffer(); } buf[len++] = c; } // Not using Integer.toString() since it creates intermediary String // Also, we want the chars to be copied to our buffer directly void writeInt(int num) { int size; if (num == Integer.MIN_VALUE) { size = INT_MIN_VALUE_CHARS.length; } else { size = (num < 0) ? stringSize(-num) + 1 : stringSize(num); } if (len+size >= buf.length) { flushBuffer(); } if (num == Integer.MIN_VALUE) { System.arraycopy(INT_MIN_VALUE_CHARS, 0, buf, len, size); } else { fillIntChars(num, buf, len+size); } len += size; } // flushBuffer writes the buffered contents to writer. But incase of // byte stream, an OuputStreamWriter is created and that buffers too. // We may need to call OutputStreamWriter#flushBuffer() using // reflection if that is really required (commented out below) void flushBuffer() { try { if (len > 0) { writer.write(buf, 0, len); len = 0; } } catch (IOException ioe) { throw new JsonException(JsonMessages.GENERATOR_WRITE_IO_ERR(), ioe); } } // private static final Method flushBufferMethod; // static { // Method m = null; // try { // m = OutputStreamWriter.class.getDeclaredMethod("flushBuffer"); // m.setAccessible(true); // } catch (Exception e) { // // no-op // } // flushBufferMethod = m; // } // void flushBufferOSW() { // flushBuffer(); // if (writer instanceof OutputStreamWriter) { // try { // flushBufferMethod.invoke(writer); // } catch (Exception e) { // // no-op // } // } // } // Requires positive x private static int stringSize(int x) { for (int i=0; ; i++) if (x <= INT_CHARS_SIZE_TABLE[i]) return i+1; } /** * Places characters representing the integer i into the * character array buf. The characters are placed into * the buffer backwards starting with the least significant * digit at the specified index (exclusive), and working * backwards from there. * * Will fail if i == Integer.MIN_VALUE */ private static void fillIntChars(int i, char[] buf, int index) { int q, r; int charPos = index; char sign = 0; if (i < 0) { sign = '-'; i = -i; } // Generate two digits per iteration while (i >= 65536) { q = i / 100; // really: r = i - (q * 100); r = i - ((q << 6) + (q << 5) + (q << 2)); i = q; buf [--charPos] = DIGIT_ONES[r]; buf [--charPos] = DIGIT_TENS[r]; } // Fall thru to fast mode for smaller numbers // assert(i <= 65536, i); for (;;) { q = (i * 52429) >>> (16+3); r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ... buf [--charPos] = DIGITS[r]; i = q; if (i == 0) break; } if (sign != 0) { buf [--charPos] = sign; } } }