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 }