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 * 051 * @since 4.2 052 */ 053public 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 public JsonWriter setSerializeNulls(boolean b) { 073 this.stream.setSerializeNulls(b); 074 return this; 075 } 076 077 /** 078 * Begins encoding a new array. Each call to this method must be paired with 079 * a call to {@link #endArray}. Output is <code>[</code>. 080 * 081 * @throws org.sonar.api.utils.text.WriterException on any failure 082 */ 083 public JsonWriter beginArray() { 084 try { 085 stream.beginArray(); 086 return this; 087 } catch (Exception e) { 088 throw rethrow(e); 089 } 090 } 091 092 /** 093 * Ends encoding the current array. Output is <code>]</code>. 094 * 095 * @throws org.sonar.api.utils.text.WriterException on any failure 096 */ 097 public JsonWriter endArray() { 098 try { 099 stream.endArray(); 100 return this; 101 } catch (Exception e) { 102 throw rethrow(e); 103 } 104 } 105 106 /** 107 * Begins encoding a new object. Each call to this method must be paired 108 * with a call to {@link #endObject}. Output is <code>{</code>. 109 * 110 * @throws org.sonar.api.utils.text.WriterException on any failure 111 */ 112 public JsonWriter beginObject() { 113 try { 114 stream.beginObject(); 115 return this; 116 } catch (Exception e) { 117 throw rethrow(e); 118 } 119 } 120 121 /** 122 * Ends encoding the current object. Output is <code>}</code>. 123 * 124 * @throws org.sonar.api.utils.text.WriterException on any failure 125 */ 126 public JsonWriter endObject() { 127 try { 128 stream.endObject(); 129 return this; 130 } catch (Exception e) { 131 throw rethrow(e); 132 } 133 } 134 135 /** 136 * Encodes the property name. Output is <code>"theName":</code>. 137 * 138 * @throws org.sonar.api.utils.text.WriterException on any failure 139 */ 140 public JsonWriter name(String name) { 141 try { 142 stream.name(name); 143 return this; 144 } catch (Exception e) { 145 throw rethrow(e); 146 } 147 } 148 149 /** 150 * Encodes {@code value}. Output is <code>true</code> or <code>false</code>. 151 * 152 * @throws org.sonar.api.utils.text.WriterException on any failure 153 */ 154 public JsonWriter value(boolean value) { 155 try { 156 stream.value(value); 157 return this; 158 } catch (Exception e) { 159 throw rethrow(e); 160 } 161 } 162 163 /** 164 * @throws org.sonar.api.utils.text.WriterException on any failure 165 */ 166 public JsonWriter value(double value) { 167 try { 168 stream.value(value); 169 return this; 170 } catch (Exception e) { 171 throw rethrow(e); 172 } 173 } 174 175 /** 176 * @throws org.sonar.api.utils.text.WriterException on any failure 177 */ 178 public JsonWriter value(@Nullable String value) { 179 try { 180 stream.value(value); 181 return this; 182 } catch (Exception e) { 183 throw rethrow(e); 184 } 185 } 186 187 /** 188 * Encodes an object that can be a : 189 * <ul> 190 * <li>primitive types: String, Number, Boolean</li> 191 * <li>java.util.Date: encoded as datetime (see {@link #valueDateTime(java.util.Date)}</li> 192 * <li><code>Map<Object, Object></code>. Method toString is called for the key.</li> 193 * <li>Iterable</li> 194 * </ul> 195 * 196 * @throws org.sonar.api.utils.text.WriterException on any failure 197 */ 198 public JsonWriter valueObject(@Nullable Object value) { 199 try { 200 if (value == null) { 201 stream.nullValue(); 202 } else { 203 if (value instanceof String) { 204 stream.value((String) value); 205 } else if (value instanceof Number) { 206 stream.value((Number) value); 207 } else if (value instanceof Boolean) { 208 stream.value((Boolean) value); 209 } else if (value instanceof Date) { 210 valueDateTime((Date) value); 211 } else if (value instanceof Enum) { 212 stream.value(((Enum)value).name()); 213 } else if (value instanceof Map) { 214 stream.beginObject(); 215 for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) { 216 stream.name(entry.getKey().toString()); 217 valueObject(entry.getValue()); 218 } 219 stream.endObject(); 220 } else if (value instanceof Iterable) { 221 stream.beginArray(); 222 for (Object o : (Iterable<Object>) value) { 223 valueObject(o); 224 } 225 stream.endArray(); 226 } else { 227 throw new IllegalArgumentException(getClass() + " does not support encoding of type: " + value.getClass()); 228 } 229 } 230 return this; 231 } catch (IllegalArgumentException e) { 232 throw e; 233 } catch (Exception e) { 234 throw rethrow(e); 235 } 236 } 237 238 /** 239 * Write a list of values in an array, for example: 240 * <pre> 241 * writer.beginArray().values(myValues).endArray(); 242 * </pre> 243 * 244 * @throws org.sonar.api.utils.text.WriterException on any failure 245 */ 246 public JsonWriter values(Iterable<String> values) { 247 for (String value : values) { 248 value(value); 249 } 250 return this; 251 } 252 253 /** 254 * @throws org.sonar.api.utils.text.WriterException on any failure 255 */ 256 public JsonWriter valueDate(@Nullable Date value) { 257 try { 258 stream.value(value == null ? null : DateUtils.formatDate(value)); 259 return this; 260 } catch (Exception e) { 261 throw rethrow(e); 262 } 263 } 264 265 public JsonWriter valueDateTime(@Nullable Date value) { 266 try { 267 stream.value(value == null ? null : DateUtils.formatDateTime(value)); 268 return this; 269 } catch (Exception e) { 270 throw rethrow(e); 271 } 272 } 273 274 /** 275 * @throws org.sonar.api.utils.text.WriterException on any failure 276 */ 277 public JsonWriter value(long value) { 278 try { 279 stream.value(value); 280 return this; 281 } catch (Exception e) { 282 throw rethrow(e); 283 } 284 } 285 286 /** 287 * @throws org.sonar.api.utils.text.WriterException on any failure 288 */ 289 public JsonWriter value(@Nullable Number value) { 290 try { 291 stream.value(value); 292 return this; 293 } catch (Exception e) { 294 throw rethrow(e); 295 } 296 } 297 298 /** 299 * Encodes the property name and value. Output is for example <code>"theName":123</code>. 300 * 301 * @throws org.sonar.api.utils.text.WriterException on any failure 302 */ 303 public JsonWriter prop(String name, @Nullable Number value) { 304 return name(name).value(value); 305 } 306 307 /** 308 * Encodes the property name and date value (ISO format). 309 * Output is for example <code>"theDate":"2013-01-24"</code>. 310 * 311 * @throws org.sonar.api.utils.text.WriterException on any failure 312 */ 313 public JsonWriter propDate(String name, @Nullable Date value) { 314 return name(name).valueDate(value); 315 } 316 317 /** 318 * Encodes the property name and datetime value (ISO format). 319 * Output is for example <code>"theDate":"2013-01-24T13:12:45+01"</code>. 320 * 321 * @throws org.sonar.api.utils.text.WriterException on any failure 322 */ 323 public JsonWriter propDateTime(String name, @Nullable Date value) { 324 return name(name).valueDateTime(value); 325 } 326 327 /** 328 * @throws org.sonar.api.utils.text.WriterException on any failure 329 */ 330 public JsonWriter prop(String name, @Nullable String 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, boolean 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, long value) { 345 return name(name).value(value); 346 } 347 348 /** 349 * @throws org.sonar.api.utils.text.WriterException on any failure 350 */ 351 public JsonWriter prop(String name, double value) { 352 return name(name).value(value); 353 } 354 355 /** 356 * @throws org.sonar.api.utils.text.WriterException on any failure 357 */ 358 public void close() { 359 try { 360 stream.close(); 361 } catch (Exception e) { 362 throw rethrow(e); 363 } 364 } 365 366 private IllegalStateException rethrow(Exception e) { 367 // stacktrace is not helpful 368 throw new WriterException("Fail to write JSON: " + e.getMessage()); 369 } 370}