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