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