001package org.hl7.fhir.utilities.json.parser; 002 003import java.io.File; 004import java.io.IOException; 005import java.io.InputStream; 006import java.io.OutputStream; 007import java.nio.charset.StandardCharsets; 008import java.util.List; 009 010import org.hl7.fhir.utilities.SimpleHTTPClient; 011import org.hl7.fhir.utilities.SimpleHTTPClient.HTTPResult; 012import org.hl7.fhir.utilities.TextFile; 013import org.hl7.fhir.utilities.Utilities; 014import org.hl7.fhir.utilities.json.JsonException; 015import org.hl7.fhir.utilities.json.model.JsonArray; 016import org.hl7.fhir.utilities.json.model.JsonBoolean; 017import org.hl7.fhir.utilities.json.model.JsonComment; 018import org.hl7.fhir.utilities.json.model.JsonElement; 019import org.hl7.fhir.utilities.json.model.JsonElementType; 020import org.hl7.fhir.utilities.json.model.JsonLocationData; 021import org.hl7.fhir.utilities.json.model.JsonNull; 022import org.hl7.fhir.utilities.json.model.JsonNumber; 023import org.hl7.fhir.utilities.json.model.JsonObject; 024import org.hl7.fhir.utilities.json.model.JsonPrimitive; 025import org.hl7.fhir.utilities.json.model.JsonProperty; 026import org.hl7.fhir.utilities.json.model.JsonString; 027import org.hl7.fhir.utilities.json.parser.JsonLexer.State; 028import org.hl7.fhir.utilities.json.parser.JsonLexer.TokenType; 029 030/** 031 * Simple parser for JSON. This parser is not particularly quick (though it's not slow) 032 * The focus for this parser is to faithfully record the line/col number of json elements 033 * so that the FHIR validator can report issues by line number 034 * 035 * Also, for the validator, the parser will accept duplicate property names 036 * 037 * JSON5: When running in Json5 mode, the parser accepts 038 * * unquoted strings for both fields and values 039 * * missing commas in objects and arrays 040 * * comments - anything starting // will be processed as a comma to the end of the line 041 * 042 * Other JSON5 features might be added in the future 043 * 044 * The FHIR Validator uses this parser in Json5 mode, and the object model is marked up 045 * with deviations from base JSON spec so that the validator can record them as errors 046 * (this is better than blowing up parsing the JSON) 047 * 048 * @author grahamegrieve 049 * 050 */ 051public class JsonParser { 052 053 public static JsonObject parseObject(InputStream stream) throws IOException, JsonException { 054 return parseObject(TextFile.streamToString(stream)); 055 } 056 057 public static JsonObject parseObject(byte[] stream) throws IOException, JsonException { 058 return parseObject(TextFile.bytesToString(stream)); 059 } 060 061 public static JsonObject parseObject(String source) throws IOException, JsonException { 062 return parseObject(source, false); 063 } 064 065 public static JsonObject parseObject(File source) throws IOException, JsonException { 066 return parseObject(TextFile.fileToString(source)); 067 } 068 069 public static JsonObject parseObjectFromFile(String source) throws IOException, JsonException { 070 return parseObject(TextFile.fileToString(source)); 071 } 072 073 public static JsonObject parseObjectFromUrl(String source) throws IOException, JsonException { 074 return parseObject(fetch(source)); 075 } 076 077 public static JsonObject parseObject(InputStream stream, boolean isJson5) throws IOException, JsonException { 078 return parseObject(TextFile.streamToString(stream), isJson5); 079 } 080 081 public static JsonObject parseObject(byte[] stream, boolean isJson5) throws IOException, JsonException { 082 return parseObject(TextFile.bytesToString(stream), isJson5); 083 } 084 085 public static JsonObject parseObject(String source, boolean isJson5) throws IOException, JsonException { 086 return parseObject(source, isJson5, false); 087 } 088 089 public static JsonObject parseObjectFromUrl(String source, boolean isJson5) throws IOException, JsonException { 090 return parseObject(fetch(source), isJson5); 091 } 092 093 public static JsonObject parseObject(InputStream stream, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException { 094 return parseObject(TextFile.streamToString(stream), isJson5, allowDuplicates); 095 } 096 097 public static JsonObject parseObject(byte[] stream, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException { 098 return parseObject(TextFile.bytesToString(stream), isJson5, allowDuplicates); 099 } 100 101 public static JsonObject parseObject(String source, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException { 102 return new JsonParser().parseJsonObject(source, isJson5, allowDuplicates); 103 } 104 105 // ================================================================ 106 107 public static JsonElement parse(InputStream stream) throws IOException, JsonException { 108 return parse(TextFile.streamToString(stream)); 109 } 110 111 public static JsonElement parse(byte[] stream) throws IOException, JsonException { 112 return parse(TextFile.bytesToString(stream)); 113 } 114 115 public static JsonElement parse(String source) throws IOException, JsonException { 116 return parse(source, false); 117 } 118 119 public static JsonElement parse(File source) throws IOException, JsonException { 120 return parse(TextFile.fileToString(source)); 121 } 122 123 public static JsonElement parseFromFile(String source) throws IOException, JsonException { 124 return parse(TextFile.fileToString(source)); 125 } 126 127 public static JsonElement parseFromUrl(String source) throws IOException, JsonException { 128 return parse(fetch(source)); 129 } 130 131 public static JsonElement parse(InputStream stream, boolean isJson5) throws IOException, JsonException { 132 return parse(TextFile.streamToString(stream), isJson5); 133 } 134 135 public static JsonElement parse(byte[] stream, boolean isJson5) throws IOException, JsonException { 136 return parse(TextFile.bytesToString(stream), isJson5); 137 } 138 139 public static JsonElement parse(String source, boolean isJson5) throws IOException, JsonException { 140 return parse(source, isJson5, false); 141 } 142 143 public static JsonElement parseFromUrl(String source, boolean isJson5) throws IOException, JsonException { 144 return parse(fetch(source), isJson5); 145 } 146 147 public static JsonElement parse(InputStream stream, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException { 148 return parse(TextFile.streamToString(stream), isJson5, allowDuplicates); 149 } 150 151 public static JsonElement parse(byte[] stream, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException { 152 return parse(TextFile.bytesToString(stream), isJson5, allowDuplicates); 153 } 154 155 public static JsonElement parse(String source, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException { 156 return new JsonParser().parseJsonElement(source, isJson5, allowDuplicates); 157 } 158 159 160 // ================================================================ 161 162 public static String compose(JsonElement element) { 163 return compose(element, false); 164 } 165 166 public static void compose(JsonElement element, OutputStream stream) throws IOException { 167 compose(element, stream, false); 168 } 169 170 public static byte[] composeBytes(JsonElement element) { 171 return composeBytes(element, false); 172 } 173 174 public static String compose(JsonElement element, boolean pretty) { 175 return new JsonParser().write(element, pretty); 176 } 177 178 public static void compose(JsonElement element, OutputStream stream, boolean pretty) throws IOException { 179 byte[] cnt = composeBytes(element, pretty); 180 stream.write(cnt); 181 } 182 183 public static byte[] composeBytes(JsonElement element, boolean pretty) { 184 String s = compose(element, pretty); 185 return s.getBytes(StandardCharsets.UTF_8); 186 } 187 188 // ================================================================ 189 190 enum ItemType { 191 Object, String, Number, Boolean, Array, End, Eof, Null; 192 } 193 private JsonLexer lexer; 194 private ItemType itemType = ItemType.Object; 195 private String itemName; 196 private String itemValue; 197 private boolean allowDuplicates = true; 198 private boolean allowComments = false; 199 private boolean allowNoComma = false; 200 private JsonLocationData startProperty; 201 private JsonLocationData endProperty; 202 private boolean itemNoComma; 203 private boolean allowUnquotedStrings; 204 private boolean itemUnquoted; 205 private boolean valueUnquoted; 206 207 private JsonObject parseJsonObject(String source, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException { 208 this.allowDuplicates = allowDuplicates; 209 this.allowComments = isJson5; 210 this.allowNoComma = isJson5; 211 this.allowUnquotedStrings = isJson5; 212 return parseSource(Utilities.stripBOM(source)); 213 } 214 215 private JsonObject parseSource(String source) throws IOException, JsonException { 216 lexer = new JsonLexer(source, allowComments, allowUnquotedStrings); 217 JsonObject result = new JsonObject(); 218 lexer.takeComments(result); 219 result.setStart(lexer.getLastLocationAWS().copy()); 220 if (lexer.getType() == TokenType.Open) { 221 lexer.next(); 222 lexer.getStates().push(new State("", true)); 223 } 224 else if (lexer.getType() != null) { 225 throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString()); 226 } else { 227 throw lexer.error("Unexpected content at start of JSON"); 228 } 229 230 if (lexer.getType() != TokenType.Close) { 231 parseProperty(); 232 readObject("$", result, true); 233 } 234 result.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy()); 235 return result; 236 } 237 238 private JsonElement parseJsonElement(String source, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException { 239 this.allowDuplicates = allowDuplicates; 240 this.allowComments = isJson5; 241 this.allowNoComma = isJson5; 242 this.allowUnquotedStrings = isJson5; 243 return parseSourceElement(Utilities.stripBOM(source)); 244 } 245 246 private JsonElement parseSourceElement(String source) throws IOException, JsonException { 247 lexer = new JsonLexer(source, allowComments, allowUnquotedStrings); 248 switch (lexer.getType()) { 249 case Boolean: 250 JsonBoolean bool = new JsonBoolean(lexer.getValue().equals("true")); 251 lexer.takeComments(bool); 252 bool.setStart(lexer.getLastLocationAWS().copy()); 253 bool.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy()); 254 return bool; 255 case Null: 256 JsonNull nll = new JsonNull(); 257 lexer.takeComments(nll); 258 nll.setStart(lexer.getLastLocationAWS().copy()); 259 nll.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy()); 260 return nll; 261 case Number: 262 JsonNumber num = new JsonNumber(lexer.getValue()); 263 lexer.takeComments(num); 264 num.setStart(lexer.getLastLocationAWS().copy()); 265 num.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy()); 266 return num; 267 case Open: 268 JsonObject obj = new JsonObject(); 269 lexer.takeComments(obj); 270 obj.setStart(lexer.getLastLocationAWS().copy()); 271 if (lexer.getType() == TokenType.Open) { 272 lexer.next(); 273 lexer.getStates().push(new State("", true)); 274 } 275 else 276 throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString()); 277 278 if (lexer.getType() != TokenType.Close) { 279 parseProperty(); 280 readObject("$", obj, true); 281 } 282 obj.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy()); 283 return obj; 284 case OpenArray: 285 JsonArray arr = new JsonArray(); 286 lexer.takeComments(arr); 287 arr.setStart(lexer.getLastLocationAWS().copy()); 288 lexer.next(); 289 lexer.getStates().push(new State("", false)); 290 if (lexer.getType() != TokenType.CloseArray) { 291 parseProperty(); 292 readArray("$", arr, true); 293 } 294 arr.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy()); 295 return arr; 296 case String: 297 JsonString str = new JsonString(lexer.getValue()); 298 lexer.takeComments(str); 299 str.setStart(lexer.getLastLocationAWS().copy()); 300 str.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy()); 301 return str; 302 default: 303 } 304 throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString()); 305 } 306 307 private void readObject(String path, JsonObject obj, boolean root) throws IOException, JsonException { 308 while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) { 309 obj.setExtraComma(false); 310 switch (itemType) { 311 case Object: 312 JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName); 313 child.setStart(startProperty.copy()); 314 lexer.takeComments(child); 315 if (obj.has(itemName) && !allowDuplicates) 316 throw lexer.error("Duplicated property name: "+itemName+ " @ "+path); 317 obj.addForParser(itemName, child, itemNoComma, itemUnquoted, valueUnquoted); 318 next(); 319 readObject(path+"."+itemName, child, false); 320 child.setEnd(endProperty.copy()); 321 break; 322 case Boolean : 323 JsonBoolean childB = new JsonBoolean(Boolean.valueOf(itemValue)); 324 childB.setStart(startProperty.copy()); 325 lexer.takeComments(childB); 326 if (obj.has(itemName) && !allowDuplicates) 327 throw lexer.error("Duplicated property name: "+itemName+ " @ "+path); 328 obj.addForParser(itemName, childB, itemNoComma, itemUnquoted, valueUnquoted); 329 childB.setEnd(endProperty.copy()); 330 break; 331 case String: 332 JsonString childS = new JsonString(itemValue); 333 childS.setStart(startProperty.copy()); 334 lexer.takeComments(childS); 335 if (obj.has(itemName) && !allowDuplicates) 336 throw lexer.error("Duplicated property name: "+itemName+ " @ "+path); 337 obj.addForParser(itemName, childS, itemNoComma, itemUnquoted, valueUnquoted); 338 childS.setEnd(endProperty.copy()); 339 break; 340 case Number: 341 JsonNumber childN = new JsonNumber(itemValue); 342 childN.setStart(startProperty.copy()); 343 lexer.takeComments(childN); 344 if (obj.has(itemName) && !allowDuplicates) 345 throw lexer.error("Duplicated property name: "+itemName+ " @ "+path); 346 obj.addForParser(itemName, childN, itemNoComma, itemUnquoted, valueUnquoted); 347 childN.setEnd(endProperty.copy()); 348 break; 349 case Null: 350 JsonNull childn = new JsonNull(); 351 childn.setStart(startProperty.copy()); 352 lexer.takeComments(childn); 353 if (obj.has(itemName) && !allowDuplicates) 354 throw lexer.error("Duplicated property name: "+itemName+ " @ "+path); 355 obj.addForParser(itemName, childn, itemNoComma, itemUnquoted, valueUnquoted); 356 childn.setEnd(endProperty.copy()); 357 break; 358 case Array: 359 JsonArray childA = new JsonArray(); // (obj.path+'.'+ItemName); 360 childA.setStart(startProperty.copy()); 361 lexer.takeComments(childA); 362 if (obj.has(itemName) && !allowDuplicates) 363 throw lexer.error("Duplicated property name: "+itemName+ " @ "+path); 364 obj.addForParser(itemName, childA, itemNoComma, itemUnquoted, valueUnquoted); 365 next(); 366 if (!readArray(path+"."+itemName, childA, false)) 367 next(true); 368 if (childA.getEnd() == null) { 369 childA.setEnd(endProperty.copy()); 370 } 371 break; 372 case Eof : 373 throw lexer.error("Unexpected End of File"); 374 case End: 375 throw lexer.error("Unexpected End"); // Don't think we can get here 376 } 377 itemNoComma = false; 378 endProperty = lexer.getLocation().copy(); 379 obj.setExtraComma(lexer.getType() == TokenType.Comma); 380 next(); 381 } 382 } 383 384 private boolean readArray(String path, JsonArray arr, boolean root) throws IOException, JsonException { 385 boolean res = false; 386 while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) { 387 res = true; 388 arr.setExtraComma(false); 389 switch (itemType) { 390 case Object: 391 JsonObject obj = new JsonObject(); // (arr.path+'['+inttostr(i)+']'); 392 obj.setStart(startProperty.copy()); 393 lexer.takeComments(obj); 394 arr.addForParser(obj, itemNoComma, valueUnquoted); 395 next(); 396 readObject(path+"["+(arr.size()-1)+"]", obj, false); 397 obj.setEnd(endProperty.copy()); 398 break; 399 case String: 400 JsonString s = new JsonString(itemValue); 401 s.setStart(startProperty.copy()); 402 lexer.takeComments(s); 403 arr.addForParser(s, itemNoComma, valueUnquoted); 404 s.setEnd(endProperty.copy()); 405 break; 406 case Number: 407 JsonNumber n = new JsonNumber(itemValue); 408 n.setStart(startProperty.copy()); 409 lexer.takeComments(n); 410 arr.addForParser(n, itemNoComma, valueUnquoted); 411 n.setEnd(endProperty.copy()); 412 break; 413 case Boolean: 414 JsonBoolean b = new JsonBoolean("true".equals(itemValue)); 415 b.setStart(startProperty.copy()); 416 lexer.takeComments(b); 417 arr.addForParser(b, itemNoComma, valueUnquoted); 418 b.setEnd(endProperty.copy()); 419 break; 420 case Null : 421 JsonNull nn = new JsonNull(); 422 nn.setStart(startProperty.copy()); 423 lexer.takeComments(nn); 424 arr.addForParser(nn, itemNoComma, valueUnquoted); 425 nn.setEnd(endProperty.copy()); 426 break; 427 case Array: 428 JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']'); 429 child.setStart(startProperty.copy()); 430 lexer.takeComments(child); 431 arr.addForParser(child, itemNoComma, valueUnquoted); 432 next(); 433 readArray(path+"["+(arr.size()-1)+"]", child, false); 434 child.setEnd(endProperty.copy()); 435 break; 436 case Eof : 437 throw lexer.error("Unexpected End of File"); 438 case End: 439 throw lexer.error("Can't get here"); 440 } 441 itemNoComma = false; 442 arr.setEnd(lexer.getLocation().copy()); 443 arr.setExtraComma(lexer.getType() == TokenType.Comma); 444 next(); 445 } 446 return res; 447 } 448 449 private void next() throws IOException { 450 next(false); 451 } 452 453 private void next(boolean noPop) throws IOException { 454 switch (itemType) { 455 case Object : 456 lexer.consume(TokenType.Open); 457 lexer.getStates().push(new State(itemName, true)); 458 if (lexer.getType() == TokenType.Close) { 459 itemType = ItemType.End; 460 lexer.next(); 461 } else 462 parseProperty(); 463 break; 464 case Null: 465 case String: 466 case Number: 467 case End: 468 case Boolean : 469 if (itemType == ItemType.End && !noPop) 470 lexer.getStates().pop(); 471 if (lexer.getType() == TokenType.Comma) { 472 lexer.next(); 473 if (allowNoComma && (lexer.getType() == TokenType.CloseArray || lexer.getType() == TokenType.Close)) { 474 itemType = ItemType.End; 475 lexer.next(); 476 } else { 477 parseProperty(); 478 } 479 } else if (lexer.getType() == TokenType.Close) { 480 itemType = ItemType.End; 481 lexer.next(); 482 } else if (lexer.getType() == TokenType.CloseArray) { 483 itemType = ItemType.End; 484 lexer.next(); 485 } else if (lexer.getType() == TokenType.Eof) { 486 itemType = ItemType.Eof; 487 } else if (allowNoComma && (lexer.getType() == TokenType.String || (!lexer.getStates().peek().isProp()) && lexer.getType().isValueType())) { 488 itemNoComma = true; 489 parseProperty(); 490 } else { 491 throw lexer.error("Unexpected JSON syntax"); 492 } 493 break; 494 case Array : 495 lexer.next(); 496 lexer.getStates().push(new State(itemName+"[]", false)); 497 parseProperty(); 498 break; 499 case Eof : 500 throw lexer.error("JSON Syntax Error - attempt to read past end of json stream"); 501 default: 502 throw lexer.error("not done yet (a): "+itemType.toString()); 503 } 504 } 505 506 private void parseProperty() throws IOException { 507 if (lexer.getStates().peek().isProp()) { 508 itemUnquoted = lexer.isUnquoted(); 509 itemName = lexer.consume(TokenType.String); 510 itemValue = null; 511 lexer.consume(TokenType.Colon); 512 } 513 startProperty = lexer.getLastLocationAWS().copy(); 514 endProperty = lexer.getLocation().copy(); 515 valueUnquoted = lexer.isUnquoted(); 516 switch (lexer.getType()) { 517 case Null : 518 itemType = ItemType.Null; 519 itemValue = lexer.getValue(); 520 lexer.next(); 521 break; 522 case String : 523 itemType = ItemType.String; 524 itemValue = lexer.getValue(); 525 lexer.next(); 526 break; 527 case Boolean : 528 itemType = ItemType.Boolean; 529 itemValue = lexer.getValue(); 530 lexer.next(); 531 break; 532 case Number : 533 itemType = ItemType.Number; 534 itemValue = lexer.getValue(); 535 lexer.next(); 536 break; 537 case Open : 538 itemType = ItemType.Object; 539 break; 540 case OpenArray : 541 itemType = ItemType.Array; 542 break; 543 case CloseArray : 544 itemType = ItemType.End; 545 break; 546 // case Close, , case Colon, case Comma, case OpenArray, ! 547 default: 548 throw lexer.error("not done yet (b): "+lexer.getType().toString()); 549 } 550 } 551 552 private String write(JsonElement element, boolean pretty) { 553 StringBuilder b = new StringBuilder(); 554 if (pretty && element.hasComments()) { 555 writeComments(b, element.getComments(), 0); 556 } 557 write(b, element, pretty, 0); 558 if (pretty) { 559 b.append("\n"); 560 } 561 return b.toString(); 562 } 563 564 private void writeComments(StringBuilder b, List<JsonComment> comments, int indent) { 565 for (JsonComment s : comments) { 566 b.append("// "); 567 b.append(s.getContent()); 568 b.append("\n"); 569 b.append(Utilities.padLeft("", ' ', indent)); 570 } 571 } 572 573 private void write(StringBuilder b, JsonElement e, boolean pretty, int indent) { 574 switch (e.type()) { 575 case ARRAY: 576 JsonArray arr = (JsonArray) e; 577 b.append("["); 578 boolean first = true; 579 boolean complex = arr.size() > 6; // arbitrary cut off 580 if (!complex) { 581 int length = 0; 582 for (JsonElement i : arr.getItems()) { 583 if (i instanceof JsonPrimitive) { 584 length = length + ((JsonPrimitive)i).toJson().length(); 585 } 586 if (i.type() == JsonElementType.ARRAY || i.type() == JsonElementType.OBJECT 587 || i.hasComments()) { // 20 is a somewhat arbitrary cut off 588 complex = true; 589 } 590 } 591 if (length > 60) { 592 complex = true; 593 } 594 } 595 for (JsonElement i : arr.getItems()) { 596 if (first) first = false; else b.append(pretty && !complex ? ", " : ","); 597 if (pretty && complex) { 598 b.append("\n"); 599 b.append(Utilities.padLeft("", ' ', indent+2)); 600 if (i.hasComments()) { 601 writeComments(b, i.getComments(), indent+2); 602 } 603 } 604 write(b, i, pretty && complex, indent+2); 605 } 606 if (pretty && complex) { 607 b.append("\n"); 608 b.append(Utilities.padLeft("", ' ', indent)); 609 } 610 b.append("]"); 611 break; 612 case BOOLEAN: 613 b.append(((JsonBoolean) e).getValue()); 614 break; 615 case NULL: 616 b.append(((JsonNull) e).getValue()); 617 break; 618 case NUMBER: 619 b.append(((JsonNumber) e).getValue()); 620 break; 621 case OBJECT: 622 b.append("{"); 623 first = true; 624 for (JsonProperty p : ((JsonObject) e).getProperties()) { 625 if (first) first = false; else b.append(","); 626 if (pretty) { 627 b.append("\n"); 628 b.append(Utilities.padLeft("", ' ', indent+2)); 629 if (p.getValue().hasComments()) { 630 writeComments(b, p.getValue().getComments(), indent+2); 631 } 632 } 633 b.append("\""); 634 b.append(p.getName()); 635 b.append(pretty ? "\" : " : "\":"); 636 write(b, p.getValue(), pretty, indent+2); 637 } 638 if (pretty) { 639 b.append("\n"); 640 b.append(Utilities.padLeft("", ' ', indent)); 641 } 642 b.append("}"); 643 break; 644 case STRING: 645 b.append("\""); 646 b.append(Utilities.escapeJson(((JsonString) e).getValue())); 647 b.append("\""); 648 break; 649 default: 650 throw new Error("Can't get here"); 651 } 652 } 653 654 private static byte[] fetch(String source) throws IOException { 655 SimpleHTTPClient fetcher = new SimpleHTTPClient(); 656 fetcher.addHeader("Accept", "application/json, application/fhir+json"); 657 HTTPResult res = fetcher.get(source+"?nocache=" + System.currentTimeMillis()); 658 res.checkThrowException(); 659 return res.getContent(); 660 } 661}