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