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.Writer; 023import java.util.Date; 024import java.util.Map; 025import javax.annotation.Nullable; 026import org.sonar.api.utils.DateUtils; 027 028/** 029 * Writes JSON as a stream. This class allows plugins to not directly depend 030 * on the underlying JSON library. 031 * <p> 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 * <p> 051 * By default, null objects are not serialized. To enable {@code null} serialization, 052 * use {@link #setSerializeNulls(boolean)}. 053 * 054 * <p> 055 * By default, emptry strings are serialized. To disable empty string serialization, 056 * use {@link #setSerializeEmptys(boolean)}. 057 * 058 * 059 * @since 4.2 060 */ 061public class JsonWriter { 062 063 private final com.google.gson.stream.JsonWriter stream; 064 private boolean serializeEmptyStrings; 065 066 private JsonWriter(Writer writer) { 067 this.stream = new com.google.gson.stream.JsonWriter(writer); 068 this.stream.setSerializeNulls(false); 069 this.stream.setLenient(false); 070 this.serializeEmptyStrings = true; 071 } 072 073 // for unit testing 074 JsonWriter(com.google.gson.stream.JsonWriter stream) { 075 this.stream = stream; 076 } 077 078 public static JsonWriter of(Writer writer) { 079 return new JsonWriter(writer); 080 } 081 082 public JsonWriter setSerializeNulls(boolean b) { 083 this.stream.setSerializeNulls(b); 084 return this; 085 } 086 087 /** 088 * Enable/disable serialization of properties which value is an empty String. 089 */ 090 public JsonWriter setSerializeEmptys(boolean serializeEmptyStrings) { 091 this.serializeEmptyStrings = serializeEmptyStrings; 092 return this; 093 } 094 095 /** 096 * Begins encoding a new array. Each call to this method must be paired with 097 * a call to {@link #endArray}. Output is <code>[</code>. 098 * 099 * @throws org.sonar.api.utils.text.WriterException on any failure 100 */ 101 public JsonWriter beginArray() { 102 try { 103 stream.beginArray(); 104 return this; 105 } catch (Exception e) { 106 throw rethrow(e); 107 } 108 } 109 110 /** 111 * Ends encoding the current array. Output is <code>]</code>. 112 * 113 * @throws org.sonar.api.utils.text.WriterException on any failure 114 */ 115 public JsonWriter endArray() { 116 try { 117 stream.endArray(); 118 return this; 119 } catch (Exception e) { 120 throw rethrow(e); 121 } 122 } 123 124 /** 125 * Begins encoding a new object. Each call to this method must be paired 126 * with a call to {@link #endObject}. Output is <code>{</code>. 127 * 128 * @throws org.sonar.api.utils.text.WriterException on any failure 129 */ 130 public JsonWriter beginObject() { 131 try { 132 stream.beginObject(); 133 return this; 134 } catch (Exception e) { 135 throw rethrow(e); 136 } 137 } 138 139 /** 140 * Ends encoding the current object. Output is <code>}</code>. 141 * 142 * @throws org.sonar.api.utils.text.WriterException on any failure 143 */ 144 public JsonWriter endObject() { 145 try { 146 stream.endObject(); 147 return this; 148 } catch (Exception e) { 149 throw rethrow(e); 150 } 151 } 152 153 /** 154 * Encodes the property name. Output is <code>"theName":</code>. 155 * 156 * @throws org.sonar.api.utils.text.WriterException on any failure 157 */ 158 public JsonWriter name(String name) { 159 try { 160 stream.name(name); 161 return this; 162 } catch (Exception e) { 163 throw rethrow(e); 164 } 165 } 166 167 /** 168 * Encodes {@code value}. Output is <code>true</code> or <code>false</code>. 169 * 170 * @throws org.sonar.api.utils.text.WriterException on any failure 171 */ 172 public JsonWriter value(boolean 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 value(double value) { 185 try { 186 stream.value(value); 187 return this; 188 } catch (Exception e) { 189 throw rethrow(e); 190 } 191 } 192 193 /** 194 * @throws org.sonar.api.utils.text.WriterException on any failure 195 */ 196 public JsonWriter value(@Nullable String value) { 197 try { 198 stream.value(serializeEmptyStrings ? value : emptyToNull(value)); 199 return this; 200 } catch (Exception e) { 201 throw rethrow(e); 202 } 203 } 204 205 /** 206 * Encodes an object that can be a : 207 * <ul> 208 * <li>primitive types: String, Number, Boolean</li> 209 * <li>java.util.Date: encoded as datetime (see {@link #valueDateTime(java.util.Date)}</li> 210 * <li>{@code Map<Object, Object>}. Method toString is called for the key.</li> 211 * <li>Iterable</li> 212 * </ul> 213 * 214 * @throws org.sonar.api.utils.text.WriterException on any failure 215 */ 216 public JsonWriter valueObject(@Nullable Object value) { 217 try { 218 if (value == null) { 219 stream.nullValue(); 220 } else { 221 if (value instanceof String) { 222 stream.value(serializeEmptyStrings ? (String) value : emptyToNull((String) value)); 223 } else if (value instanceof Number) { 224 stream.value((Number) value); 225 } else if (value instanceof Boolean) { 226 stream.value((Boolean) value); 227 } else if (value instanceof Date) { 228 valueDateTime((Date) value); 229 } else if (value instanceof Enum) { 230 stream.value(((Enum) value).name()); 231 } else if (value instanceof Map) { 232 stream.beginObject(); 233 for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) { 234 stream.name(entry.getKey().toString()); 235 valueObject(entry.getValue()); 236 } 237 stream.endObject(); 238 } else if (value instanceof Iterable) { 239 stream.beginArray(); 240 for (Object o : (Iterable<Object>) value) { 241 valueObject(o); 242 } 243 stream.endArray(); 244 } else { 245 throw new IllegalArgumentException(getClass() + " does not support encoding of type: " + value.getClass()); 246 } 247 } 248 return this; 249 } catch (IllegalArgumentException e) { 250 throw e; 251 } catch (Exception e) { 252 throw rethrow(e); 253 } 254 } 255 256 /** 257 * Write a list of values in an array, for example: 258 * <pre> 259 * writer.beginArray().values(myValues).endArray(); 260 * </pre> 261 * 262 * @throws org.sonar.api.utils.text.WriterException on any failure 263 */ 264 public JsonWriter values(Iterable<String> values) { 265 for (String value : values) { 266 value(value); 267 } 268 return this; 269 } 270 271 /** 272 * @throws org.sonar.api.utils.text.WriterException on any failure 273 */ 274 public JsonWriter valueDate(@Nullable Date value) { 275 try { 276 stream.value(value == null ? null : DateUtils.formatDate(value)); 277 return this; 278 } catch (Exception e) { 279 throw rethrow(e); 280 } 281 } 282 283 public JsonWriter valueDateTime(@Nullable Date value) { 284 try { 285 stream.value(value == null ? null : DateUtils.formatDateTime(value)); 286 return this; 287 } catch (Exception e) { 288 throw rethrow(e); 289 } 290 } 291 292 /** 293 * @throws org.sonar.api.utils.text.WriterException on any failure 294 */ 295 public JsonWriter value(long value) { 296 try { 297 stream.value(value); 298 return this; 299 } catch (Exception e) { 300 throw rethrow(e); 301 } 302 } 303 304 /** 305 * @throws org.sonar.api.utils.text.WriterException on any failure 306 */ 307 public JsonWriter value(@Nullable Number value) { 308 try { 309 stream.value(value); 310 return this; 311 } catch (Exception e) { 312 throw rethrow(e); 313 } 314 } 315 316 /** 317 * Encodes the property name and value. Output is for example <code>"theName":123</code>. 318 * 319 * @throws org.sonar.api.utils.text.WriterException on any failure 320 */ 321 public JsonWriter prop(String name, @Nullable Number value) { 322 return name(name).value(value); 323 } 324 325 /** 326 * Encodes the property name and date value (ISO format). 327 * Output is for example <code>"theDate":"2013-01-24"</code>. 328 * 329 * @throws org.sonar.api.utils.text.WriterException on any failure 330 */ 331 public JsonWriter propDate(String name, @Nullable Date value) { 332 return name(name).valueDate(value); 333 } 334 335 /** 336 * Encodes the property name and datetime value (ISO format). 337 * Output is for example <code>"theDate":"2013-01-24T13:12:45+01"</code>. 338 * 339 * @throws org.sonar.api.utils.text.WriterException on any failure 340 */ 341 public JsonWriter propDateTime(String name, @Nullable Date value) { 342 return name(name).valueDateTime(value); 343 } 344 345 /** 346 * @throws org.sonar.api.utils.text.WriterException on any failure 347 */ 348 public JsonWriter prop(String name, @Nullable String value) { 349 return name(name).value(value); 350 } 351 352 /** 353 * @throws org.sonar.api.utils.text.WriterException on any failure 354 */ 355 public JsonWriter prop(String name, boolean value) { 356 return name(name).value(value); 357 } 358 359 /** 360 * @throws org.sonar.api.utils.text.WriterException on any failure 361 */ 362 public JsonWriter prop(String name, long value) { 363 return name(name).value(value); 364 } 365 366 /** 367 * @throws org.sonar.api.utils.text.WriterException on any failure 368 */ 369 public JsonWriter prop(String name, double value) { 370 return name(name).value(value); 371 } 372 373 /** 374 * @throws org.sonar.api.utils.text.WriterException on any failure 375 */ 376 public void close() { 377 try { 378 stream.close(); 379 } catch (Exception e) { 380 throw rethrow(e); 381 } 382 } 383 384 private static IllegalStateException rethrow(Exception e) { 385 throw new WriterException("Fail to write JSON", e); 386 } 387 388 @Nullable 389 private static String emptyToNull(@Nullable String value) { 390 if (value == null || value.isEmpty()) { 391 return null; 392 } 393 return value; 394 } 395}