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  /**
073   * Begins encoding a new array. Each call to this method must be paired with
074   * a call to {@link #endArray}. Output is <code>[</code>.
075   *
076   * @throws org.sonar.api.utils.text.WriterException on any failure
077   */
078  public JsonWriter beginArray() {
079    try {
080      stream.beginArray();
081      return this;
082    } catch (Exception e) {
083      throw rethrow(e);
084    }
085  }
086
087  /**
088   * Ends encoding the current array. Output is <code>]</code>.
089   *
090   * @throws org.sonar.api.utils.text.WriterException on any failure
091   */
092  public JsonWriter endArray() {
093    try {
094      stream.endArray();
095      return this;
096    } catch (Exception e) {
097      throw rethrow(e);
098    }
099  }
100
101  /**
102   * Begins encoding a new object. Each call to this method must be paired
103   * with a call to {@link #endObject}. Output is <code>{</code>.
104   *
105   * @throws org.sonar.api.utils.text.WriterException on any failure
106   */
107  public JsonWriter beginObject() {
108    try {
109      stream.beginObject();
110      return this;
111    } catch (Exception e) {
112      throw rethrow(e);
113    }
114  }
115
116  /**
117   * Ends encoding the current object. Output is <code>}</code>.
118   *
119   * @throws org.sonar.api.utils.text.WriterException on any failure
120   */
121  public JsonWriter endObject() {
122    try {
123      stream.endObject();
124      return this;
125    } catch (Exception e) {
126      throw rethrow(e);
127    }
128  }
129
130  /**
131   * Encodes the property name. Output is <code>"theName":</code>.
132   *
133   * @throws org.sonar.api.utils.text.WriterException on any failure
134   */
135  public JsonWriter name(String name) {
136    try {
137      stream.name(name);
138      return this;
139    } catch (Exception e) {
140      throw rethrow(e);
141    }
142  }
143
144  /**
145   * Encodes {@code value}. Output is <code>true</code> or <code>false</code>.
146   *
147   * @throws org.sonar.api.utils.text.WriterException on any failure
148   */
149  public JsonWriter value(boolean value) {
150    try {
151      stream.value(value);
152      return this;
153    } catch (Exception e) {
154      throw rethrow(e);
155    }
156  }
157
158  /**
159   * @throws org.sonar.api.utils.text.WriterException on any failure
160   */
161  public JsonWriter value(double value) {
162    try {
163      stream.value(value);
164      return this;
165    } catch (Exception e) {
166      throw rethrow(e);
167    }
168  }
169
170  /**
171   * @throws org.sonar.api.utils.text.WriterException on any failure
172   */
173  public JsonWriter value(@Nullable String value) {
174    try {
175      stream.value(value);
176      return this;
177    } catch (Exception e) {
178      throw rethrow(e);
179    }
180  }
181
182  /**
183   * Encodes an object that can be a :
184   * <ul>
185   * <li>primitive types: String, Number, Boolean</li>
186   * <li>java.util.Date: encoded as datetime (see {@link #valueDateTime(java.util.Date)}</li>
187   * <li><code>Map<Object, Object></code>. Method toString is called for the key.</li>
188   * <li>Iterable</li>
189   * </ul>
190   *
191   * @throws org.sonar.api.utils.text.WriterException on any failure
192   */
193  public JsonWriter valueObject(@Nullable Object value) {
194    try {
195      if (value == null) {
196        stream.nullValue();
197      } else {
198        if (value instanceof String) {
199          stream.value((String) value);
200        } else if (value instanceof Number) {
201          stream.value((Number) value);
202        } else if (value instanceof Boolean) {
203          stream.value((Boolean) value);
204        } else if (value instanceof Date) {
205          valueDateTime((Date) value);
206        } else if (value instanceof Map) {
207          stream.beginObject();
208          for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) {
209            stream.name(entry.getKey().toString());
210            valueObject(entry.getValue());
211          }
212          stream.endObject();
213        } else if (value instanceof Iterable) {
214          stream.beginArray();
215          for (Object o : (Iterable<Object>) value) {
216            valueObject(o);
217          }
218          stream.endArray();
219        } else {
220          throw new IllegalArgumentException(getClass() + " does not support encoding of type: " + value.getClass());
221        }
222      }
223      return this;
224    } catch (IllegalArgumentException e) {
225      throw e;
226    } catch (Exception e) {
227      throw rethrow(e);
228    }
229  }
230
231  /**
232   * Write a list of values in an array, for example:
233   * <pre>
234   *   writer.beginArray().values(myValues).endArray();
235   * </pre>
236   *
237   * @throws org.sonar.api.utils.text.WriterException on any failure
238   */
239  public JsonWriter values(Iterable<String> values) {
240    for (String value : values) {
241      value(value);
242    }
243    return this;
244  }
245
246  /**
247   * @throws org.sonar.api.utils.text.WriterException on any failure
248   */
249  public JsonWriter valueDate(@Nullable Date value) {
250    try {
251      stream.value(value == null ? null : DateUtils.formatDate(value));
252      return this;
253    } catch (Exception e) {
254      throw rethrow(e);
255    }
256  }
257
258  public JsonWriter valueDateTime(@Nullable Date value) {
259    try {
260      stream.value(value == null ? null : DateUtils.formatDateTime(value));
261      return this;
262    } catch (Exception e) {
263      throw rethrow(e);
264    }
265  }
266
267  /**
268   * @throws org.sonar.api.utils.text.WriterException on any failure
269   */
270  public JsonWriter value(long value) {
271    try {
272      stream.value(value);
273      return this;
274    } catch (Exception e) {
275      throw rethrow(e);
276    }
277  }
278
279  /**
280   * @throws org.sonar.api.utils.text.WriterException on any failure
281   */
282  public JsonWriter value(@Nullable Number value) {
283    try {
284      stream.value(value);
285      return this;
286    } catch (Exception e) {
287      throw rethrow(e);
288    }
289  }
290
291  /**
292   * Encodes the property name and value. Output is for example <code>"theName":123</code>.
293   *
294   * @throws org.sonar.api.utils.text.WriterException on any failure
295   */
296  public JsonWriter prop(String name, @Nullable Number value) {
297    return name(name).value(value);
298  }
299
300  /**
301   * Encodes the property name and date value (ISO format).
302   * Output is for example <code>"theDate":"2013-01-24"</code>.
303   *
304   * @throws org.sonar.api.utils.text.WriterException on any failure
305   */
306  public JsonWriter propDate(String name, @Nullable Date value) {
307    return name(name).valueDate(value);
308  }
309
310  /**
311   * Encodes the property name and datetime value (ISO format).
312   * Output is for example <code>"theDate":"2013-01-24T13:12:45+01"</code>.
313   *
314   * @throws org.sonar.api.utils.text.WriterException on any failure
315   */
316  public JsonWriter propDateTime(String name, @Nullable Date value) {
317    return name(name).valueDateTime(value);
318  }
319
320  /**
321   * @throws org.sonar.api.utils.text.WriterException on any failure
322   */
323  public JsonWriter prop(String name, @Nullable String value) {
324    return name(name).value(value);
325  }
326
327  /**
328   * @throws org.sonar.api.utils.text.WriterException on any failure
329   */
330  public JsonWriter prop(String name, boolean 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, long 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, double value) {
345    return name(name).value(value);
346  }
347
348  /**
349   * @throws org.sonar.api.utils.text.WriterException on any failure
350   */
351  public void close() {
352    try {
353      stream.close();
354    } catch (Exception e) {
355      throw rethrow(e);
356    }
357  }
358
359  private IllegalStateException rethrow(Exception e) {
360    // stacktrace is not helpful
361    throw new WriterException("Fail to write JSON: " + e.getMessage());
362  }
363}