1/* 2 * Copyright (c) 2021-2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16package escompat 17 18export class JSON { 19 /** 20 * Converts byte to JSON format 21 * 22 * @param d: byte - byte to be converted to a JSON as a Number 23 * 24 * @returns String - JSON representation of byte 25 */ 26 public static stringify(d: byte): String { 27 return StringBuilder.toString(d) 28 } 29 30 /** 31 * Converts short to JSON format 32 * 33 * @param d: short - short to be converted to a JSON as a Number 34 * 35 * @returns String - JSON representation of short 36 */ 37 public static stringify(d: short): String { 38 return StringBuilder.toString(d) 39 } 40 41 /** 42 * Converts int to JSON format 43 * 44 * @param d: int - int to be converted to a JSON as a Number 45 * 46 * @returns String - JSON representation of int 47 */ 48 public static stringify(d: int): String { 49 return StringBuilder.toString(d) 50 } 51 52 /** 53 * Converts long to JSON format 54 * 55 * @param d: long - long to be converted to a JSON as a Number 56 * 57 * @returns String - JSON representation of long 58 */ 59 public static stringify(d: long): String { 60 return StringBuilder.toString(d) 61 } 62 63 /** 64 * Converts float to JSON format 65 * 66 * @param d: float - float to be converted to a JSON as a Number 67 * 68 * @returns String - JSON representation of float 69 */ 70 public static stringify(d: float): String { 71 if (Float.isFinite(d)) { 72 return StringBuilder.toString(d) 73 } else { 74 return "null" 75 } 76 } 77 78 /** 79 * Converts double to JSON format 80 * 81 * @param d: double - double to be converted to a JSON as a Number 82 * 83 * @returns String - JSON representation of double 84 */ 85 public static stringify(d: double): String { 86 if (Double.isFinite(d)) { 87 return StringBuilder.toString(d) 88 } else { 89 return "null" 90 } 91 } 92 93 /** 94 * Converts char to JSON format 95 * 96 * @param d: char - char to be converted to a JSON as a String 97 * 98 * @returns String - JSON representation of char 99 */ 100 public static stringify(d: char): String { 101 return "\"" + StringBuilder.toString(d) + "\"" 102 } 103 104 /** 105 * Converts boolean to JSON format 106 * 107 * @param d: boolean - boolean to be converted to a JSON as a Boolean literal 108 * 109 * @returns String - JSON representation of boolean 110 */ 111 public static stringify(d: boolean): String { 112 if (d) { 113 return "true" 114 } 115 return "false" 116 } 117 118 private static readonly ESCAPED_CHARS: char[] = [c'\"', c'\\', c'\/', c'\b', c'\f', c'\n', c'\r', c'\t'] 119 private static readonly NON_ESCAPED_CHARS: char[] = [c'"', c'\\', c'/', c'b', c'f', c'n', c'r', c't'] 120 private static readonly JSON_SPACE_INDENT_LIMIT = 10 121 122 /** 123 * Converts String to JSON format 124 * 125 * @param d: String - byte to be converted to a JSON as a String 126 * 127 * @returns String - JSON representation of byte 128 */ 129 public static stringify(d: String): String { 130 let sb = new StringBuilder([c'\"']) 131 let len = d.getLength() 132 133 let prevChar: char = 0 134 for (let i = 0; i < len; ++i) { 135 let currChar = d.charAt(i) 136 137 if (Char.isHighSurrogate(currChar)) { 138 if (i != 0 && Char.isHighSurrogate(prevChar)) { 139 JSON.appendCharHex(sb, prevChar) 140 } 141 } else if (Char.isLowSurrogate(currChar)) { 142 if (i != 0 && Char.isHighSurrogate(prevChar)) { 143 sb.append(prevChar) 144 sb.append(currChar) 145 } else { 146 JSON.appendCharHex(sb, currChar) 147 } 148 } else { 149 if (i != 0 && Char.isHighSurrogate(prevChar)) { 150 JSON.appendCharHex(sb, prevChar) 151 } 152 153 let escapedCharIndex = lastIndexOf(JSON.ESCAPED_CHARS, currChar, JSON.ESCAPED_CHARS.length) 154 if (escapedCharIndex != -1) { 155 sb.append(c'\\') 156 sb.append(JSON.NON_ESCAPED_CHARS[escapedCharIndex]) 157 } else { 158 sb.append(currChar) 159 } 160 } 161 162 prevChar = currChar 163 } 164 165 if (len > 0) { 166 if (Char.isHighSurrogate(prevChar)) { 167 JSON.appendCharHex(sb, prevChar) 168 } 169 } 170 171 sb.append(c'\"') 172 return sb.toString() 173 } 174 175 private static appendCharHex(buffer: StringBuilder, chr: char): StringBuilder { 176 buffer.append("\\u").append(new Int(chr).toString(16)) 177 return buffer 178 } 179 180 /** 181 * Converts an object to a JavaScript Object Notation (JSON) string. 182 * 183 * @param obj: Object An object to be converted. 184 * 185 * @returns String - JSON representation of Object 186 */ 187 public static stringify(obj: NullishType): String { 188 return new JSONWriter().write(obj) 189 } 190 191 /** 192 * Converts an object to a JavaScript Object Notation (JSON) string. 193 * 194 * @param obj: Object An object to be converted. 195 * 196 * @param replacer A function that transforms the results. 197 * 198 * @param space A string or number that's used to insert white space 199 * (including indentation, line break characters, etc.) into the output JSON string for readability purposes. 200 * 201 * @returns String - JSON representation of Object 202 */ 203 public static stringify( 204 obj: NullishType, 205 replacer: ((key: string, value: NullishType) => NullishType) | undefined | null, 206 space?: string | number): string { 207 208 const replacerOpt: ((key: string, value: NullishType) => NullishType) | undefined = (replacer === null) ? undefined : replacer 209 210 if (space instanceof String || space === undefined) { 211 return new JSONWriter(replacerOpt, space).write(obj) 212 } else { 213 return new JSONWriter(replacerOpt, JSON.spaceFromNumber(space as number)).write(obj) 214 } 215 } 216 217 /** 218 * Converts an object to a JavaScript Object Notation (JSON) string. 219 * 220 * @param obj: Object An object to be converted. 221 * 222 * @param replacer An array with elements indicating names of the properties in the object 223 * that should be included in the resulting JSON string 224 * 225 * @param space A string or number that's used to insert white space 226 * (including indentation, line break characters, etc.) into the output JSON string for readability purposes. 227 * 228 * @returns String - JSON representation of Object 229 */ 230 public static stringify(obj: NullishType, replacer: (string | number)[], space?: string | number): string { 231 const filter = new string[replacer.length] 232 for (let i = 0; i < replacer.length; i++) { 233 filter[i] = `${replacer[i]}` 234 } 235 236 if (space instanceof String || space === undefined) { 237 return new JSONWriter(filter, space).write(obj) 238 } else { 239 return new JSONWriter(filter, JSON.spaceFromNumber(space as number)).write(obj) 240 } 241 } 242 243 private static spaceFromNumber(space: number): String { 244 let spacesCount = 0 245 if (space > 0) { 246 if (space > JSON.JSON_SPACE_INDENT_LIMIT) { 247 spacesCount = JSON.JSON_SPACE_INDENT_LIMIT 248 } else { 249 spacesCount = space as int 250 } 251 } 252 253 return " ".repeat(spacesCount) 254 } 255 256 public static native stringify(d: JSValue): String; 257 258 //-------------------------- 259 // arrays 260 //-------------------------- 261 262 /** 263 * Converts bytes array to JSON format 264 * 265 * @param d: byte[] - bytes array to be converted to a JSON as an Array of Numbers 266 * 267 * @returns String - JSON representation of bytes array 268 */ 269 public static stringify(d: byte[]): String { 270 let s = new StringBuilder("[") 271 let last_elem = d.length - 1 272 for (let i = 0; i < last_elem; ++i) { 273 s.append(d[i]) 274 s.append(',') 275 } 276 if (d.length > 0) { 277 s.append(d[last_elem]) 278 } 279 s.append(']') 280 return s.toString() 281 } 282 283 /** 284 * Converts shorts array to JSON format 285 * 286 * @param d: short[] - shorts array to be converted to a JSON as an Array of Numbers 287 * 288 * @returns String - JSON representation of shorts array 289 */ 290 public static stringify(d: short[]): String { 291 let s = new StringBuilder("[") 292 let last_elem = d.length - 1 293 for (let i = 0; i < last_elem; ++i) { 294 s.append(d[i]) 295 s.append(',') 296 } 297 if (d.length > 0) { 298 s.append(d[last_elem]) 299 } 300 s.append(']') 301 return s.toString() 302 } 303 304 /** 305 * Converts ints array to JSON format 306 * 307 * @param d: int[] - ints array to be converted to a JSON as an Array of Numbers 308 * 309 * @returns String - JSON representation of ints array 310 */ 311 public static stringify(d: int[]): String { 312 let s = new StringBuilder("[") 313 let last_elem = d.length - 1 314 for (let i = 0; i < last_elem; ++i) { 315 s.append(d[i]) 316 s.append(',') 317 } 318 if (d.length > 0) { 319 s.append(d[last_elem]) 320 } 321 s.append(']') 322 return s.toString() 323 } 324 325 /** 326 * Converts longs array to JSON format 327 * 328 * @param d: long[] - longs array to be converted to a JSON as an Array of Numbers 329 * 330 * @returns String - JSON representation of longs array 331 */ 332 public static stringify(d: long[]): String { 333 let s = new StringBuilder("[") 334 let last_elem = d.length - 1 335 for (let i = 0; i < last_elem; ++i) { 336 s.append(d[i]) 337 s.append(',') 338 } 339 if (d.length > 0) { 340 s.append(d[last_elem]) 341 } 342 s.append(']') 343 return s.toString() 344 } 345 346 /** 347 * Converts array of bytes to JSON format 348 * 349 * @param d: byte[] - array of byte to be converted to a JSON as an Array of Numbers 350 * 351 * @returns String - JSON representation of array of bytes 352 */ 353 public static stringify(d: float[]): String { 354 let s = new StringBuilder("[") 355 let last_elem = d.length - 1 356 for (let i = 0; i < last_elem; ++i) { 357 s.append(JSON.stringify(d[i])) 358 s.append(',') 359 } 360 if (d.length > 0) { 361 s.append(JSON.stringify(d[last_elem])) 362 } 363 s.append(']') 364 return s.toString() 365 } 366 367 /** 368 * Converts doubles array to JSON format 369 * 370 * @param d: double[] - doubles array to be converted to a JSON as an Array of Numbers 371 * 372 * @returns String - JSON representation of doubles array 373 */ 374 public static stringify(d: double[]): String { 375 let s = new StringBuilder("[") 376 let last_elem = d.length - 1 377 for (let i = 0; i < last_elem; ++i) { 378 s.append(JSON.stringify(d[i])) 379 s.append(',') 380 } 381 if (d.length > 0) { 382 s.append(JSON.stringify(d[last_elem])) 383 } 384 s.append(']') 385 return s.toString() 386 } 387 388 /** 389 * Converts chars array to JSON format 390 * 391 * @param d: char[] - chars array to be converted to a JSON as an Array of Numbers 392 * 393 * @returns String - JSON representation of chars array 394 */ 395 public static stringify(d: char[]): String { 396 let s = new StringBuilder("[") 397 let last_elem = d.length - 1 398 for (let i = 0; i < last_elem; ++i) { 399 s.append("\"" + d[i] + "\"") 400 s.append(',') 401 } 402 if (d.length > 0) { 403 s.append("\"" + d[last_elem] + "\"") 404 } 405 s.append(']') 406 return s.toString() 407 } 408 409 /** 410 * Converts booleans array to JSON format 411 * 412 * @param d: boolean[] - booleans array to be converted to a JSON as an Array of Boolean literals 413 * 414 * @returns String - JSON representation of booleans array 415 */ 416 public static stringify(d: boolean[]): String { 417 let s = new StringBuilder("[") 418 let last_elem = d.length - 1 419 for (let i = 0; i < last_elem; ++i) { 420 if (d[i]) { 421 s.append("true,") 422 } else { 423 s.append("false,") 424 } 425 } 426 if (d.length > 0) { 427 s.append(d[last_elem]) 428 } 429 s.append(']') 430 return s.toString() 431 } 432 433 private static IsBoxedType(typ : Type) : boolean { 434 return typ instanceof BooleanType 435 || typ instanceof ByteType 436 || typ instanceof CharType 437 || typ instanceof ShortType 438 || typ instanceof IntType 439 || typ instanceof LongType 440 || typ instanceof FloatType 441 || typ instanceof DoubleType 442 } 443 444 // TODO(kirill-mitkin): Map<Long, Long> blocked by internal issue 445 private static checkType(typ: Type, used: Array<TypeColor>): boolean { 446 let id = typ.getId() 447 let ind = used.findIndex((tc: TypeColor) => tc.typeID == id) 448 if (ind != -1) { 449 return (used.at(ind) as TypeColor).color == TypeColor.VISITED 450 } 451 used.push(new TypeColor(id, TypeColor.AT_STACK)) 452 let ok = true 453 if (typ instanceof ClassType) { 454 let ct = typ as ClassType 455 ok &= ct.hasEmptyConstructor() 456 for (let f = 0; f < ct.getFieldsNum(); f++) { 457 ok &= JSON.checkType(ct.getField(f).getType(), used) 458 } 459 } else if (typ instanceof ArrayType) { 460 let et = (typ as ArrayType).getElementType() 461 ok = JSON.checkType(et, used) 462 } else if (typ instanceof StringType 463 || typ instanceof DoubleType 464 || typ instanceof BooleanType 465 || typ instanceof NullType) { 466 ok = true 467 } else { 468 ok = false 469 } 470 (used.at(ind) as TypeColor).color = TypeColor.VISITED 471 return ok 472 } 473 474 // TODO(kirill-mitkin): For testing purpose only, shpuld be replaced by error checking 475 public static isCorrectForParsing(typ: Type): boolean { 476 return JSON.checkType(typ, new Array<TypeColor>()) 477 } 478 479 public static parse<T>(text: String, type: Type): Nullish<T> { 480 return JSON.parse<T>(text, undefined, type) 481 } 482 483 public static parse<T>(text: string, reviver: ((key: string, value: NullishType) => NullishType) | undefined, type: Type): Nullish<T> { 484 if (!JSON.checkType(type, new Array<TypeColor>())) { 485 throw new Error("Incorrect type: " + type.toString()) 486 } 487 488 let jsonValue = JSONParser.parse(text) 489 const parsingResult = new JSONValueParser(reviver).parse(jsonValue, type) as T 490 491 if (reviver !== undefined) { 492 return reviver("", parsingResult) as T 493 } else { 494 return parsingResult 495 } 496 } 497} 498 499class JSONWriter { 500 // NOTE(cheezzario) replace with Type.for<JSValue>() when it will be implemented 501 private static readonly STD_CORE_INTEROP_JSVALUE_TYPE = (Type.of([] as JSValue[]) as ArrayType).getElementType() 502 private static readonly ESCOMPAT_ARRAY_TYPE = Type.of(new Array<Object>(0)) 503 504 private readonly replacer: ((key: String, value: NullishType) => NullishType) | undefined 505 private readonly fieldsFilter: string[] 506 private readonly space: String | undefined 507 508 private readonly useReplacer: boolean 509 private readonly useFieldsFilter: boolean 510 private readonly useIndent: boolean 511 512 private indentLevel: int = 0 513 514 private readonly path = new Set<Object>() 515 private buffer = new StringBuilder() 516 517 constructor(replacer?: (key: String, value: NullishType) => NullishType, space?: String) { 518 this.replacer = replacer 519 this.fieldsFilter = [] 520 this.space = space 521 522 this.useReplacer = this.replacer != undefined 523 this.useFieldsFilter = false 524 this.useIndent = !(this.space == undefined || this.space == "") 525 } 526 527 constructor(filter: string[], space?: String) { 528 this.replacer = undefined 529 this.fieldsFilter = filter 530 this.space = space 531 532 this.useReplacer = false 533 this.useFieldsFilter = true 534 this.useIndent = !(this.space == undefined || this.space == "") 535 } 536 537 write(obj: NullishType): String { 538 if (this.useReplacer) { 539 this.writeObject(this.replacer!("", obj)) 540 } else { 541 this.writeObject(obj) 542 } 543 544 return this.buffer.toString() 545 } 546 547 private writeObject(obj: NullishType): void { 548 if (obj === null) { 549 this.buffer.append("null") 550 } else if (obj === undefined) { 551 this.buffer.append("undefined") 552 } else if (obj instanceof String) { 553 this.buffer.append(JSON.stringify(obj as String)) 554 } else if (this.writeValueTypeWrapper(obj)) { 555 // nothing to do - write completed successfully 556 } else { 557 const objType = Type.of(obj) 558 559 if (objType.getName() == JSONWriter.STD_CORE_INTEROP_JSVALUE_TYPE.getName()) { 560 this.buffer.append(JSON.stringify(obj as JSValue)) 561 } else if (objType instanceof ArrayType) { 562 this.writeArrayValue(Value.of(obj) as ArrayValue, objType as ArrayType) 563 } else if (objType.getName() == JSONWriter.ESCOMPAT_ARRAY_TYPE.getName()) { 564 this.writeEscompatArray(obj as Array<Object>) 565 } else if (objType instanceof ClassType) { 566 this.writeClassValue(obj, objType as ClassType) 567 } else if (objType instanceof LambdaType) { 568 this.buffer.append("undefined") 569 } else { 570 assert false : "unsupported object type: " + objType 571 } 572 } 573 } 574 575 private writeClassValue(obj: Object, objType: ClassType): void { 576 const currentBuffer = this.buffer 577 this.buffer = new StringBuilder("{") 578 579 if (this.useIndent) { 580 this.buffer.append("\n") 581 this.indentLevel += 1 582 } 583 584 const writableFields = this.getWritableFields(objType, Value.of(obj) as ClassValue) 585 586 let fieldDumped = false 587 if (writableFields.length > 0) { 588 this.path.add(obj) 589 fieldDumped = this.writeClassFields(writableFields) 590 this.path.delete(obj) 591 } 592 593 if (this.useIndent) { 594 this.indentLevel -= 1 595 this.buffer.append("\n") 596 } 597 598 this.writePadding() 599 this.buffer.append("}") 600 601 if (fieldDumped) { 602 currentBuffer.append(this.buffer.toString()) 603 } else { 604 currentBuffer.append("{}") 605 } 606 607 this.buffer = currentBuffer 608 } 609 610 private writeClassFields(writableFields: Array<[Field, Value]>): boolean { 611 const FIELD_TYPE = 0 612 const FIELD_VALUE = 1 613 614 let fieldDumped = false 615 for (let fieldIdx = 0; fieldIdx < writableFields.length; fieldIdx++) { 616 const fieldTypeValuePair = writableFields[fieldIdx] 617 618 const objField = fieldTypeValuePair[FIELD_TYPE] 619 const objFieldValue = fieldTypeValuePair[FIELD_VALUE] 620 621 const objToDump = this.useReplacer ? this.replacer!(objField.getName(), objFieldValue.getData()) : objFieldValue.getData() 622 if (objToDump !== undefined) { 623 if (fieldDumped) { 624 this.buffer.append(",") 625 if (this.useIndent) { 626 this.buffer.append("\n") 627 } 628 } else { 629 fieldDumped = true 630 } 631 632 this.checkReferencesCycle(objToDump) 633 this.writePadding() 634 635 this.buffer.append('"') 636 this.buffer.append(objField.getName()) 637 this.buffer.append('":') 638 639 if (this.useIndent) { 640 this.buffer.append(" ") 641 } 642 643 this.writeObject(objToDump) 644 } 645 } 646 647 return fieldDumped 648 } 649 650 private getWritableFields(classType: ClassType, classValue: ClassValue): Array<[Field, Value]> { 651 const writableFields = new Array<[Field, Value]>() 652 653 if (this.useFieldsFilter) { 654 for (let fieldIdx = 0; fieldIdx < this.fieldsFilter.length; fieldIdx++) { 655 const fieldName = this.fieldsFilter[fieldIdx] 656 if (!classType.hasField(fieldName)) { 657 continue 658 } 659 660 const fieldType = classType.getFieldByName(fieldName) 661 if (!this.isWritableFieldType(fieldType)) { 662 continue 663 } 664 665 const fieldValue = classValue.getFieldByName(fieldName) 666 667 const fieldTypeValuePair: [Field, Value] = [fieldType, fieldValue] 668 writableFields.push(fieldTypeValuePair) 669 } 670 } else { 671 const fieldsCount = classValue.getFieldsNum() 672 for (let fieldIdx = 0; fieldIdx < fieldsCount; fieldIdx++) { 673 const fieldType = classType.getField(fieldIdx) 674 if (!this.isWritableFieldType(fieldType)) { 675 continue 676 } 677 678 const fieldValue = classValue.getField(fieldIdx) 679 680 const fieldTypeValuePair: [Field, Value] = [fieldType, fieldValue] 681 writableFields.push(fieldTypeValuePair) 682 } 683 } 684 685 return writableFields 686 } 687 688 private isWritableFieldType(field: Field): boolean { 689 const fieldType = field.getType() 690 691 if (field.isStatic() || fieldType instanceof LambdaType) { 692 return false 693 } 694 695 return true 696 } 697 698 private writeArrayValue(arrayValue: ArrayValue, arrayType: ArrayType): void { 699 this.buffer.append("[") 700 701 if (this.useIndent) { 702 this.buffer.append("\n") 703 this.indentLevel += 1 704 } 705 706 const arrayLength = arrayValue.getLength() as int 707 if (arrayLength > 0) { 708 const array: Object = arrayValue.getData()! 709 710 this.path.add(array) 711 712 const lastElementIndex = arrayLength - 1 713 714 for (let idx = 0; idx < lastElementIndex; idx++) { 715 this.writeArrayElementValue(arrayValue.getElement(idx), idx) 716 this.buffer.append(",") 717 718 if (this.useIndent) { 719 this.buffer.append("\n") 720 } 721 } 722 723 this.writeArrayElementValue(arrayValue.getElement(lastElementIndex), lastElementIndex) 724 725 this.path.delete(array) 726 } 727 728 if (this.useIndent) { 729 this.indentLevel -= 1 730 this.buffer.append("\n") 731 } 732 733 this.writePadding() 734 735 this.buffer.append("]") 736 } 737 738 private writeArrayElementValue(elemValue: Value, elemIndex: int): void { 739 this.writeObjectsArrayElement(elemValue.getData(), elemIndex) 740 } 741 742 private writeValueTypeWrapper(obj: Object): boolean { 743 if (obj instanceof Boolean) { 744 this.buffer.append(JSON.stringify(obj.unboxed())) 745 return true 746 } else if (obj instanceof Byte) { 747 this.buffer.append(JSON.stringify(obj.unboxed())) 748 return true 749 } else if (obj instanceof Char) { 750 this.buffer.append(JSON.stringify(obj.unboxed())) 751 return true 752 } else if (obj instanceof Short) { 753 this.buffer.append(JSON.stringify(obj.unboxed())) 754 return true 755 } else if (obj instanceof Int) { 756 this.buffer.append(JSON.stringify(obj.unboxed())) 757 return true 758 } else if (obj instanceof Long) { 759 this.buffer.append(JSON.stringify(obj.unboxed())) 760 return true 761 } else if (obj instanceof Float) { 762 this.buffer.append(JSON.stringify(obj.unboxed())) 763 return true 764 } else if (obj instanceof Double) { 765 this.buffer.append(JSON.stringify(obj.unboxed())) 766 return true 767 } else { 768 return false 769 } 770 } 771 772 private writeEscompatArray<T>(array: Array<T>): void { 773 this.buffer.append("[") 774 775 if (this.useIndent) { 776 this.buffer.append("\n") 777 this.indentLevel += 1 778 } 779 780 if (array.length > 0) { 781 this.path.add(array) 782 783 const lastElementIndex = (array.length as int) - 1 784 785 for (let idx = 0; idx < lastElementIndex; idx++) { 786 this.writeObjectsArrayElement(array[idx], idx) 787 this.buffer.append(",") 788 789 if (this.useIndent) { 790 this.buffer.append("\n") 791 } 792 } 793 794 this.writeObjectsArrayElement(array[lastElementIndex], lastElementIndex) 795 796 this.path.delete(array) 797 } 798 799 if (this.useIndent) { 800 this.indentLevel -= 1 801 this.buffer.append("\n") 802 } 803 804 this.writePadding() 805 806 this.buffer.append("]") 807 } 808 809 private writeObjectsArrayElement(arrayElement: NullishType, elementIndex: int): void { 810 this.writePadding() 811 812 const replacedArrayElement = this.useReplacer ? this.replacer!("" + elementIndex, arrayElement) : arrayElement 813 814 const arrayElementType = Type.of(replacedArrayElement) 815 if (replacedArrayElement == null || arrayElementType instanceof LambdaType) { 816 this.buffer.append("null") 817 } else { 818 this.checkReferencesCycle(replacedArrayElement) 819 this.writeObject(replacedArrayElement) 820 } 821 } 822 823 private checkReferencesCycle(obj: NullishType): void { 824 if (obj == null) { 825 return 826 } 827 828 const cycleDetected = this.path.has(obj) 829 if (cycleDetected) { 830 throw new TypeError("cyclic object value") 831 } 832 } 833 834 private writePadding() { 835 if (this.indentLevel > 0) { 836 const indent = this.space!.repeat(this.indentLevel) 837 this.buffer.append(indent) 838 } 839 } 840} 841 842class TypeColor { 843 readonly static VISITED : int = 1 844 readonly static AT_STACK : int = 2 845 typeID : long 846 color : int 847 constructor(typeID: long, color: int) { 848 this.typeID = typeID 849 this.color = color 850 } 851} 852 853class Position { 854 row: int = 1 855 col: int = 1 856 index: int = -1 857 858 makeCopy(): Position { 859 let c = new Position() 860 c.row = this.row 861 c.col = this.col 862 c.index = this.index 863 return c 864 } 865 866 restoreFromCopy(c: Position) { 867 this.row = c.row 868 this.col = c.col 869 this.index = c.index 870 } 871} 872 873export class JSONParser { 874 private json: String 875 private curPos: Position 876 private curChar: char 877 private escapeWhitespaces: boolean 878 // TODO(ivan-tyulyandin): replace the hardcode with proper use of Array<T>/Map<T> in JSONValue inheritors 879 880 constructor(json: String) { 881 this.json = json 882 this.escapeWhitespaces = true 883 this.curPos = new Position() 884 } 885 886 private getCurPosDescr(): String { 887 return "" + this.curPos.row + ":" + this.curPos.col 888 } 889 890 private getNextChar(): boolean { 891 do { 892 ++this.curPos.index 893 if (this.curPos.index >= this.json.getLength()) { 894 return false 895 } 896 this.curChar = this.json.charAt(this.curPos.index) 897 ++this.curPos.col 898 if (this.curChar == c'\n') { 899 ++this.curPos.row 900 this.curPos.col = 1 901 } 902 } while (this.escapeWhitespaces && (this.curChar == c' ' || this.curChar == c'\t' || this.curChar == c'\n')) 903 return true 904 } 905 906 /** 907 * Parses JSON into JSONValue 908 * 909 * @param json: String - a string with JSON 910 * 911 * @return JSONValue - JSON representation 912 * 913 * @throws SyntaxError if JSON is invalid 914 * 915 */ 916 static parse(json: String): JSONValue { 917 let parser = new JSONParser(json) 918 let res: JSONValue = new JSONValue() 919 return parser.parse(res) 920 } 921 922 private parse(res: JSONValue): JSONValue { 923 // Fetch next symbol and call related parse method 924 // TODO(ivan-tyulyandin): replace with switch, does not work due to frontend bug with switch over static fields with equal names 925 this.getNextChar() 926 if (this.curChar == JSONObject.START_CHAR) { 927 let obj = new JSONObject() 928 this.parse(obj) 929 res = obj 930 } else if (this.curChar == JSONArray.START_CHAR) { 931 let arr = new JSONArray() 932 this.parse(arr) 933 res = arr 934 } else if (this.curChar == JSONString.START_CHAR) { 935 let str = new JSONString() 936 this.parse(str) 937 res = str 938 } else if (this.curChar == JSONTrue.START_CHAR) { 939 let tr = new JSONTrue() 940 this.parse(tr) 941 res = tr 942 } else if (this.curChar == JSONFalse.START_CHAR) { 943 let fls = new JSONFalse() 944 this.parse(fls) 945 res = fls 946 } else if (this.curChar == JSONNull.START_CHAR) { 947 let nl = new JSONNull() 948 this.parse(nl) 949 res = nl 950 } else if (Char.isDecDigit(this.curChar) || this.curChar == c'-') { 951 let n = new JSONNumber() 952 this.parse(n) 953 res = n 954 } else { 955 throw new SyntaxError("Unexpected char \"" + this.curChar + "\" at " + this.getCurPosDescr()) 956 } 957 return res 958 } 959 960 private parseKeyValue(res: JSONObject): JSONObject { 961 res.keys_.push(this.parse(new JSONValue()) as JSONString) 962 this.getNextChar() 963 if (this.curChar != c':') { 964 throw new SyntaxError("Expected : \",\" at " + this.getCurPosDescr() + " got \"" + this.curChar + "\"") 965 } 966 res.values.push(this.parse(new JSONValue())) 967 return res 968 } 969 970 private parse(res: JSONObject): JSONObject { 971 let posCopy = this.curPos.makeCopy() 972 this.getNextChar() 973 if (this.curChar == JSONObject.END_CHAR) { 974 return res 975 } 976 this.curPos.restoreFromCopy(posCopy) 977 978 res = this.parseKeyValue(res) 979 // Parse JSONObject until } 980 while (this.getNextChar()) { 981 switch (this.curChar) { 982 case c',': 983 res = this.parseKeyValue(res) 984 break 985 // TODO(ivan-tyulyandin): replace by JSONObject.END_CHAR, frontend bug 986 case c'}': 987 return res 988 default: 989 throw new SyntaxError("Expected \",\" or \"" + JSONObject.END_CHAR + "\" at " + this.getCurPosDescr() + ", got \"" + this.curChar + "\"") 990 } 991 } 992 } 993 994 private parse(res: JSONArray): JSONArray { 995 let posCopy = this.curPos.makeCopy() 996 this.getNextChar() 997 if (this.curChar == JSONArray.END_CHAR) { 998 return res 999 } 1000 this.curPos.restoreFromCopy(posCopy) 1001 1002 res.values.push(this.parse(new JSONValue())) 1003 // Parse JSONArray until ] 1004 while (this.getNextChar()) { 1005 switch (this.curChar) { 1006 case c',': 1007 res.values.push(this.parse(new JSONValue())) 1008 break 1009 // TODO(ivan-tyulyandin): replace by JSONArray.END_CHAR, frontend bug 1010 case c']': 1011 return res 1012 default: 1013 throw new SyntaxError("Expected , or " + JSONArray.END_CHAR + " at " + this.getCurPosDescr() + ", got " + this.curChar) 1014 } 1015 } 1016 } 1017 1018 private parse(res: JSONNumber): JSONNumber { 1019 let number = new StringBuilder() 1020 let posCopy = this.curPos.makeCopy() 1021 do { 1022 number.append(this.curChar) 1023 posCopy = this.curPos.makeCopy() 1024 } while (this.getNextChar() && (Char.isDecDigit(this.curChar) || 1025 this.curChar == c'.' || 1026 this.curChar == c'e' || 1027 this.curChar == c'-' || 1028 this.curChar == c'+' )) 1029 res.value = Double.parseFloat(number.toString()) 1030 this.curPos.restoreFromCopy(posCopy) 1031 return res 1032 } 1033 1034 private static readonly CAN_BE_ESCAPED_CHARS: char[] = [c'"', c'\\', c'/', c'b', c'f', c'n', c'r', c't'] 1035 private static readonly ESCAPED_CHARS: char[] = [c'\"', c'\\', c'\/', c'\b', c'\f', c'\n', c'\r', c'\t'] 1036 1037 private parse(res: JSONString): JSONString { 1038 let resBuilder: StringBuilder = new StringBuilder() 1039 let hasMetEscape: boolean = false 1040 this.escapeWhitespaces = false 1041 while (this.getNextChar() && ((this.curChar != JSONString.END_CHAR) || (this.curChar == JSONString.END_CHAR && hasMetEscape))) { 1042 if (!hasMetEscape) { 1043 if (this.curChar != c'\\') { 1044 resBuilder.append(this.curChar) 1045 } 1046 } else { 1047 let escapedCharIndex = indexOf(JSONParser.CAN_BE_ESCAPED_CHARS, this.curChar, 0) 1048 if (escapedCharIndex != -1) { 1049 resBuilder.append(JSONParser.ESCAPED_CHARS[escapedCharIndex]) 1050 } else { 1051 throw new SyntaxError("Bad escape sequence \\" + this.curChar + " at " + this.getCurPosDescr()) 1052 return res 1053 } 1054 } 1055 hasMetEscape = this.curChar == c'\\' 1056 } 1057 if (this.curChar == JSONString.END_CHAR) { 1058 res.value = resBuilder.toString() 1059 } else { 1060 throw new SyntaxError("Unexpected end of String at " + this.getCurPosDescr()) 1061 } 1062 this.escapeWhitespaces = true 1063 return res 1064 } 1065 1066 private parse(res: JSONTrue): JSONTrue { 1067 let metTrue = this.getNextChar() && this.curChar == c'r' 1068 && this.getNextChar() && this.curChar == c'u' 1069 && this.getNextChar() && this.curChar == c'e' 1070 if (!metTrue) { 1071 throw new SyntaxError("Expected true at " + this.getCurPosDescr()) 1072 } 1073 return res 1074 } 1075 1076 private parse(res: JSONFalse): JSONFalse { 1077 let metFalse = this.getNextChar() && this.curChar == c'a' 1078 && this.getNextChar() && this.curChar == c'l' 1079 && this.getNextChar() && this.curChar == c's' 1080 && this.getNextChar() && this.curChar == c'e' 1081 if (!metFalse) { 1082 throw new SyntaxError("Expected false at " + this.getCurPosDescr()) 1083 } 1084 return res 1085 } 1086 1087 private parse(res: JSONNull): JSONNull { 1088 let metNull = this.getNextChar() && this.curChar == c'u' 1089 && this.getNextChar() && this.curChar == c'l' 1090 && this.getNextChar() && this.curChar == c'l' 1091 if (!metNull) { 1092 throw new SyntaxError("Expected null at " + this.getCurPosDescr()) 1093 } 1094 return res 1095 } 1096} 1097 1098class JSONValueParser { 1099 private reviver: ((key: string, value: NullishType) => NullishType) | undefined 1100 1101 constructor(reviver: ((key: string, value: NullishType) => NullishType) | undefined) { 1102 this.reviver = reviver 1103 } 1104 1105 parse(value: JSONValue, typ: Type): NullishType { 1106 if (value instanceof JSONObject && typ instanceof ClassType) { 1107 return this.parseObject(value as JSONObject, typ as ClassType) 1108 } else if (value instanceof JSONArray && typ instanceof ArrayType) { 1109 return this.parseArray(value as JSONArray, typ as ArrayType) 1110 } else if (value instanceof JSONNumber && typ instanceof DoubleType) { 1111 return new Double((value as JSONNumber).value) 1112 } else if (value instanceof JSONString && typ instanceof StringType) { 1113 return (value as JSONString).value 1114 } else if (value instanceof JSONTrue && typ instanceof BooleanType) { 1115 return new Boolean(true) 1116 } else if (value instanceof JSONFalse && typ instanceof BooleanType) { 1117 return new Boolean(false) 1118 } else if (value instanceof JSONNull && typ instanceof NullType) { 1119 return null 1120 } else { 1121 throw new Error(typ + " is expected, but get " + value) 1122 } 1123 } 1124 1125 private revivingClassFieldSetter(classVal: ClassValue, fieldName: string, fieldVal: NullishType): void { 1126 const revivedVal = this.reviver!(fieldName, fieldVal) 1127 1128 // setFieldByName doesn't allow null/undefined values 1129 if (revivedVal != null) { 1130 classVal.setFieldByName(fieldName, Value.of(revivedVal)) 1131 } 1132 } 1133 1134 private classFieldSetter(classVal: ClassValue, fieldName: string, fieldVal: NullishType): void { 1135 classVal.setFieldByName(fieldName, Value.of(fieldVal)) 1136 } 1137 1138 private parseObject(jsonObj: JSONObject, classType: ClassType): Object { 1139 let setClassFieldFn = (cv: ClassValue, fn: string, fv: NullishType) => { cv.setFieldByName(fn, Value.of(fv)) } 1140 1141 if (this.reviver != undefined) { 1142 setClassFieldFn = this.revivingClassFieldSetter 1143 } 1144 1145 let object = classType.make() 1146 let classVal = Value.of(object) as ClassValue 1147 const jsonObjFields = jsonObj.getFields() 1148 for (let field_num = 0; field_num < classType.getFieldsNum(); field_num++) { 1149 let classField = classType.getField(field_num) 1150 if (classField.isStatic()) { 1151 continue 1152 } 1153 1154 const jsonFieldVal = jsonObjFields.get(classField.getName()) 1155 if (jsonFieldVal === undefined) { 1156 throw new Error("Cannot find " + classField.getName() + " in keys of " + classType.getName()) 1157 } 1158 1159 const classFieldVal = this.parse(jsonFieldVal, classField.getType()) 1160 setClassFieldFn(classVal, classField.getName(), classFieldVal) 1161 } 1162 1163 return object 1164 } 1165 1166 private revivingArrayElementSetter(array: NullishType[], index: int, elemVal: NullishType): void { 1167 const revivedVal = this.reviver!(`${index}`, elemVal) 1168 array[index] = revivedVal !== undefined ? revivedVal : null 1169 } 1170 1171 private parseArray(jVal: JSONArray, aType: ArrayType): Object | null { 1172 let len = jVal.values.length 1173 let arr = aType.make(len as int) as NullishType[] 1174 1175 let setArrayElementFn = (a: NullishType[], i: int, v: NullishType) => { a[i] = v } 1176 1177 if (this.reviver !== undefined) { 1178 setArrayElementFn = this.revivingArrayElementSetter 1179 } 1180 1181 for (let i = 0; i < len; i++) { 1182 let elem = this.parse(jVal.values.at(i) as JSONValue, aType.getElementType()) 1183 setArrayElementFn(arr, i, elem) 1184 } 1185 return arr 1186 } 1187} 1188 1189export class JSONValue extends Object {} 1190 1191export class JSONObject extends JSONValue { 1192 keys_: Array<JSONString> = new Array<JSONString>() 1193 values: Array<JSONValue> = new Array<JSONValue>() 1194 readonly static START_CHAR = c'{' 1195 readonly static END_CHAR = c'}' 1196 readonly static SEPARATOR = c':' 1197 readonly static DELIMETER = c',' 1198 1199 getFields(): Map<String, JSONValue> { 1200 const propsCount = this.keys_.length 1201 1202 const result = new Map<String, JSONValue>() 1203 1204 for (let i = 0; i < propsCount; i++) { 1205 const key = this.keys_[i] 1206 const val = this.values[i] 1207 1208 result.set(key.value, val) 1209 } 1210 1211 return result 1212 } 1213 1214 public override toString(): String { 1215 let res = new StringBuilder([JSONObject.START_CHAR]) 1216 for (let i = 0; i < this.keys_.length - 1; ++i) { 1217 res.append("" + this.keys_.at(i) + JSONObject.SEPARATOR + this.values.at(i) + JSONObject.DELIMETER) 1218 } 1219 if (this.keys_.length > 0) { 1220 res.append("" + this.keys_.at(this.keys_.length - 1) + JSONObject.SEPARATOR + this.values.at(this.keys_.length - 1)) 1221 } 1222 res.append(JSONObject.END_CHAR) 1223 return res.toString() 1224 } 1225} 1226 1227export class JSONArray extends JSONValue { 1228 values: Array<JSONValue> = new Array<JSONValue>() 1229 readonly static START_CHAR = c'[' 1230 readonly static END_CHAR = c']' 1231 readonly static SEPARATOR = c',' 1232 1233 public override toString(): String { 1234 let res = new StringBuilder([JSONArray.START_CHAR]) 1235 for (let i = 0; i < this.values.length - 1; ++i) { 1236 res.append("" + this.values.at(i) + JSONArray.SEPARATOR) 1237 } 1238 if (this.values.length > 0) { 1239 res.append("" + this.values.at(this.values.length - 1)) 1240 } 1241 res.append(JSONArray.END_CHAR) 1242 return res.toString() 1243 } 1244} 1245 1246export class JSONNumber extends JSONValue { 1247 value: double 1248 1249 public override toString(): String { 1250 return (new Double(this.value)).toString() 1251 } 1252} 1253 1254export class JSONString extends JSONValue { 1255 value: String 1256 readonly static START_CHAR = c'"' 1257 readonly static END_CHAR = c'"' 1258 1259 public override toString(): String { 1260 return this.value 1261 } 1262} 1263 1264export class JSONTrue extends JSONValue { 1265 readonly static value = "true" 1266 readonly static START_CHAR = c't' 1267 1268 public override toString(): String { 1269 return JSONTrue.value 1270 } 1271} 1272 1273export class JSONFalse extends JSONValue { 1274 readonly static value = "false" 1275 readonly static START_CHAR = c'f' 1276 1277 public override toString(): String { 1278 return JSONFalse.value 1279 } 1280} 1281 1282export class JSONNull extends JSONValue { 1283 readonly static value = "null" 1284 readonly static START_CHAR = c'n' 1285 1286 public override toString(): String { 1287 return JSONNull.value 1288 } 1289} 1290 1291export interface JSONable<T> { 1292 static createFromJSONValue(json: JSONValue): T { 1293 throw new JSONTypeError("createFromJSONValue was not overrided") 1294 } 1295} 1296