001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info AT sonarsource DOT com
005 *
006 * This program 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 * This program 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 java.io.IOException;
023import java.io.Writer;
024import java.util.Date;
025import java.util.Map;
026import javax.annotation.Nullable;
027import org.sonar.api.utils.DateUtils;
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 *   try (JsonWriter jsonWriter = JsonWriter.of(writer)) {
036 *     jsonWriter
037 *       .beginObject()
038 *       .prop("aBoolean", true)
039 *       .prop("aInt", 123)
040 *       .prop("aString", "foo")
041 *       .beginObject().name("aList")
042 *         .beginArray()
043 *           .beginObject().prop("key", "ABC").endObject()
044 *           .beginObject().prop("key", "DEF").endObject()
045 *         .endArray()
046 *       .endObject()
047 *   }
048 * </pre>
049 * 
050 * <p>
051 * By default, null objects are not serialized. To enable {@code null} serialization,
052 * use {@link #setSerializeNulls(boolean)}.
053 * </p>
054 * <p>
055 * By default, empty strings are serialized. To disable empty string serialization,
056 * use {@link #setSerializeEmptys(boolean)}.
057 * </p>
058 * <p>
059 * {@link JsonWriter} implements {@link AutoCloseable} since version 6.3. The
060 * method {@link #close()} closes the underlying writer.
061 * </p>
062 *
063 *
064 * @since 4.2
065 */
066public class JsonWriter implements AutoCloseable {
067
068  private final com.google.gson.stream.JsonWriter stream;
069  private boolean serializeEmptyStrings;
070
071  private JsonWriter(Writer writer) {
072    this.stream = new com.google.gson.stream.JsonWriter(writer);
073    this.stream.setSerializeNulls(false);
074    this.stream.setLenient(false);
075    this.serializeEmptyStrings = true;
076  }
077
078  // for unit testing
079  JsonWriter(com.google.gson.stream.JsonWriter stream) {
080    this.stream = stream;
081  }
082
083  public static JsonWriter of(Writer writer) {
084    return new JsonWriter(writer);
085  }
086
087  public JsonWriter setSerializeNulls(boolean b) {
088    this.stream.setSerializeNulls(b);
089    return this;
090  }
091
092  /**
093   * Enable/disable serialization of properties which value is an empty String.
094   */
095  public JsonWriter setSerializeEmptys(boolean serializeEmptyStrings) {
096    this.serializeEmptyStrings = serializeEmptyStrings;
097    return this;
098  }
099
100  /**
101   * Begins encoding a new array. Each call to this method must be paired with
102   * a call to {@link #endArray}. Output is <code>[</code>.
103   *
104   * @throws org.sonar.api.utils.text.WriterException on any failure
105   */
106  public JsonWriter beginArray() {
107    try {
108      stream.beginArray();
109      return this;
110    } catch (Exception e) {
111      throw rethrow(e);
112    }
113  }
114
115  /**
116   * Ends encoding the current array. Output is <code>]</code>.
117   *
118   * @throws org.sonar.api.utils.text.WriterException on any failure
119   */
120  public JsonWriter endArray() {
121    try {
122      stream.endArray();
123      return this;
124    } catch (Exception e) {
125      throw rethrow(e);
126    }
127  }
128
129  /**
130   * Begins encoding a new object. Each call to this method must be paired
131   * with a call to {@link #endObject}. Output is <code>{</code>.
132   *
133   * @throws org.sonar.api.utils.text.WriterException on any failure
134   */
135  public JsonWriter beginObject() {
136    try {
137      stream.beginObject();
138      return this;
139    } catch (Exception e) {
140      throw rethrow(e);
141    }
142  }
143
144  /**
145   * Ends encoding the current object. Output is <code>}</code>.
146   *
147   * @throws org.sonar.api.utils.text.WriterException on any failure
148   */
149  public JsonWriter endObject() {
150    try {
151      stream.endObject();
152      return this;
153    } catch (Exception e) {
154      throw rethrow(e);
155    }
156  }
157
158  /**
159   * Encodes the property name. Output is <code>"theName":</code>.
160   *
161   * @throws org.sonar.api.utils.text.WriterException on any failure
162   */
163  public JsonWriter name(String name) {
164    try {
165      stream.name(name);
166      return this;
167    } catch (Exception e) {
168      throw rethrow(e);
169    }
170  }
171
172  /**
173   * Encodes {@code value}. Output is <code>true</code> or <code>false</code>.
174   *
175   * @throws org.sonar.api.utils.text.WriterException on any failure
176   */
177  public JsonWriter value(boolean value) {
178    try {
179      stream.value(value);
180      return this;
181    } catch (Exception e) {
182      throw rethrow(e);
183    }
184  }
185
186  /**
187   * @throws org.sonar.api.utils.text.WriterException on any failure
188   */
189  public JsonWriter value(double value) {
190    try {
191      stream.value(value);
192      return this;
193    } catch (Exception e) {
194      throw rethrow(e);
195    }
196  }
197
198  /**
199   * @throws org.sonar.api.utils.text.WriterException on any failure
200   */
201  public JsonWriter value(@Nullable String value) {
202    try {
203      stream.value(serializeEmptyStrings ? value : emptyToNull(value));
204      return this;
205    } catch (Exception e) {
206      throw rethrow(e);
207    }
208  }
209
210  /**
211   * Encodes an object that can be a :
212   * <ul>
213   * <li>primitive types: String, Number, Boolean</li>
214   * <li>java.util.Date: encoded as datetime (see {@link #valueDateTime(java.util.Date)}</li>
215   * <li>{@code Map<Object, Object>}. Method toString is called for the key.</li>
216   * <li>Iterable</li>
217   * </ul>
218   *
219   * @throws org.sonar.api.utils.text.WriterException on any failure
220   */
221  public JsonWriter valueObject(@Nullable Object value) {
222
223    try {
224      if (value == null) {
225        stream.nullValue();
226        return this;
227      }
228      valueNonNullObject(value);
229      return this;
230    } catch (IllegalArgumentException e) {
231      throw e;
232    } catch (Exception e) {
233      throw rethrow(e);
234    }
235  }
236
237  private void valueNonNullObject(Object value) throws IOException {
238    if (value instanceof String) {
239      stream.value(serializeEmptyStrings ? (String) value : emptyToNull((String) value));
240    } else if (value instanceof Number) {
241      stream.value((Number) value);
242    } else if (value instanceof Boolean) {
243      stream.value((Boolean) value);
244    } else if (value instanceof Date) {
245      valueDateTime((Date) value);
246    } else if (value instanceof Enum) {
247      stream.value(((Enum) value).name());
248    } else if (value instanceof Map) {
249      stream.beginObject();
250      for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) {
251        stream.name(entry.getKey().toString());
252        valueObject(entry.getValue());
253      }
254      stream.endObject();
255    } else if (value instanceof Iterable) {
256      stream.beginArray();
257      for (Object o : (Iterable<Object>) value) {
258        valueObject(o);
259      }
260      stream.endArray();
261    } else {
262      throw new IllegalArgumentException(getClass() + " does not support encoding of type: " + value.getClass());
263    }
264  }
265
266  /**
267   * Write a list of values in an array, for example:
268   * <pre>
269   *   writer.beginArray().values(myValues).endArray();
270   * </pre>
271   *
272   * @throws org.sonar.api.utils.text.WriterException on any failure
273   */
274  public JsonWriter values(Iterable<String> values) {
275    for (String value : values) {
276      value(value);
277    }
278    return this;
279  }
280
281  /**
282   * @throws org.sonar.api.utils.text.WriterException on any failure
283   */
284  public JsonWriter valueDate(@Nullable Date value) {
285    try {
286      stream.value(value == null ? null : DateUtils.formatDate(value));
287      return this;
288    } catch (Exception e) {
289      throw rethrow(e);
290    }
291  }
292
293  public JsonWriter valueDateTime(@Nullable Date value) {
294    try {
295      stream.value(value == null ? null : DateUtils.formatDateTime(value));
296      return this;
297    } catch (Exception e) {
298      throw rethrow(e);
299    }
300  }
301
302  /**
303   * @throws org.sonar.api.utils.text.WriterException on any failure
304   */
305  public JsonWriter value(long value) {
306    try {
307      stream.value(value);
308      return this;
309    } catch (Exception e) {
310      throw rethrow(e);
311    }
312  }
313
314  /**
315   * @throws org.sonar.api.utils.text.WriterException on any failure
316   */
317  public JsonWriter value(@Nullable Number value) {
318    try {
319      stream.value(value);
320      return this;
321    } catch (Exception e) {
322      throw rethrow(e);
323    }
324  }
325
326  /**
327   * Encodes the property name and value. Output is for example <code>"theName":123</code>.
328   *
329   * @throws org.sonar.api.utils.text.WriterException on any failure
330   */
331  public JsonWriter prop(String name, @Nullable Number value) {
332    return name(name).value(value);
333  }
334
335  /**
336   * Encodes the property name and date value (ISO format).
337   * Output is for example <code>"theDate":"2013-01-24"</code>.
338   *
339   * @throws org.sonar.api.utils.text.WriterException on any failure
340   */
341  public JsonWriter propDate(String name, @Nullable Date value) {
342    return name(name).valueDate(value);
343  }
344
345  /**
346   * Encodes the property name and datetime value (ISO format).
347   * Output is for example <code>"theDate":"2013-01-24T13:12:45+01"</code>.
348   *
349   * @throws org.sonar.api.utils.text.WriterException on any failure
350   */
351  public JsonWriter propDateTime(String name, @Nullable Date value) {
352    return name(name).valueDateTime(value);
353  }
354
355  /**
356   * @throws org.sonar.api.utils.text.WriterException on any failure
357   */
358  public JsonWriter prop(String name, @Nullable String value) {
359    return name(name).value(value);
360  }
361
362  /**
363   * @throws org.sonar.api.utils.text.WriterException on any failure
364   */
365  public JsonWriter prop(String name, boolean value) {
366    return name(name).value(value);
367  }
368
369  /**
370   * @throws org.sonar.api.utils.text.WriterException on any failure
371   */
372  public JsonWriter prop(String name, long value) {
373    return name(name).value(value);
374  }
375
376  /**
377   * @throws org.sonar.api.utils.text.WriterException on any failure
378   */
379  public JsonWriter prop(String name, double value) {
380    return name(name).value(value);
381  }
382
383  /**
384   * @throws org.sonar.api.utils.text.WriterException on any failure
385   */
386  @Override
387  public void close() {
388    try {
389      stream.close();
390    } catch (Exception e) {
391      throw rethrow(e);
392    }
393  }
394
395  private static IllegalStateException rethrow(Exception e) {
396    throw new WriterException("Fail to write JSON", e);
397  }
398
399  @Nullable
400  private static String emptyToNull(@Nullable String value) {
401    if (value == null || value.isEmpty()) {
402      return null;
403    }
404    return value;
405  }
406}