001package org.hl7.fhir.utilities.json; 002 003import java.io.File; 004 005/* 006 Copyright (c) 2011+, HL7, Inc. 007 All rights reserved. 008 009 Redistribution and use in source and binary forms, with or without modification, 010 are permitted provided that the following conditions are met: 011 012 * Redistributions of source code must retain the above copyright notice, this 013 list of conditions and the following disclaimer. 014 * Redistributions in binary form must reproduce the above copyright notice, 015 this list of conditions and the following disclaimer in the documentation 016 and/or other materials provided with the distribution. 017 * Neither the name of HL7 nor the names of its contributors may be used to 018 endorse or promote products derived from this software without specific 019 prior written permission. 020 021 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 022 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 023 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 024 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 025 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 026 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 027 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 028 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 029 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 030 POSSIBILITY OF SUCH DAMAGE. 031 032 */ 033 034 035 036import java.io.IOException; 037import java.io.InputStream; 038import java.math.BigDecimal; 039import java.nio.charset.StandardCharsets; 040import java.util.Map; 041import java.util.Stack; 042 043import org.hl7.fhir.utilities.SimpleHTTPClient; 044import org.hl7.fhir.utilities.SimpleHTTPClient.HTTPResult; 045import org.hl7.fhir.utilities.TextFile; 046import org.hl7.fhir.utilities.Utilities; 047 048import com.google.gson.Gson; 049import com.google.gson.GsonBuilder; 050import com.google.gson.JsonArray; 051import com.google.gson.JsonElement; 052import com.google.gson.JsonNull; 053import com.google.gson.JsonObject; 054import com.google.gson.JsonPrimitive; 055 056 057/** 058 * This is created to get a json parser that can track line numbers... grr... 059 * 060 * @author Grahame Grieve 061 * 062 */ 063public class JsonTrackingParser { 064 065 public class PresentedBigDecimal extends BigDecimal { 066 067 public String presentation; 068 069 public PresentedBigDecimal(String value) { 070 super(value); 071 presentation = value; 072 } 073 074 public String getPresentation() { 075 return presentation; 076 } 077 078 } 079 080 public enum TokenType { 081 Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean; 082 } 083 084 public class LocationData { 085 private int line; 086 private int col; 087 088 protected LocationData(int line, int col) { 089 super(); 090 this.line = line; 091 this.col = col; 092 } 093 094 public int getLine() { 095 return line; 096 } 097 098 public int getCol() { 099 return col; 100 } 101 102 public void newLine() { 103 line++; 104 col = 1; 105 } 106 107 public LocationData copy() { 108 return new LocationData(line, col); 109 } 110 } 111 112 private class State { 113 private String name; 114 private boolean isProp; 115 protected State(String name, boolean isProp) { 116 super(); 117 this.name = name; 118 this.isProp = isProp; 119 } 120 public String getName() { 121 return name; 122 } 123 public boolean isProp() { 124 return isProp; 125 } 126 } 127 128 private class Lexer { 129 private String source; 130 private int cursor; 131 private String peek; 132 private String value; 133 private TokenType type; 134 private Stack<State> states = new Stack<State>(); 135 private LocationData lastLocationBWS; 136 private LocationData lastLocationAWS; 137 private LocationData location; 138 private StringBuilder b = new StringBuilder(); 139 140 public Lexer(String source) throws IOException { 141 this.source = source; 142 cursor = -1; 143 location = new LocationData(1, 1); 144 start(); 145 } 146 147 private boolean more() { 148 return peek != null || cursor < source.length(); 149 } 150 151 private String getNext(int length) throws IOException { 152 String result = ""; 153 if (peek != null) { 154 if (peek.length() > length) { 155 result = peek.substring(0, length); 156 peek = peek.substring(length); 157 } else { 158 result = peek; 159 peek = null; 160 } 161 } 162 if (result.length() < length) { 163 int len = length - result.length(); 164 if (cursor > source.length() - len) 165 throw error("Attempt to read past end of source"); 166 result = result + source.substring(cursor+1, cursor+len+1); 167 cursor = cursor + len; 168 } 169 for (char ch : result.toCharArray()) 170 if (ch == '\n') 171 location.newLine(); 172 else 173 location.col++; 174 return result; 175 } 176 177 private char getNextChar() throws IOException { 178 if (peek != null) { 179 char ch = peek.charAt(0); 180 peek = peek.length() == 1 ? null : peek.substring(1); 181 return ch; 182 } else { 183 cursor++; 184 if (cursor >= source.length()) 185 return (char) 0; 186 char ch = source.charAt(cursor); 187 if (ch == '\n') { 188 location.newLine(); 189 } else { 190 location.col++; 191 } 192 return ch; 193 } 194 } 195 196 private void push(char ch){ 197 peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek; 198 } 199 200 private void parseWord(String word, char ch, TokenType type) throws IOException { 201 this.type = type; 202 value = ""+ch+getNext(word.length()-1); 203 if (!value.equals(word)) 204 throw error("Syntax error in json reading special word "+word); 205 } 206 207 private IOException error(String msg) { 208 return new IOException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.line)+" (path=["+path()+"])"); 209 } 210 211 private String path() { 212 if (states.empty()) 213 return value; 214 else { 215 String result = ""; 216 for (State s : states) 217 result = result + '/'+ s.getName(); 218 result = result + value; 219 return result; 220 } 221 } 222 223 public void start() throws IOException { 224// char ch = getNextChar(); 225// if (ch = '\.uEF') 226// begin 227// // skip BOM 228// getNextChar(); 229// getNextChar(); 230// end 231// else 232// push(ch); 233 next(); 234 } 235 236 public TokenType getType() { 237 return type; 238 } 239 240 public String getValue() { 241 return value; 242 } 243 244 245 public LocationData getLastLocationBWS() { 246 return lastLocationBWS; 247 } 248 249 public LocationData getLastLocationAWS() { 250 return lastLocationAWS; 251 } 252 253 public void next() throws IOException { 254 lastLocationBWS = location.copy(); 255 char ch; 256 do { 257 ch = getNextChar(); 258 if (allowComments && ch == '/') { 259 char ch1 = getNextChar(); 260 if (ch1 == '/') { 261 while (more() && !Utilities.charInSet(ch, '\r', '\n')) { 262 ch = getNextChar(); 263 } 264 } else { 265 push(ch1); 266 } 267 } 268 } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t')); 269 lastLocationAWS = location.copy(); 270 271 if (!more()) { 272 type = TokenType.Eof; 273 } else { 274 switch (ch) { 275 case '{' : 276 type = TokenType.Open; 277 break; 278 case '}' : 279 type = TokenType.Close; 280 break; 281 case '"' : 282 type = TokenType.String; 283 b.setLength(0); 284 do { 285 ch = getNextChar(); 286 if (ch == '\\') { 287 ch = getNextChar(); 288 switch (ch) { 289 case '"': b.append('"'); break; 290 case '\'': b.append('\''); break; 291 case '\\': b.append('\\'); break; 292 case '/': b.append('/'); break; 293 case 'n': b.append('\n'); break; 294 case 'r': b.append('\r'); break; 295 case 't': b.append('\t'); break; 296 case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break; 297 default : 298 throw error("unknown escape sequence: \\"+ch); 299 } 300 ch = ' '; 301 } else if (ch != '"') 302 b.append(ch); 303 } while (more() && (ch != '"')); 304 if (!more()) 305 throw error("premature termination of json stream during a string"); 306 value = b.toString(); 307 break; 308 case ':' : 309 type = TokenType.Colon; 310 break; 311 case ',' : 312 type = TokenType.Comma; 313 break; 314 case '[' : 315 type = TokenType.OpenArray; 316 break; 317 case ']' : 318 type = TokenType.CloseArray; 319 break; 320 case 't' : 321 parseWord("true", ch, TokenType.Boolean); 322 break; 323 case 'f' : 324 parseWord("false", ch, TokenType.Boolean); 325 break; 326 case 'n' : 327 parseWord("null", ch, TokenType.Null); 328 break; 329 default: 330 if ((ch >= '0' && ch <= '9') || ch == '-') { 331 type = TokenType.Number; 332 b.setLength(0); 333 while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.') || ch == '+' || ch == 'e' || ch == 'E') { 334 b.append(ch); 335 ch = getNextChar(); 336 } 337 value = b.toString(); 338 push(ch); 339 } else 340 throw error("Unexpected char '"+ch+"' in json stream"); 341 } 342 } 343 } 344 345 public String consume(TokenType type) throws IOException { 346 if (this.type != type) 347 throw error("JSON syntax error - found "+this.type.toString()+" expecting "+type.toString()); 348 String result = value; 349 next(); 350 return result; 351 } 352 353 } 354 355 enum ItemType { 356 Object, String, Number, Boolean, Array, End, Eof, Null; 357 } 358 private Map<JsonElement, LocationData> map; 359 private Lexer lexer; 360 private ItemType itemType = ItemType.Object; 361 private String itemName; 362 private String itemValue; 363 private boolean errorOnDuplicates = true; 364 private boolean allowComments = false; 365 366 public static JsonObject parseJson(String source) throws IOException { 367 return parse(source, null); 368 } 369 370 public static JsonObject parseJson(InputStream stream) throws IOException { 371 return parse(TextFile.streamToString(stream), null); 372 } 373 374 public static JsonObject parseJson(byte[] stream) throws IOException { 375 return parse(TextFile.bytesToString(stream), null); 376 } 377 378 public static JsonObject parseJson(byte[] stream, boolean allowDuplicates) throws IOException { 379 return parse(TextFile.bytesToString(stream), null, allowDuplicates); 380 } 381 382 public static JsonObject parseJson(File source) throws IOException { 383 return parse(TextFile.fileToString(source), null); 384 } 385 386 public static JsonObject parseJsonFile(String source) throws IOException { 387 return parse(TextFile.fileToString(source), null); 388 } 389 390 public static JsonObject parse(String source, Map<JsonElement, LocationData> map) throws IOException { 391 return parse(source, map, false); 392 } 393 394 public static JsonObject parse(String source, Map<JsonElement, LocationData> map, boolean allowDuplicates) throws IOException { 395 return parse(source, map, allowDuplicates, false); 396 } 397 398 public static JsonObject parse(String source, Map<JsonElement, LocationData> map, boolean allowDuplicates, boolean allowComments) throws IOException { 399 JsonTrackingParser self = new JsonTrackingParser(); 400 self.map = map; 401 self.setErrorOnDuplicates(!allowDuplicates); 402 self.setAllowComments(allowComments); 403 return self.parse(Utilities.stripBOM(source)); 404 } 405 406 private JsonObject parse(String source) throws IOException { 407 lexer = new Lexer(source); 408 JsonObject result = new JsonObject(); 409 LocationData loc = lexer.location.copy(); 410 if (lexer.getType() == TokenType.Open) { 411 lexer.next(); 412 lexer.states.push(new State("", false)); 413 } 414 else 415 throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString()); 416 417 parseProperty(); 418 readObject(result, true); 419 if (map != null) 420 map.put(result, loc); 421 return result; 422 } 423 424 private void readObject(JsonObject obj, boolean root) throws IOException { 425 if (map != null) 426 map.put(obj, lexer.location.copy()); 427 428 while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) { 429 switch (itemType) { 430 case Object: 431 JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName); 432 LocationData loc = lexer.location.copy(); 433 if (!obj.has(itemName)) 434 obj.add(itemName, child); 435 else if (errorOnDuplicates) 436 throw lexer.error("Duplicated property name: "+itemName); 437 next(); 438 readObject(child, false); 439 if (map != null) 440 map.put(obj, loc); 441 break; 442 case Boolean : 443 JsonPrimitive v = new JsonPrimitive(Boolean.valueOf(itemValue)); 444 if (!obj.has(itemName)) 445 obj.add(itemName, v); 446 else if (errorOnDuplicates) 447 throw lexer.error("Duplicated property name: "+itemName); 448 if (map != null) 449 map.put(v, lexer.location.copy()); 450 break; 451 case String: 452 v = new JsonPrimitive(itemValue); 453 if (!obj.has(itemName)) 454 obj.add(itemName, v); 455 else if (errorOnDuplicates) 456 throw lexer.error("Duplicated property name: "+itemName); 457 if (map != null) 458 map.put(v, lexer.location.copy()); 459 break; 460 case Number: 461 v = new JsonPrimitive(new PresentedBigDecimal(itemValue)); 462 if (!obj.has(itemName)) 463 obj.add(itemName, v); 464 else if (errorOnDuplicates) 465 throw lexer.error("Duplicated property name: "+itemName); 466 if (map != null) 467 map.put(v, lexer.location.copy()); 468 break; 469 case Null: 470 JsonNull n = new JsonNull(); 471 if (!obj.has(itemName)) 472 obj.add(itemName, n); 473 else if (errorOnDuplicates) 474 throw lexer.error("Duplicated property name: "+itemName); 475 if (map != null) 476 map.put(n, lexer.location.copy()); 477 break; 478 case Array: 479 JsonArray arr = new JsonArray(); // (obj.path+'.'+ItemName); 480 loc = lexer.location.copy(); 481 if (!obj.has(itemName)) 482 obj.add(itemName, arr); 483 else if (errorOnDuplicates) 484 throw lexer.error("Duplicated property name: "+itemName); 485 next(); 486 if (!readArray(arr, false)) 487 next(true); 488 if (map != null) 489 map.put(arr, loc); 490 break; 491 case Eof : 492 throw lexer.error("Unexpected End of File"); 493 case End: 494 // TODO GG: This isn't handled. Should it be? 495 break; 496 } 497 next(); 498 } 499 } 500 501 private boolean readArray(JsonArray arr, boolean root) throws IOException { 502 boolean res = false; 503 while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) { 504 res = true; 505 switch (itemType) { 506 case Object: 507 JsonObject obj = new JsonObject(); // (arr.path+'['+inttostr(i)+']'); 508 LocationData loc = lexer.location.copy(); 509 arr.add(obj); 510 next(); 511 readObject(obj, false); 512 if (map != null) 513 map.put(obj, loc); 514 break; 515 case String: 516 JsonPrimitive v = new JsonPrimitive(itemValue); 517 arr.add(v); 518 if (map != null) 519 map.put(v, lexer.location.copy()); 520 break; 521 case Number: 522 v = new JsonPrimitive(new BigDecimal(itemValue)); 523 arr.add(v); 524 if (map != null) 525 map.put(v, lexer.location.copy()); 526 break; 527 case Null : 528 JsonNull n = new JsonNull(); 529 arr.add(n); 530 if (map != null) 531 map.put(n, lexer.location.copy()); 532 break; 533 case Array: 534 JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']'); 535 loc = lexer.location.copy(); 536 arr.add(child); 537 next(); 538 readArray(child, false); 539 if (map != null) 540 map.put(arr, loc); 541 break; 542 case Eof : 543 throw lexer.error("Unexpected End of File"); 544 case End: 545 case Boolean: 546 // TODO GG: These aren't handled. SHould they be? 547 break; 548 } 549 next(); 550 } 551 return res; 552 } 553 554 private void next() throws IOException { 555 next(false); 556 } 557 558 private void next(boolean noPop) throws IOException { 559 switch (itemType) { 560 case Object : 561 lexer.consume(TokenType.Open); 562 lexer.states.push(new State(itemName, false)); 563 if (lexer.getType() == TokenType.Close) { 564 itemType = ItemType.End; 565 lexer.next(); 566 } else 567 parseProperty(); 568 break; 569 case Null: 570 case String: 571 case Number: 572 case End: 573 case Boolean : 574 if (itemType == ItemType.End && !noPop) 575 lexer.states.pop(); 576 if (lexer.getType() == TokenType.Comma) { 577 lexer.next(); 578 parseProperty(); 579 } else if (lexer.getType() == TokenType.Close) { 580 itemType = ItemType.End; 581 lexer.next(); 582 } else if (lexer.getType() == TokenType.CloseArray) { 583 itemType = ItemType.End; 584 lexer.next(); 585 } else if (lexer.getType() == TokenType.Eof) { 586 itemType = ItemType.Eof; 587 } else 588 throw lexer.error("Unexpected JSON syntax"); 589 break; 590 case Array : 591 lexer.next(); 592 lexer.states.push(new State(itemName+"[]", true)); 593 parseProperty(); 594 break; 595 case Eof : 596 throw lexer.error("JSON Syntax Error - attempt to read past end of json stream"); 597 default: 598 throw lexer.error("not done yet (a): "+itemType.toString()); 599 } 600 } 601 602 private void parseProperty() throws IOException { 603 if (!lexer.states.peek().isProp) { 604 itemName = lexer.consume(TokenType.String); 605 itemValue = null; 606 lexer.consume(TokenType.Colon); 607 } 608 switch (lexer.getType()) { 609 case Null : 610 itemType = ItemType.Null; 611 itemValue = lexer.value; 612 lexer.next(); 613 break; 614 case String : 615 itemType = ItemType.String; 616 itemValue = lexer.value; 617 lexer.next(); 618 break; 619 case Boolean : 620 itemType = ItemType.Boolean; 621 itemValue = lexer.value; 622 lexer.next(); 623 break; 624 case Number : 625 itemType = ItemType.Number; 626 itemValue = lexer.value; 627 lexer.next(); 628 break; 629 case Open : 630 itemType = ItemType.Object; 631 break; 632 case OpenArray : 633 itemType = ItemType.Array; 634 break; 635 case CloseArray : 636 itemType = ItemType.End; 637 break; 638 // case Close, , case Colon, case Comma, case OpenArray, ! 639 default: 640 throw lexer.error("not done yet (b): "+lexer.getType().toString()); 641 } 642 } 643 644 public boolean isErrorOnDuplicates() { 645 return errorOnDuplicates; 646 } 647 648 public void setErrorOnDuplicates(boolean errorOnDuplicates) { 649 this.errorOnDuplicates = errorOnDuplicates; 650 } 651 652 653 public boolean isAllowComments() { 654 return allowComments; 655 } 656 657 public void setAllowComments(boolean allowComments) { 658 this.allowComments = allowComments; 659 } 660 661 public static void write(JsonObject json, File file) throws IOException { 662 Gson gson = new GsonBuilder().setPrettyPrinting().create(); 663 String jcnt = gson.toJson(json); 664 TextFile.stringToFile(jcnt, file); 665 } 666 667 public static void write(JsonObject json, File file, boolean pretty) throws IOException { 668 Gson gson = pretty ? new GsonBuilder().setPrettyPrinting().create() : new GsonBuilder().create(); 669 String jcnt = gson.toJson(json); 670 TextFile.stringToFile(jcnt, file); 671 } 672 673 public static void write(JsonObject json, String fileName) throws IOException { 674 Gson gson = new GsonBuilder().setPrettyPrinting().create(); 675 String jcnt = gson.toJson(json); 676 TextFile.stringToFile(jcnt, fileName); 677 } 678 679 public static String write(JsonObject json) { 680 Gson gson = new GsonBuilder().setPrettyPrinting().create(); 681 return gson.toJson(json); 682 } 683 684 public static String writeDense(JsonObject json) { 685 Gson gson = new GsonBuilder().create(); 686 return gson.toJson(json); 687 } 688 689 public static byte[] writeBytes(JsonObject json, boolean pretty) { 690 if (pretty) { 691 Gson gson = new GsonBuilder().setPrettyPrinting().create(); 692 return gson.toJson(json).getBytes(StandardCharsets.UTF_8); 693 } else { 694 Gson gson = new GsonBuilder().create(); 695 return gson.toJson(json).getBytes(StandardCharsets.UTF_8); 696 } 697 } 698 699 public static JsonObject fetchJson(String source) throws IOException { 700 SimpleHTTPClient fetcher = new SimpleHTTPClient(); 701 HTTPResult res = fetcher.get(source+"?nocache=" + System.currentTimeMillis()); 702 res.checkThrowException(); 703 return parseJson(res.getContent()); 704 } 705 706 707}