001/*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * SonarQube is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.utils.text;
021
022import org.sonar.api.utils.DateUtils;
023
024import javax.annotation.Nullable;
025import java.io.Writer;
026import java.util.Date;
027import java.util.Map;
028
029/**
030 * Writes JSON as a stream. This class allows plugins to not directly depend
031 * on the underlying JSON library.
032 * <p/>
033 * <h3>How to use</h3>
034 * <pre>
035 *   StringWriter json = new StringWriter();
036 *   JsonWriter writer = JsonWriter.of(json);
037 *   writer
038 *     .beginObject()
039 *     .prop("aBoolean", true)
040 *     .prop("aInt", 123)
041 *     .prop("aString", "foo")
042 *     .beginObject().name("aList")
043 *       .beginArray()
044 *         .beginObject().prop("key", "ABC").endObject()
045 *         .beginObject().prop("key", "DEF").endObject()
046 *       .endArray()
047 *     .endObject()
048 *     .close();
049 * </pre>
050 *
051 * @since 4.2
052 */
053public class JsonWriter {
054
055  private final com.google.gson.stream.JsonWriter stream;
056
057  private JsonWriter(Writer writer) {
058    this.stream = new com.google.gson.stream.JsonWriter(writer);
059    this.stream.setSerializeNulls(false);
060    this.stream.setLenient(false);
061  }
062
063  // for unit testing
064  JsonWriter(com.google.gson.stream.JsonWriter stream) {
065    this.stream = stream;
066  }
067
068  public static JsonWriter of(Writer writer) {
069    return new JsonWriter(writer);
070  }
071
072  public JsonWriter setSerializeNulls(boolean b) {
073    this.stream.setSerializeNulls(b);
074    return this;
075  }
076
077  /**
078   * Begins encoding a new array. Each call to this method must be paired with
079   * a call to {@link #endArray}. Output is <code>[</code>.
080   *
081   * @throws org.sonar.api.utils.text.WriterException on any failure
082   */
083  public JsonWriter beginArray() {
084    try {
085      stream.beginArray();
086      return this;
087    } catch (Exception e) {
088      throw rethrow(e);
089    }
090  }
091
092  /**
093   * Ends encoding the current array. Output is <code>]</code>.
094   *
095   * @throws org.sonar.api.utils.text.WriterException on any failure
096   */
097  public JsonWriter endArray() {
098    try {
099      stream.endArray();
100      return this;
101    } catch (Exception e) {
102      throw rethrow(e);
103    }
104  }
105
106  /**
107   * Begins encoding a new object. Each call to this method must be paired
108   * with a call to {@link #endObject}. Output is <code>{</code>.
109   *
110   * @throws org.sonar.api.utils.text.WriterException on any failure
111   */
112  public JsonWriter beginObject() {
113    try {
114      stream.beginObject();
115      return this;
116    } catch (Exception e) {
117      throw rethrow(e);
118    }
119  }
120
121  /**
122   * Ends encoding the current object. Output is <code>}</code>.
123   *
124   * @throws org.sonar.api.utils.text.WriterException on any failure
125   */
126  public JsonWriter endObject() {
127    try {
128      stream.endObject();
129      return this;
130    } catch (Exception e) {
131      throw rethrow(e);
132    }
133  }
134
135  /**
136   * Encodes the property name. Output is <code>"theName":</code>.
137   *
138   * @throws org.sonar.api.utils.text.WriterException on any failure
139   */
140  public JsonWriter name(String name) {
141    try {
142      stream.name(name);
143      return this;
144    } catch (Exception e) {
145      throw rethrow(e);
146    }
147  }
148
149  /**
150   * Encodes {@code value}. Output is <code>true</code> or <code>false</code>.
151   *
152   * @throws org.sonar.api.utils.text.WriterException on any failure
153   */
154  public JsonWriter value(boolean value) {
155    try {
156      stream.value(value);
157      return this;
158    } catch (Exception e) {
159      throw rethrow(e);
160    }
161  }
162
163  /**
164   * @throws org.sonar.api.utils.text.WriterException on any failure
165   */
166  public JsonWriter value(double value) {
167    try {
168      stream.value(value);
169      return this;
170    } catch (Exception e) {
171      throw rethrow(e);
172    }
173  }
174
175  /**
176   * @throws org.sonar.api.utils.text.WriterException on any failure
177   */
178  public JsonWriter value(@Nullable String value) {
179    try {
180      stream.value(value);
181      return this;
182    } catch (Exception e) {
183      throw rethrow(e);
184    }
185  }
186
187  /**
188   * Encodes an object that can be a :
189   * <ul>
190   * <li>primitive types: String, Number, Boolean</li>
191   * <li>java.util.Date: encoded as datetime (see {@link #valueDateTime(java.util.Date)}</li>
192   * <li><code>Map<Object, Object></code>. Method toString is called for the key.</li>
193   * <li>Iterable</li>
194   * </ul>
195   *
196   * @throws org.sonar.api.utils.text.WriterException on any failure
197   */
198  public JsonWriter valueObject(@Nullable Object value) {
199    try {
200      if (value == null) {
201        stream.nullValue();
202      } else {
203        if (value instanceof String) {
204          stream.value((String) value);
205        } else if (value instanceof Number) {
206          stream.value((Number) value);
207        } else if (value instanceof Boolean) {
208          stream.value((Boolean) value);
209        } else if (value instanceof Date) {
210          valueDateTime((Date) value);
211        } else if (value instanceof Enum) {
212          stream.value(((Enum)value).name());
213        } else if (value instanceof Map) {
214          stream.beginObject();
215          for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) {
216            stream.name(entry.getKey().toString());
217            valueObject(entry.getValue());
218          }
219          stream.endObject();
220        } else if (value instanceof Iterable) {
221          stream.beginArray();
222          for (Object o : (Iterable<Object>) value) {
223            valueObject(o);
224          }
225          stream.endArray();
226        } else {
227          throw new IllegalArgumentException(getClass() + " does not support encoding of type: " + value.getClass());
228        }
229      }
230      return this;
231    } catch (IllegalArgumentException e) {
232      throw e;
233    } catch (Exception e) {
234      throw rethrow(e);
235    }
236  }
237
238  /**
239   * Write a list of values in an array, for example:
240   * <pre>
241   *   writer.beginArray().values(myValues).endArray();
242   * </pre>
243   *
244   * @throws org.sonar.api.utils.text.WriterException on any failure
245   */
246  public JsonWriter values(Iterable<String> values) {
247    for (String value : values) {
248      value(value);
249    }
250    return this;
251  }
252
253  /**
254   * @throws org.sonar.api.utils.text.WriterException on any failure
255   */
256  public JsonWriter valueDate(@Nullable Date value) {
257    try {
258      stream.value(value == null ? null : DateUtils.formatDate(value));
259      return this;
260    } catch (Exception e) {
261      throw rethrow(e);
262    }
263  }
264
265  public JsonWriter valueDateTime(@Nullable Date value) {
266    try {
267      stream.value(value == null ? null : DateUtils.formatDateTime(value));
268      return this;
269    } catch (Exception e) {
270      throw rethrow(e);
271    }
272  }
273
274  /**
275   * @throws org.sonar.api.utils.text.WriterException on any failure
276   */
277  public JsonWriter value(long value) {
278    try {
279      stream.value(value);
280      return this;
281    } catch (Exception e) {
282      throw rethrow(e);
283    }
284  }
285
286  /**
287   * @throws org.sonar.api.utils.text.WriterException on any failure
288   */
289  public JsonWriter value(@Nullable Number value) {
290    try {
291      stream.value(value);
292      return this;
293    } catch (Exception e) {
294      throw rethrow(e);
295    }
296  }
297
298  /**
299   * Encodes the property name and value. Output is for example <code>"theName":123</code>.
300   *
301   * @throws org.sonar.api.utils.text.WriterException on any failure
302   */
303  public JsonWriter prop(String name, @Nullable Number value) {
304    return name(name).value(value);
305  }
306
307  /**
308   * Encodes the property name and date value (ISO format).
309   * Output is for example <code>"theDate":"2013-01-24"</code>.
310   *
311   * @throws org.sonar.api.utils.text.WriterException on any failure
312   */
313  public JsonWriter propDate(String name, @Nullable Date value) {
314    return name(name).valueDate(value);
315  }
316
317  /**
318   * Encodes the property name and datetime value (ISO format).
319   * Output is for example <code>"theDate":"2013-01-24T13:12:45+01"</code>.
320   *
321   * @throws org.sonar.api.utils.text.WriterException on any failure
322   */
323  public JsonWriter propDateTime(String name, @Nullable Date value) {
324    return name(name).valueDateTime(value);
325  }
326
327  /**
328   * @throws org.sonar.api.utils.text.WriterException on any failure
329   */
330  public JsonWriter prop(String name, @Nullable String value) {
331    return name(name).value(value);
332  }
333
334  /**
335   * @throws org.sonar.api.utils.text.WriterException on any failure
336   */
337  public JsonWriter prop(String name, boolean value) {
338    return name(name).value(value);
339  }
340
341  /**
342   * @throws org.sonar.api.utils.text.WriterException on any failure
343   */
344  public JsonWriter prop(String name, long value) {
345    return name(name).value(value);
346  }
347
348  /**
349   * @throws org.sonar.api.utils.text.WriterException on any failure
350   */
351  public JsonWriter prop(String name, double value) {
352    return name(name).value(value);
353  }
354
355  /**
356   * @throws org.sonar.api.utils.text.WriterException on any failure
357   */
358  public void close() {
359    try {
360      stream.close();
361    } catch (Exception e) {
362      throw rethrow(e);
363    }
364  }
365
366  private IllegalStateException rethrow(Exception e) {
367    // stacktrace is not helpful
368    throw new WriterException("Fail to write JSON: " + e.getMessage());
369  }
370}