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