1 /******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16 17 package com.badlogic.gdx.utils; 18 19 import java.io.Closeable; 20 import java.io.DataOutputStream; 21 import java.io.IOException; 22 import java.io.OutputStream; 23 24 /** Builder style API for emitting UBJSON. 25 * @author Justin Shapcott */ 26 public class UBJsonWriter implements Closeable { 27 28 final DataOutputStream out; 29 30 private JsonObject current; 31 private boolean named; 32 private final Array<JsonObject> stack = new Array(); 33 34 public UBJsonWriter (OutputStream out) { 35 if (!(out instanceof DataOutputStream)) out = new DataOutputStream(out); 36 this.out = (DataOutputStream)out; 37 } 38 39 /** Begins a new object container. To finish the object call {@link #pop()}. 40 * @return This writer, for chaining */ 41 public UBJsonWriter object () throws IOException { 42 if (current != null) { 43 if (!current.array) { 44 if (!named) throw new IllegalStateException("Name must be set."); 45 named = false; 46 } 47 } 48 stack.add(current = new JsonObject(false)); 49 return this; 50 } 51 52 /** Begins a new named object container, having the given name. To finish the object call {@link #pop()}. 53 * @return This writer, for chaining */ 54 public UBJsonWriter object (String name) throws IOException { 55 name(name).object(); 56 return this; 57 } 58 59 /** Begins a new array container. To finish the array call {@link #pop()}. 60 * @return this writer, for chaining. */ 61 public UBJsonWriter array () throws IOException { 62 if (current != null) { 63 if (!current.array) { 64 if (!named) throw new IllegalStateException("Name must be set."); 65 named = false; 66 } 67 } 68 stack.add(current = new JsonObject(true)); 69 return this; 70 } 71 72 /** Begins a new named array container, having the given name. To finish the array call {@link #pop()}. 73 * @return this writer, for chaining. */ 74 public UBJsonWriter array (String name) throws IOException { 75 name(name).array(); 76 return this; 77 } 78 79 /** Appends a name for the next object, array, or value. 80 * @return this writer, for chaining */ 81 public UBJsonWriter name (String name) throws IOException { 82 if (current == null || current.array) throw new IllegalStateException("Current item must be an object."); 83 byte[] bytes = name.getBytes("UTF-8"); 84 if (bytes.length <= Byte.MAX_VALUE) { 85 out.writeByte('i'); 86 out.writeByte(bytes.length); 87 } else if (bytes.length <= Short.MAX_VALUE) { 88 out.writeByte('I'); 89 out.writeShort(bytes.length); 90 } else { 91 out.writeByte('l'); 92 out.writeInt(bytes.length); 93 } 94 out.write(bytes); 95 named = true; 96 return this; 97 } 98 99 /** Appends a {@code byte} value to the stream. This corresponds to the {@code int8} value type in the UBJSON specification. 100 * @return this writer, for chaining */ 101 public UBJsonWriter value (byte value) throws IOException { 102 checkName(); 103 out.writeByte('i'); 104 out.writeByte(value); 105 return this; 106 } 107 108 /** Appends a {@code short} value to the stream. This corresponds to the {@code int16} value type in the UBJSON specification. 109 * @return this writer, for chaining */ 110 public UBJsonWriter value (short value) throws IOException { 111 checkName(); 112 out.writeByte('I'); 113 out.writeShort(value); 114 return this; 115 } 116 117 /** Appends an {@code int} value to the stream. This corresponds to the {@code int32} value type in the UBJSON specification. 118 * @return this writer, for chaining */ 119 public UBJsonWriter value (int value) throws IOException { 120 checkName(); 121 out.writeByte('l'); 122 out.writeInt(value); 123 return this; 124 } 125 126 /** Appends a {@code long} value to the stream. This corresponds to the {@code int64} value type in the UBJSON specification. 127 * @return this writer, for chaining */ 128 public UBJsonWriter value (long value) throws IOException { 129 checkName(); 130 out.writeByte('L'); 131 out.writeLong(value); 132 return this; 133 } 134 135 /** Appends a {@code float} value to the stream. This corresponds to the {@code float32} value type in the UBJSON specification. 136 * @return this writer, for chaining */ 137 public UBJsonWriter value (float value) throws IOException { 138 checkName(); 139 out.writeByte('d'); 140 out.writeFloat(value); 141 return this; 142 } 143 144 /** Appends a {@code double} value to the stream. This corresponds to the {@code float64} value type in the UBJSON 145 * specification. 146 * @return this writer, for chaining */ 147 public UBJsonWriter value (double value) throws IOException { 148 checkName(); 149 out.writeByte('D'); 150 out.writeDouble(value); 151 return this; 152 } 153 154 /** Appends a {@code boolean} value to the stream. This corresponds to the {@code boolean} value type in the UBJSON 155 * specification. 156 * @return this writer, for chaining */ 157 public UBJsonWriter value (boolean value) throws IOException { 158 checkName(); 159 out.writeByte(value ? 'T' : 'F'); 160 return this; 161 } 162 163 /** Appends a {@code char} value to the stream. Because, in Java, a {@code char} is 16 bytes, this corresponds to the 164 * {@code int16} value type in the UBJSON specification. 165 * @return this writer, for chaining */ 166 public UBJsonWriter value (char value) throws IOException { 167 checkName(); 168 out.writeByte('I'); 169 out.writeChar(value); 170 return this; 171 } 172 173 /** Appends a {@code String} value to the stream. This corresponds to the {@code string} value type in the UBJSON specification. 174 * @return this writer, for chaining */ 175 public UBJsonWriter value (String value) throws IOException { 176 checkName(); 177 byte[] bytes = value.getBytes("UTF-8"); 178 out.writeByte('S'); 179 if (bytes.length <= Byte.MAX_VALUE) { 180 out.writeByte('i'); 181 out.writeByte(bytes.length); 182 } else if (bytes.length <= Short.MAX_VALUE) { 183 out.writeByte('I'); 184 out.writeShort(bytes.length); 185 } else { 186 out.writeByte('l'); 187 out.writeInt(bytes.length); 188 } 189 out.write(bytes); 190 return this; 191 } 192 193 /** Appends an optimized {@code byte array} value to the stream. As an optimized array, the {@code int8} value type marker and 194 * element count are encoded once at the array marker instead of repeating the type marker for each element. 195 * @return this writer, for chaining */ 196 public UBJsonWriter value (byte[] values) throws IOException { 197 array(); 198 out.writeByte('$'); 199 out.writeByte('i'); 200 out.writeByte('#'); 201 value(values.length); 202 for (int i = 0, n = values.length; i < n; i++) { 203 out.writeByte(values[i]); 204 } 205 pop(true); 206 return this; 207 } 208 209 /** Appends an optimized {@code short array} value to the stream. As an optimized array, the {@code int16} value type marker and 210 * element count are encoded once at the array marker instead of repeating the type marker for each element. 211 * @return this writer, for chaining */ 212 public UBJsonWriter value (short[] values) throws IOException { 213 array(); 214 out.writeByte('$'); 215 out.writeByte('I'); 216 out.writeByte('#'); 217 value(values.length); 218 for (int i = 0, n = values.length; i < n; i++) { 219 out.writeShort(values[i]); 220 } 221 pop(true); 222 return this; 223 } 224 225 /** Appends an optimized {@code int array} value to the stream. As an optimized array, the {@code int32} value type marker and 226 * element count are encoded once at the array marker instead of repeating the type marker for each element. 227 * @return this writer, for chaining */ 228 public UBJsonWriter value (int[] values) throws IOException { 229 array(); 230 out.writeByte('$'); 231 out.writeByte('l'); 232 out.writeByte('#'); 233 value(values.length); 234 for (int i = 0, n = values.length; i < n; i++) { 235 out.writeInt(values[i]); 236 } 237 pop(true); 238 return this; 239 } 240 241 /** Appends an optimized {@code long array} value to the stream. As an optimized array, the {@code int64} value type marker and 242 * element count are encoded once at the array marker instead of repeating the type marker for each element. 243 * @return this writer, for chaining */ 244 public UBJsonWriter value (long[] values) throws IOException { 245 array(); 246 out.writeByte('$'); 247 out.writeByte('L'); 248 out.writeByte('#'); 249 value(values.length); 250 for (int i = 0, n = values.length; i < n; i++) { 251 out.writeLong(values[i]); 252 } 253 pop(true); 254 return this; 255 } 256 257 /** Appends an optimized {@code float array} value to the stream. As an optimized array, the {@code float32} value type marker 258 * and element count are encoded once at the array marker instead of repeating the type marker for each element. 259 * @return this writer, for chaining */ 260 public UBJsonWriter value (float[] values) throws IOException { 261 array(); 262 out.writeByte('$'); 263 out.writeByte('d'); 264 out.writeByte('#'); 265 value(values.length); 266 for (int i = 0, n = values.length; i < n; i++) { 267 out.writeFloat(values[i]); 268 } 269 pop(true); 270 return this; 271 } 272 273 /** Appends an optimized {@code double array} value to the stream. As an optimized array, the {@code float64} value type marker 274 * and element count are encoded once at the array marker instead of repeating the type marker for each element. element count 275 * are encoded once at the array marker instead of for each element. 276 * @return this writer, for chaining */ 277 public UBJsonWriter value (double[] values) throws IOException { 278 array(); 279 out.writeByte('$'); 280 out.writeByte('D'); 281 out.writeByte('#'); 282 value(values.length); 283 for (int i = 0, n = values.length; i < n; i++) { 284 out.writeDouble(values[i]); 285 } 286 pop(true); 287 return this; 288 } 289 290 /** Appends a {@code boolean array} value to the stream. 291 * @return this writer, for chaining */ 292 public UBJsonWriter value (boolean[] values) throws IOException { 293 array(); 294 for (int i = 0, n = values.length; i < n; i++) { 295 out.writeByte(values[i] ? 'T' : 'F'); 296 } 297 pop(); 298 return this; 299 } 300 301 /** Appends an optimized {@code char array} value to the stream. As an optimized array, the {@code int16} value type marker and 302 * element count are encoded once at the array marker instead of repeating the type marker for each element. 303 * @return this writer, for chaining */ 304 public UBJsonWriter value (char[] values) throws IOException { 305 array(); 306 out.writeByte('$'); 307 out.writeByte('C'); 308 out.writeByte('#'); 309 value(values.length); 310 for (int i = 0, n = values.length; i < n; i++) { 311 out.writeChar(values[i]); 312 } 313 pop(true); 314 return this; 315 } 316 317 /** Appends an optimized {@code String array} value to the stream. As an optimized array, the {@code String} value type marker 318 * and element count are encoded once at the array marker instead of repeating the type marker for each element. 319 * @return this writer, for chaining */ 320 public UBJsonWriter value (String[] values) throws IOException { 321 array(); 322 out.writeByte('$'); 323 out.writeByte('S'); 324 out.writeByte('#'); 325 value(values.length); 326 for (int i = 0, n = values.length; i < n; i++) { 327 byte[] bytes = values[i].getBytes("UTF-8"); 328 if (bytes.length <= Byte.MAX_VALUE) { 329 out.writeByte('i'); 330 out.writeByte(bytes.length); 331 } else if (bytes.length <= Short.MAX_VALUE) { 332 out.writeByte('I'); 333 out.writeShort(bytes.length); 334 } else { 335 out.writeByte('l'); 336 out.writeInt(bytes.length); 337 } 338 out.write(bytes); 339 } 340 pop(true); 341 return this; 342 } 343 344 /** Appends the given JsonValue, including all its fields recursively, to the stream. 345 * @return this writer, for chaining */ 346 public UBJsonWriter value (JsonValue value) throws IOException { 347 if (value.isObject()) { 348 if (value.name != null) 349 object(value.name); 350 else 351 object(); 352 for (JsonValue child = value.child; child != null; child = child.next) 353 value(child); 354 pop(); 355 } else if (value.isArray()) { 356 if (value.name != null) 357 array(value.name); 358 else 359 array(); 360 for (JsonValue child = value.child; child != null; child = child.next) 361 value(child); 362 pop(); 363 } else if (value.isBoolean()) { 364 if (value.name != null) name(value.name); 365 value(value.asBoolean()); 366 } else if (value.isDouble()) { 367 if (value.name != null) name(value.name); 368 value(value.asDouble()); 369 } else if (value.isLong()) { 370 if (value.name != null) name(value.name); 371 value(value.asLong()); 372 } else if (value.isString()) { 373 if (value.name != null) name(value.name); 374 value(value.asString()); 375 } else if (value.isNull()) { 376 if (value.name != null) name(value.name); 377 value(); 378 } else { 379 throw new IOException("Unhandled JsonValue type"); 380 } 381 return this; 382 } 383 384 /** Appends the object to the stream, if it is a known value type. This is a convenience method that calls through to the 385 * appropriate value method. 386 * @return this writer, for chaining */ 387 public UBJsonWriter value (Object object) throws IOException { 388 if (object == null) { 389 return value(); 390 } else if (object instanceof Number) { 391 Number number = (Number)object; 392 if (object instanceof Byte) return value(number.byteValue()); 393 if (object instanceof Short) return value(number.shortValue()); 394 if (object instanceof Integer) return value(number.intValue()); 395 if (object instanceof Long) return value(number.longValue()); 396 if (object instanceof Float) return value(number.floatValue()); 397 if (object instanceof Double) return value(number.doubleValue()); 398 } else if (object instanceof Character) { 399 return value(((Character)object).charValue()); 400 } else if (object instanceof CharSequence) { 401 return value(object.toString()); 402 } else 403 throw new IOException("Unknown object type."); 404 405 return this; 406 } 407 408 /** Appends a {@code null} value to the stream. 409 * @return this writer, for chaining */ 410 public UBJsonWriter value () throws IOException { 411 checkName(); 412 out.writeByte('Z'); 413 return this; 414 } 415 416 /** Appends a named {@code byte} value to the stream. 417 * @return this writer, for chaining */ 418 public UBJsonWriter set (String name, byte value) throws IOException { 419 return name(name).value(value); 420 } 421 422 /** Appends a named {@code short} value to the stream. 423 * @return this writer, for chaining */ 424 public UBJsonWriter set (String name, short value) throws IOException { 425 return name(name).value(value); 426 } 427 428 /** Appends a named {@code int} value to the stream. 429 * @return this writer, for chaining */ 430 public UBJsonWriter set (String name, int value) throws IOException { 431 return name(name).value(value); 432 } 433 434 /** Appends a named {@code long} value to the stream. 435 * @return this writer, for chaining */ 436 public UBJsonWriter set (String name, long value) throws IOException { 437 return name(name).value(value); 438 } 439 440 /** Appends a named {@code float} value to the stream. 441 * @return this writer, for chaining */ 442 public UBJsonWriter set (String name, float value) throws IOException { 443 return name(name).value(value); 444 } 445 446 /** Appends a named {@code double} value to the stream. 447 * @return this writer, for chaining */ 448 public UBJsonWriter set (String name, double value) throws IOException { 449 return name(name).value(value); 450 } 451 452 /** Appends a named {@code boolean} value to the stream. 453 * @return this writer, for chaining */ 454 public UBJsonWriter set (String name, boolean value) throws IOException { 455 return name(name).value(value); 456 } 457 458 /** Appends a named {@code char} value to the stream. 459 * @return this writer, for chaining */ 460 public UBJsonWriter set (String name, char value) throws IOException { 461 return name(name).value(value); 462 } 463 464 /** Appends a named {@code String} value to the stream. 465 * @return this writer, for chaining */ 466 public UBJsonWriter set (String name, String value) throws IOException { 467 return name(name).value(value); 468 } 469 470 /** Appends a named {@code byte} array value to the stream. 471 * @return this writer, for chaining */ 472 public UBJsonWriter set (String name, byte[] value) throws IOException { 473 return name(name).value(value); 474 } 475 476 /** Appends a named {@code short} array value to the stream. 477 * @return this writer, for chaining */ 478 public UBJsonWriter set (String name, short[] value) throws IOException { 479 return name(name).value(value); 480 } 481 482 /** Appends a named {@code int} array value to the stream. 483 * @return this writer, for chaining */ 484 public UBJsonWriter set (String name, int[] value) throws IOException { 485 return name(name).value(value); 486 } 487 488 /** Appends a named {@code long} array value to the stream. 489 * @return this writer, for chaining */ 490 public UBJsonWriter set (String name, long[] value) throws IOException { 491 return name(name).value(value); 492 } 493 494 /** Appends a named {@code float} array value to the stream. 495 * @return this writer, for chaining */ 496 public UBJsonWriter set (String name, float[] value) throws IOException { 497 return name(name).value(value); 498 } 499 500 /** Appends a named {@code double} array value to the stream. 501 * @return this writer, for chaining */ 502 public UBJsonWriter set (String name, double[] value) throws IOException { 503 return name(name).value(value); 504 } 505 506 /** Appends a named {@code boolean} array value to the stream. 507 * @return this writer, for chaining */ 508 public UBJsonWriter set (String name, boolean[] value) throws IOException { 509 return name(name).value(value); 510 } 511 512 /** Appends a named {@code char} array value to the stream. 513 * @return this writer, for chaining */ 514 public UBJsonWriter set (String name, char[] value) throws IOException { 515 return name(name).value(value); 516 } 517 518 /** Appends a named {@code String} array value to the stream. 519 * @return this writer, for chaining */ 520 public UBJsonWriter set (String name, String[] value) throws IOException { 521 return name(name).value(value); 522 } 523 524 /** Appends a named {@code null} array value to the stream. 525 * @return this writer, for chaining */ 526 public UBJsonWriter set (String name) throws IOException { 527 return name(name).value(); 528 } 529 530 private void checkName () { 531 if (current != null) { 532 if (!current.array) { 533 if (!named) throw new IllegalStateException("Name must be set."); 534 named = false; 535 } 536 } 537 } 538 539 /** Ends the current object or array and pops it off of the element stack. 540 * @return This writer, for chaining */ 541 public UBJsonWriter pop () throws IOException { 542 return pop(false); 543 } 544 545 protected UBJsonWriter pop (boolean silent) throws IOException { 546 if (named) throw new IllegalStateException("Expected an object, array, or value since a name was set."); 547 if (silent) 548 stack.pop(); 549 else 550 stack.pop().close(); 551 current = stack.size == 0 ? null : stack.peek(); 552 return this; 553 } 554 555 /** Flushes the underlying stream. This forces any buffered output bytes to be written out to the stream. */ 556 public void flush () throws IOException { 557 out.flush(); 558 } 559 560 /** Closes the underlying output stream and releases any system resources associated with the stream. */ 561 public void close () throws IOException { 562 while (stack.size > 0) 563 pop(); 564 out.close(); 565 } 566 567 private class JsonObject { 568 final boolean array; 569 570 JsonObject (boolean array) throws IOException { 571 this.array = array; 572 out.writeByte(array ? '[' : '{'); 573 } 574 575 void close () throws IOException { 576 out.writeByte(array ? ']' : '}'); 577 } 578 } 579 580 } 581