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 Enum) {
207          stream.value(((Enum)value).name());
208        } else if (value instanceof Map) {
209          stream.beginObject();
210          for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) {
211            stream.name(entry.getKey().toString());
212            valueObject(entry.getValue());
213          }
214          stream.endObject();
215        } else if (value instanceof Iterable) {
216          stream.beginArray();
217          for (Object o : (Iterable<Object>) value) {
218            valueObject(o);
219          }
220          stream.endArray();
221        } else {
222          throw new IllegalArgumentException(getClass() + " does not support encoding of type: " + value.getClass());
223        }
224      }
225      return this;
226    } catch (IllegalArgumentException e) {
227      throw e;
228    } catch (Exception e) {
229      throw rethrow(e);
230    }
231  }
232
233  /**
234   * Write a list of values in an array, for example:
235   * <pre>
236   *   writer.beginArray().values(myValues).endArray();
237   * </pre>
238   *
239   * @throws org.sonar.api.utils.text.WriterException on any failure
240   */
241  public JsonWriter values(Iterable<String> values) {
242    for (String value : values) {
243      value(value);
244    }
245    return this;
246  }
247
248  /**
249   * @throws org.sonar.api.utils.text.WriterException on any failure
250   */
251  public JsonWriter valueDate(@Nullable Date value) {
252    try {
253      stream.value(value == null ? null : DateUtils.formatDate(value));
254      return this;
255    } catch (Exception e) {
256      throw rethrow(e);
257    }
258  }
259
260  public JsonWriter valueDateTime(@Nullable Date value) {
261    try {
262      stream.value(value == null ? null : DateUtils.formatDateTime(value));
263      return this;
264    } catch (Exception e) {
265      throw rethrow(e);
266    }
267  }
268
269  /**
270   * @throws org.sonar.api.utils.text.WriterException on any failure
271   */
272  public JsonWriter value(long value) {
273    try {
274      stream.value(value);
275      return this;
276    } catch (Exception e) {
277      throw rethrow(e);
278    }
279  }
280
281  /**
282   * @throws org.sonar.api.utils.text.WriterException on any failure
283   */
284  public JsonWriter value(@Nullable Number value) {
285    try {
286      stream.value(value);
287      return this;
288    } catch (Exception e) {
289      throw rethrow(e);
290    }
291  }
292
293  /**
294   * Encodes the property name and value. Output is for example <code>"theName":123</code>.
295   *
296   * @throws org.sonar.api.utils.text.WriterException on any failure
297   */
298  public JsonWriter prop(String name, @Nullable Number value) {
299    return name(name).value(value);
300  }
301
302  /**
303   * Encodes the property name and date value (ISO format).
304   * Output is for example <code>"theDate":"2013-01-24"</code>.
305   *
306   * @throws org.sonar.api.utils.text.WriterException on any failure
307   */
308  public JsonWriter propDate(String name, @Nullable Date value) {
309    return name(name).valueDate(value);
310  }
311
312  /**
313   * Encodes the property name and datetime value (ISO format).
314   * Output is for example <code>"theDate":"2013-01-24T13:12:45+01"</code>.
315   *
316   * @throws org.sonar.api.utils.text.WriterException on any failure
317   */
318  public JsonWriter propDateTime(String name, @Nullable Date value) {
319    return name(name).valueDateTime(value);
320  }
321
322  /**
323   * @throws org.sonar.api.utils.text.WriterException on any failure
324   */
325  public JsonWriter prop(String name, @Nullable String value) {
326    return name(name).value(value);
327  }
328
329  /**
330   * @throws org.sonar.api.utils.text.WriterException on any failure
331   */
332  public JsonWriter prop(String name, boolean value) {
333    return name(name).value(value);
334  }
335
336  /**
337   * @throws org.sonar.api.utils.text.WriterException on any failure
338   */
339  public JsonWriter prop(String name, long value) {
340    return name(name).value(value);
341  }
342
343  /**
344   * @throws org.sonar.api.utils.text.WriterException on any failure
345   */
346  public JsonWriter prop(String name, double value) {
347    return name(name).value(value);
348  }
349
350  /**
351   * @throws org.sonar.api.utils.text.WriterException on any failure
352   */
353  public void close() {
354    try {
355      stream.close();
356    } catch (Exception e) {
357      throw rethrow(e);
358    }
359  }
360
361  private IllegalStateException rethrow(Exception e) {
362    // stacktrace is not helpful
363    throw new WriterException("Fail to write JSON: " + e.getMessage());
364  }
365}