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