001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 *
027 * This file is based on:
028 * org.json.JSONWriter
029 * from the JSON in Java implementation.
030 *
031 * Copyright (c) 2002 JSON.org
032 *
033 * Permission is hereby granted, free of charge, to any person obtaining a copy
034 * of this software and associated documentation files (the "Software"), to deal
035 * in the Software without restriction, including without limitation the rights
036 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
037 * copies of the Software, and to permit persons to whom the Software is
038 * furnished to do so, subject to the following conditions:
039 *
040 * The above copyright notice and this permission notice shall be included in all
041 * copies or substantial portions of the Software.
042 *
043 * The Software shall be used for Good, not Evil.
044 *
045 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
046 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
047 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
048 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
049 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
050 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
051 * SOFTWARE.
052 */
053
054package org.opencms.json;
055
056import java.io.IOException;
057import java.io.Writer;
058
059/**
060 * JSONWriter provides a quick and convenient way of producing JSON text.
061 * The texts produced strictly conform to JSON syntax rules. No whitespace is
062 * added, so the results are ready for transmission or storage. Each instance of
063 * JSONWriter can produce one JSON text.
064 * <p>
065 * A JSONWriter instance provides a <code>value</code> method for appending
066 * values to the
067 * text, and a <code>key</code>
068 * method for adding keys before values in objects. There are <code>array</code>
069 * and <code>endArray</code> methods that make and bound array values, and
070 * <code>object</code> and <code>endObject</code> methods which make and bound
071 * object values. All of these methods return the JSONWriter instance,
072 * permitting a cascade style. For example, <pre>
073 * new JSONWriter(myWriter)
074 *     .object()
075 *         .key("JSON")
076 *         .value("Hello, World!")
077 *     .endObject();</pre> which writes <pre>
078 * {"JSON":"Hello, World!"}</pre>
079 * <p>
080 * The first method called must be <code>array</code> or <code>object</code>.
081 * There are no methods for adding commas or colons. JSONWriter adds them for
082 * you. Objects and arrays can be nested up to 20 levels deep.
083 * <p>
084 * This can sometimes be easier than using a JSONObject to build a string.<p>
085 *
086 */
087public class JSONWriter {
088
089    /** The maximum depth. */
090    private static final int MAXDEPTH = 20;
091
092    /**
093     * The current m_mode. Values:
094     * 'a' (array),
095     * 'd' (done),
096     * 'i' (initial),
097     * 'k' (key),
098     * 'o' (object).
099     */
100    protected char m_mode;
101
102    /**
103     * The m_writer that will receive the output.
104     */
105    protected Writer m_writer;
106
107    /**
108     * The m_comma flag determines if a m_comma should be output before the next
109     * value.
110     */
111    private boolean m_comma;
112
113    /**
114     * The object/array m_stack.
115     */
116    private char[] m_stack;
117
118    /**
119     * The m_stack m_top index. A value of 0 indicates that the m_stack is empty.
120     */
121    private int m_top;
122
123    /**
124     * Make a fresh JSONWriter.<p>
125     *
126     * It can be used to build one JSON text.<p>
127     *
128     * @param w the writer to use
129     */
130    public JSONWriter(Writer w) {
131
132        m_comma = false;
133        m_mode = 'i';
134        m_stack = new char[MAXDEPTH];
135        m_top = 0;
136        m_writer = w;
137    }
138
139    /**
140     * Begin appending a new array.<p>
141     *
142     * All values until the balancing
143     * <code>endArray</code> will be appended to this array. The
144     * <code>endArray</code> method must be called to mark the array's end.<p>
145     *
146     * @return this
147     * @throws JSONException if the nesting is too deep, or if the object is
148     * started in the wrong place (for example as a key or after the end of the
149     * outermost array or object)
150     */
151    public JSONWriter array() throws JSONException {
152
153        if ((m_mode == 'i') || (m_mode == 'o') || (m_mode == 'a')) {
154            push('a');
155            append("[");
156            m_comma = false;
157            return this;
158        }
159        throw new JSONException("Misplaced array.");
160    }
161
162    /**
163     * End an array. This method most be called to balance calls to
164     * <code>array</code>.<p>
165     *
166     * @return this
167     * @throws JSONException if incorrectly nested
168     */
169    public JSONWriter endArray() throws JSONException {
170
171        return end('a', ']');
172    }
173
174    /**
175     * End an object. This method most be called to balance calls to
176     * <code>object</code>.<p>
177     *
178     * @return this
179     * @throws JSONException if incorrectly nested
180     */
181    public JSONWriter endObject() throws JSONException {
182
183        return end('k', '}');
184    }
185
186    /**
187     * Append a key. The key will be associated with the next value. In an
188     * object, every value must be preceded by a key.<p>
189     *
190     * @param s a key string
191     * @return this
192     * @throws JSONException if the key is out of place. For example, keys
193     *  do not belong in arrays or if the key is null
194     */
195    public JSONWriter key(String s) throws JSONException {
196
197        if (s == null) {
198            throw new JSONException("Null key.");
199        }
200        if (m_mode == 'k') {
201            try {
202                if (m_comma) {
203                    m_writer.write(',');
204                }
205                m_writer.write(JSONObject.quote(s));
206                m_writer.write(':');
207                m_comma = false;
208                m_mode = 'o';
209                return this;
210            } catch (IOException e) {
211                throw new JSONException(e);
212            }
213        }
214        throw new JSONException("Misplaced key.");
215    }
216
217    /**
218     * Begin appending a new object.<p>
219     *
220     * All keys and values until the balancing
221     * <code>endObject</code> will be appended to this object. The
222     * <code>endObject</code> method must be called to mark the object's end.<p>
223     *
224     * @return this
225     * @throws JSONException if the nesting is too deep, or if the object is
226     * started in the wrong place (for example as a key or after the end of the
227     * outermost array or object)
228     */
229    public JSONWriter object() throws JSONException {
230
231        if (m_mode == 'i') {
232            m_mode = 'o';
233        }
234        if ((m_mode == 'o') || (m_mode == 'a')) {
235            append("{");
236            push('k');
237            m_comma = false;
238            return this;
239        }
240        throw new JSONException("Misplaced object.");
241
242    }
243
244    /**
245     * Append either the value <code>true</code> or the value
246     * <code>false</code>.<p>
247     *
248     * @param b a boolean
249     * @return this
250     * @throws JSONException if something goes wrong
251     */
252    public JSONWriter value(boolean b) throws JSONException {
253
254        return append(b ? "true" : "false");
255    }
256
257    /**
258     * Append a double value.<p>
259     *
260     * @param d a double
261     * @return this
262     * @throws JSONException if the number is not finite
263     */
264    public JSONWriter value(double d) throws JSONException {
265
266        return this.value(new Double(d));
267    }
268
269    /**
270     * Append a long value.<p>
271     *
272     * @param l a long
273     * @return this
274     * @throws JSONException if something goes wrong
275     */
276    public JSONWriter value(long l) throws JSONException {
277
278        return append(Long.toString(l));
279    }
280
281    /**
282     * Append an object value.<p>
283     *
284     * @param o the object to append. It can be null, or a Boolean, Number,
285     *   String, JSONObject, or JSONArray, or an object with a toJSONString()
286     *   method
287     * @return this
288     * @throws JSONException if the value is out of sequence
289     */
290    public JSONWriter value(Object o) throws JSONException {
291
292        return append(JSONObject.valueToString(o));
293    }
294
295    /**
296     * Append a value.<p>
297     *
298     * @param s a string value
299     * @return this
300     * @throws JSONException if the value is out of sequence
301     */
302    private JSONWriter append(String s) throws JSONException {
303
304        if (s == null) {
305            throw new JSONException("Null pointer");
306        }
307        if ((m_mode == 'o') || (m_mode == 'a')) {
308            try {
309                if (m_comma && (m_mode == 'a')) {
310                    m_writer.write(',');
311                }
312                m_writer.write(s);
313            } catch (IOException e) {
314                throw new JSONException(e);
315            }
316            if (m_mode == 'o') {
317                m_mode = 'k';
318            }
319            m_comma = true;
320            return this;
321        }
322        throw new JSONException("Value out of sequence.");
323    }
324
325    /**
326     * End something.<p>
327     *
328     * @param m mode
329     * @param c closing character
330     * @return this
331     * @throws JSONException if unbalanced
332     */
333    private JSONWriter end(char m, char c) throws JSONException {
334
335        if (m_mode != m) {
336            throw new JSONException(m == 'o' ? "Misplaced endObject." : "Misplaced endArray.");
337        }
338        pop(m);
339        try {
340            m_writer.write(c);
341        } catch (IOException e) {
342            throw new JSONException(e);
343        }
344        m_comma = true;
345        return this;
346    }
347
348    /**
349     * Pop an array or object scope.<p>
350     *
351     * @param c the scope to close
352     * @throws JSONException zf nesting is wrong
353     */
354    private void pop(char c) throws JSONException {
355
356        if ((m_top <= 0) || (m_stack[m_top - 1] != c)) {
357            throw new JSONException("Nesting error.");
358        }
359        m_top -= 1;
360        m_mode = m_top == 0 ? 'd' : m_stack[m_top - 1];
361    }
362
363    /**
364     * Push an array or object scope.<p>
365     *
366     * @param c the scope to open
367     * @throws JSONException if nesting is too deep
368     */
369    private void push(char c) throws JSONException {
370
371        if (m_top >= MAXDEPTH) {
372            throw new JSONException("Nesting too deep.");
373        }
374        m_stack[m_top] = c;
375        m_mode = c;
376        m_top += 1;
377    }
378}