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 */ 020 package org.sonar.api.utils.text; 021 022 import org.sonar.api.utils.DateUtils; 023 024 import javax.annotation.Nullable; 025 import java.io.Writer; 026 import java.util.Date; 027 import 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 */ 053 public 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 Map) { 207 stream.beginObject(); 208 for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) { 209 stream.name(entry.getKey().toString()); 210 valueObject(entry.getValue()); 211 } 212 stream.endObject(); 213 } else if (value instanceof Iterable) { 214 stream.beginArray(); 215 for (Object o : (Iterable<Object>) value) { 216 valueObject(o); 217 } 218 stream.endArray(); 219 } else { 220 throw new IllegalArgumentException(getClass() + " does not support encoding of type: " + value.getClass()); 221 } 222 } 223 return this; 224 } catch (IllegalArgumentException e) { 225 throw e; 226 } catch (Exception e) { 227 throw rethrow(e); 228 } 229 } 230 231 /** 232 * Write a list of values in an array, for example: 233 * <pre> 234 * writer.beginArray().values(myValues).endArray(); 235 * </pre> 236 * 237 * @throws org.sonar.api.utils.text.WriterException on any failure 238 */ 239 public JsonWriter values(Iterable<String> values) { 240 for (String value : values) { 241 value(value); 242 } 243 return this; 244 } 245 246 /** 247 * @throws org.sonar.api.utils.text.WriterException on any failure 248 */ 249 public JsonWriter valueDate(@Nullable Date value) { 250 try { 251 stream.value(value == null ? null : DateUtils.formatDate(value)); 252 return this; 253 } catch (Exception e) { 254 throw rethrow(e); 255 } 256 } 257 258 public JsonWriter valueDateTime(@Nullable Date value) { 259 try { 260 stream.value(value == null ? null : DateUtils.formatDateTime(value)); 261 return this; 262 } catch (Exception e) { 263 throw rethrow(e); 264 } 265 } 266 267 /** 268 * @throws org.sonar.api.utils.text.WriterException on any failure 269 */ 270 public JsonWriter value(long value) { 271 try { 272 stream.value(value); 273 return this; 274 } catch (Exception e) { 275 throw rethrow(e); 276 } 277 } 278 279 /** 280 * @throws org.sonar.api.utils.text.WriterException on any failure 281 */ 282 public JsonWriter value(@Nullable Number value) { 283 try { 284 stream.value(value); 285 return this; 286 } catch (Exception e) { 287 throw rethrow(e); 288 } 289 } 290 291 /** 292 * Encodes the property name and value. Output is for example <code>"theName":123</code>. 293 * 294 * @throws org.sonar.api.utils.text.WriterException on any failure 295 */ 296 public JsonWriter prop(String name, @Nullable Number value) { 297 return name(name).value(value); 298 } 299 300 /** 301 * Encodes the property name and date value (ISO format). 302 * Output is for example <code>"theDate":"2013-01-24"</code>. 303 * 304 * @throws org.sonar.api.utils.text.WriterException on any failure 305 */ 306 public JsonWriter propDate(String name, @Nullable Date value) { 307 return name(name).valueDate(value); 308 } 309 310 /** 311 * Encodes the property name and datetime value (ISO format). 312 * Output is for example <code>"theDate":"2013-01-24T13:12:45+01"</code>. 313 * 314 * @throws org.sonar.api.utils.text.WriterException on any failure 315 */ 316 public JsonWriter propDateTime(String name, @Nullable Date value) { 317 return name(name).valueDateTime(value); 318 } 319 320 /** 321 * @throws org.sonar.api.utils.text.WriterException on any failure 322 */ 323 public JsonWriter prop(String name, @Nullable String value) { 324 return name(name).value(value); 325 } 326 327 /** 328 * @throws org.sonar.api.utils.text.WriterException on any failure 329 */ 330 public JsonWriter prop(String name, boolean 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, long 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, double value) { 345 return name(name).value(value); 346 } 347 348 /** 349 * @throws org.sonar.api.utils.text.WriterException on any failure 350 */ 351 public void close() { 352 try { 353 stream.close(); 354 } catch (Exception e) { 355 throw rethrow(e); 356 } 357 } 358 359 private IllegalStateException rethrow(Exception e) { 360 // stacktrace is not helpful 361 throw new WriterException("Fail to write JSON: " + e.getMessage()); 362 } 363 }