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}