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.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 *   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 * <p>
052 * By default, null objects are not serialized. To enable {@code null} serialization,
053 * use {@link #setSerializeNulls(boolean)}.
054 * 
055 * <p>
056 * By default, emptry strings are serialized. To disable empty string serialization,
057 * use {@link #setSerializeEmptys(boolean)}.
058 * 
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>}. 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
219    try {
220      if (value == null) {
221        stream.nullValue();
222        return this;
223      }
224      valueNonNullObject(value);
225      return this;
226    } catch (IllegalArgumentException e) {
227      throw e;
228    } catch (Exception e) {
229      throw rethrow(e);
230    }
231  }
232
233  private void valueNonNullObject(Object value) throws IOException {
234    if (value instanceof String) {
235      stream.value(serializeEmptyStrings ? (String) value : emptyToNull((String) value));
236    } else if (value instanceof Number) {
237      stream.value((Number) value);
238    } else if (value instanceof Boolean) {
239      stream.value((Boolean) value);
240    } else if (value instanceof Date) {
241      valueDateTime((Date) value);
242    } else if (value instanceof Enum) {
243      stream.value(((Enum) value).name());
244    } else if (value instanceof Map) {
245      stream.beginObject();
246      for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) {
247        stream.name(entry.getKey().toString());
248        valueObject(entry.getValue());
249      }
250      stream.endObject();
251    } else if (value instanceof Iterable) {
252      stream.beginArray();
253      for (Object o : (Iterable<Object>) value) {
254        valueObject(o);
255      }
256      stream.endArray();
257    } else {
258      throw new IllegalArgumentException(getClass() + " does not support encoding of type: " + value.getClass());
259    }
260  }
261
262  /**
263   * Write a list of values in an array, for example:
264   * <pre>
265   *   writer.beginArray().values(myValues).endArray();
266   * </pre>
267   *
268   * @throws org.sonar.api.utils.text.WriterException on any failure
269   */
270  public JsonWriter values(Iterable<String> values) {
271    for (String value : values) {
272      value(value);
273    }
274    return this;
275  }
276
277  /**
278   * @throws org.sonar.api.utils.text.WriterException on any failure
279   */
280  public JsonWriter valueDate(@Nullable Date value) {
281    try {
282      stream.value(value == null ? null : DateUtils.formatDate(value));
283      return this;
284    } catch (Exception e) {
285      throw rethrow(e);
286    }
287  }
288
289  public JsonWriter valueDateTime(@Nullable Date value) {
290    try {
291      stream.value(value == null ? null : DateUtils.formatDateTime(value));
292      return this;
293    } catch (Exception e) {
294      throw rethrow(e);
295    }
296  }
297
298  /**
299   * @throws org.sonar.api.utils.text.WriterException on any failure
300   */
301  public JsonWriter value(long value) {
302    try {
303      stream.value(value);
304      return this;
305    } catch (Exception e) {
306      throw rethrow(e);
307    }
308  }
309
310  /**
311   * @throws org.sonar.api.utils.text.WriterException on any failure
312   */
313  public JsonWriter value(@Nullable Number value) {
314    try {
315      stream.value(value);
316      return this;
317    } catch (Exception e) {
318      throw rethrow(e);
319    }
320  }
321
322  /**
323   * Encodes the property name and value. Output is for example <code>"theName":123</code>.
324   *
325   * @throws org.sonar.api.utils.text.WriterException on any failure
326   */
327  public JsonWriter prop(String name, @Nullable Number value) {
328    return name(name).value(value);
329  }
330
331  /**
332   * Encodes the property name and date value (ISO format).
333   * Output is for example <code>"theDate":"2013-01-24"</code>.
334   *
335   * @throws org.sonar.api.utils.text.WriterException on any failure
336   */
337  public JsonWriter propDate(String name, @Nullable Date value) {
338    return name(name).valueDate(value);
339  }
340
341  /**
342   * Encodes the property name and datetime value (ISO format).
343   * Output is for example <code>"theDate":"2013-01-24T13:12:45+01"</code>.
344   *
345   * @throws org.sonar.api.utils.text.WriterException on any failure
346   */
347  public JsonWriter propDateTime(String name, @Nullable Date value) {
348    return name(name).valueDateTime(value);
349  }
350
351  /**
352   * @throws org.sonar.api.utils.text.WriterException on any failure
353   */
354  public JsonWriter prop(String name, @Nullable String value) {
355    return name(name).value(value);
356  }
357
358  /**
359   * @throws org.sonar.api.utils.text.WriterException on any failure
360   */
361  public JsonWriter prop(String name, boolean value) {
362    return name(name).value(value);
363  }
364
365  /**
366   * @throws org.sonar.api.utils.text.WriterException on any failure
367   */
368  public JsonWriter prop(String name, long value) {
369    return name(name).value(value);
370  }
371
372  /**
373   * @throws org.sonar.api.utils.text.WriterException on any failure
374   */
375  public JsonWriter prop(String name, double value) {
376    return name(name).value(value);
377  }
378
379  /**
380   * @throws org.sonar.api.utils.text.WriterException on any failure
381   */
382  public void close() {
383    try {
384      stream.close();
385    } catch (Exception e) {
386      throw rethrow(e);
387    }
388  }
389
390  private static IllegalStateException rethrow(Exception e) {
391    throw new WriterException("Fail to write JSON", e);
392  }
393
394  @Nullable
395  private static String emptyToNull(@Nullable String value) {
396    if (value == null || value.isEmpty()) {
397      return null;
398    }
399    return value;
400  }
401}