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    
028    /**
029     * Writes JSON as a stream. This class allows plugins to not directly depend
030     * on the underlying JSON library.
031     *
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     * @since 4.2
051     */
052    public class JsonWriter {
053    
054      private final com.google.gson.stream.JsonWriter stream;
055    
056      private JsonWriter(Writer writer) {
057        this.stream = new com.google.gson.stream.JsonWriter(writer);
058        this.stream.setSerializeNulls(false);
059        this.stream.setLenient(false);
060      }
061    
062      // for unit testing
063      JsonWriter(com.google.gson.stream.JsonWriter stream) {
064        this.stream = stream;
065      }
066    
067      public static JsonWriter of(Writer writer) {
068        return new JsonWriter(writer);
069      }
070    
071      /**
072       * Begins encoding a new array. Each call to this method must be paired with
073       * a call to {@link #endArray}. Output is <code>[</code>.
074       *
075       * @throws org.sonar.api.utils.text.WriterException on any failure
076       */
077      public JsonWriter beginArray() {
078        try {
079          stream.beginArray();
080          return this;
081        } catch (Exception e) {
082          throw rethrow(e);
083        }
084      }
085    
086      /**
087       * Ends encoding the current array. Output is <code>]</code>.
088       *
089       * @throws org.sonar.api.utils.text.WriterException on any failure
090       */
091      public JsonWriter endArray() {
092        try {
093          stream.endArray();
094          return this;
095        } catch (Exception e) {
096          throw rethrow(e);
097        }
098      }
099    
100      /**
101       * Begins encoding a new object. Each call to this method must be paired
102       * with a call to {@link #endObject}. Output is <code>{</code>.
103       *
104       * @throws org.sonar.api.utils.text.WriterException on any failure
105       */
106      public JsonWriter beginObject() {
107        try {
108          stream.beginObject();
109          return this;
110        } catch (Exception e) {
111          throw rethrow(e);
112        }
113      }
114    
115      /**
116       * Ends encoding the current object. Output is <code>}</code>.
117       *
118       * @throws org.sonar.api.utils.text.WriterException on any failure
119       */
120      public JsonWriter endObject() {
121        try {
122          stream.endObject();
123          return this;
124        } catch (Exception e) {
125          throw rethrow(e);
126        }
127      }
128    
129      /**
130       * Encodes the property name. Output is <code>"theName":</code>.
131       *
132       * @throws org.sonar.api.utils.text.WriterException on any failure
133       */
134      public JsonWriter name(String name) {
135        try {
136          stream.name(name);
137          return this;
138        } catch (Exception e) {
139          throw rethrow(e);
140        }
141      }
142    
143      /**
144       * Encodes {@code value}. Output is <code>true</code> or <code>false</code>.
145       *
146       * @throws org.sonar.api.utils.text.WriterException on any failure
147       */
148      public JsonWriter value(boolean value) {
149        try {
150          stream.value(value);
151          return this;
152        } catch (Exception e) {
153          throw rethrow(e);
154        }
155      }
156    
157      /**
158       * @throws org.sonar.api.utils.text.WriterException on any failure
159       */
160      public JsonWriter value(double value) {
161        try {
162          stream.value(value);
163          return this;
164        } catch (Exception e) {
165          throw rethrow(e);
166        }
167      }
168    
169      /**
170       * @throws org.sonar.api.utils.text.WriterException on any failure
171       */
172      public JsonWriter value(@Nullable String 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 valueDate(@Nullable Date value) {
185        try {
186          stream.value(value == null ? null : DateUtils.formatDate(value));
187          return this;
188        } catch (Exception e) {
189          throw rethrow(e);
190        }
191      }
192    
193      public JsonWriter valueDateTime(@Nullable Date value) {
194        try {
195          stream.value(value == null ? null : DateUtils.formatDateTime(value));
196          return this;
197        } catch (Exception e) {
198          throw rethrow(e);
199        }
200      }
201    
202      /**
203       * @throws org.sonar.api.utils.text.WriterException on any failure
204       */
205      public JsonWriter value(long value) {
206        try {
207          stream.value(value);
208          return this;
209        } catch (Exception e) {
210          throw rethrow(e);
211        }
212      }
213    
214      /**
215       * @throws org.sonar.api.utils.text.WriterException on any failure
216       */
217      public JsonWriter value(@Nullable Number value) {
218        try {
219          stream.value(value);
220          return this;
221        } catch (Exception e) {
222          throw rethrow(e);
223        }
224      }
225    
226      /**
227       * Encodes the property name and value. Output is for example <code>"theName":123</code>.
228       *
229       * @throws org.sonar.api.utils.text.WriterException on any failure
230       */
231      public JsonWriter prop(String name, @Nullable Number value) {
232        return name(name).value(value);
233      }
234    
235      /**
236       * Encodes the property name and date value (ISO format).
237       * Output is for example <code>"theDate":"2013-01-24"</code>.
238       *
239       * @throws org.sonar.api.utils.text.WriterException on any failure
240       */
241      public JsonWriter propDate(String name, @Nullable Date value) {
242        return name(name).valueDate(value);
243      }
244    
245      /**
246       * Encodes the property name and datetime value (ISO format).
247       * Output is for example <code>"theDate":"2013-01-24T13:12:45+01"</code>.
248       *
249       * @throws org.sonar.api.utils.text.WriterException on any failure
250       */
251      public JsonWriter propDateTime(String name, @Nullable Date value) {
252        return name(name).valueDateTime(value);
253      }
254    
255      /**
256       * @throws org.sonar.api.utils.text.WriterException on any failure
257       */
258      public JsonWriter prop(String name, @Nullable String value) {
259        return name(name).value(value);
260      }
261    
262      /**
263       * @throws org.sonar.api.utils.text.WriterException on any failure
264       */
265      public JsonWriter prop(String name, boolean value) {
266        return name(name).value(value);
267      }
268    
269      /**
270       * @throws org.sonar.api.utils.text.WriterException on any failure
271       */
272      public JsonWriter prop(String name, long value) {
273        return name(name).value(value);
274      }
275    
276      /**
277       * @throws org.sonar.api.utils.text.WriterException on any failure
278       */
279      public JsonWriter prop(String name, double value) {
280        return name(name).value(value);
281      }
282    
283      /**
284       * @throws org.sonar.api.utils.text.WriterException on any failure
285       */
286      public void close() {
287        try {
288          stream.close();
289        } catch (Exception e) {
290          throw rethrow(e);
291        }
292      }
293    
294      private IllegalStateException rethrow(Exception e) {
295        // stacktrace is not helpful
296        throw new WriterException("Fail to write JSON: " + e.getMessage());
297      }
298    }