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