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