001package org.hl7.fhir.r5.formats;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033/*
034Copyright (c) 2011+, HL7, Inc
035All rights reserved.
036
037Redistribution and use in source and binary forms, with or without modification, 
038are permitted provided that the following conditions are met:
039
040 * Redistributions of source code must retain the above copyright notice, this 
041   list of conditions and the following disclaimer.
042 * Redistributions in binary form must reproduce the above copyright notice, 
043   this list of conditions and the following disclaimer in the documentation 
044   and/or other materials provided with the distribution.
045 * Neither the name of HL7 nor the names of its contributors may be used to 
046   endorse or promote products derived from this software without specific 
047   prior written permission.
048
049THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
050ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
051WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
052IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
053INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
054NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
055PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
056WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
057ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
058POSSIBILITY OF SUCH DAMAGE.
059
060*/
061
062import java.io.IOException;
063import java.io.InputStream;
064import java.io.OutputStream;
065import java.io.OutputStreamWriter;
066import java.math.BigDecimal;
067import java.util.HashMap;
068import java.util.List;
069import java.util.Map;
070
071import org.hl7.fhir.exceptions.FHIRFormatError;
072import org.hl7.fhir.instance.model.api.IIdType;
073import org.hl7.fhir.r5.model.DataType;
074import org.hl7.fhir.r5.model.DomainResource;
075import org.hl7.fhir.r5.model.Element;
076import org.hl7.fhir.r5.model.IdType;
077import org.hl7.fhir.r5.model.PrimitiveType;
078import org.hl7.fhir.r5.model.Resource;
079import org.hl7.fhir.r5.model.StringType;
080import org.hl7.fhir.r5.test.utils.ClassesLoadedFlags;
081import org.hl7.fhir.utilities.FileUtilities;
082import org.hl7.fhir.utilities.Utilities;
083import org.hl7.fhir.utilities.json.JsonTrackingParser;
084import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
085import org.hl7.fhir.utilities.xhtml.XhtmlNode;
086import org.hl7.fhir.utilities.xhtml.XhtmlParser;
087import org.hl7.fhir.utilities.xml.IXMLWriter;
088
089import com.google.gson.JsonArray;
090import com.google.gson.JsonElement;
091import com.google.gson.JsonObject;
092import com.google.gson.JsonSyntaxException;
093
094/**
095 * General parser for JSON content. You instantiate an JsonParser of these, but you 
096 * actually use parse or parseGeneral defined on this class
097 * 
098 * The two classes are separated to keep generated and manually maintained code apart.
099 */
100public abstract class JsonParserBase extends ParserBase implements IParser {
101
102  static {
103//    LoggerFactory.getLogger("org.hl7.fhir.r5.formats.JsonParserBase").debug("JSON Parser is being loaded");
104    ClassesLoadedFlags.ourJsonParserBaseLoaded = true;
105  }
106
107  @Override
108  public ParserType getType() {
109          return ParserType.JSON;
110  }
111
112        // private static com.google.gson.JsonParser  parser = new com.google.gson.JsonParser();
113  
114  // -- in descendent generated code --------------------------------------
115  
116  abstract protected Resource parseResource(JsonObject json) throws IOException, FHIRFormatError;
117  abstract protected DataType parseType(JsonObject json, String type) throws IOException, FHIRFormatError;
118  abstract protected DataType parseAnyType(JsonObject json, String type) throws IOException, FHIRFormatError;
119  abstract protected DataType parseType(String prefix, JsonObject json) throws IOException, FHIRFormatError;
120  abstract protected boolean hasTypeName(JsonObject json, String prefix);
121  abstract protected void composeResource(Resource resource) throws IOException;
122  abstract protected void composeTypeInner(DataType type) throws IOException;
123
124  /* -- entry points --------------------------------------------------- */
125
126  /**
127   * @throws FHIRFormatError 
128   * Parse content that is known to be a resource
129   * @throws IOException 
130   * @throws  
131   */
132  @Override
133  public Resource parse(InputStream input) throws IOException, FHIRFormatError {
134    JsonObject json = loadJson(input);
135    return parseResource(json);
136  }
137
138  /**
139   * parse xml that is known to be a resource, and that has already been read into a JSON object  
140   * @throws IOException 
141   * @throws FHIRFormatError 
142   */
143  public Resource parse(JsonObject json) throws FHIRFormatError, IOException {
144    return parseResource(json);
145  }
146
147  @Override
148  public DataType parseType(InputStream input, String type) throws IOException, FHIRFormatError {
149    JsonObject json = loadJson(input);
150    return parseType(json, type);
151  }
152
153  @Override
154  public DataType parseAnyType(InputStream input, String type) throws IOException, FHIRFormatError {
155    JsonObject json = loadJson(input);
156    return parseAnyType(json, type);
157  }
158
159  protected JsonObject getJObject(JsonObject parent, String name) throws IOException {
160    JsonElement j = parent.get(name);
161    if (j == null) { 
162      return null;
163    }
164    if (!(j instanceof JsonObject)) {
165      throw new IOException("property "+name+" is a "+j.getClass()+" looking for an object");
166    }
167    return (JsonObject) j;
168  }
169  
170  protected JsonArray getJArray(JsonObject parent, String name) throws IOException {
171    JsonElement j = parent.get(name);
172    if (j == null) { 
173      return null;
174    }
175    if (!(j instanceof JsonArray)) {
176      throw new IOException("property "+name+" is a "+j.getClass()+" looking for an Array");
177    }
178    return (JsonArray) j;
179  }
180  
181  protected JsonObject getJsonObjectFromArray(JsonArray array, int i) throws IOException {
182    JsonElement e = array.get(i);
183    if (e.isJsonObject()) {
184      return (JsonObject) e;
185    }
186    if (e.isJsonNull()) {
187      return new JsonObject();
188    }
189    throw new IOException("Array item "+i+" is a "+e.getClass()+" looking for an Object");
190  }
191  
192  /**
193   * Compose a resource to a stream, possibly using pretty presentation for a human reader (used in the spec, for example, but not normally in production)
194   * @throws IOException 
195   */
196  @Override
197  public void compose(OutputStream stream, Resource resource) throws IOException {
198    OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8");
199    if (style == OutputStyle.CANONICAL) {
200      json = new JsonCreatorCanonical(osw);
201    } else if (style == OutputStyle.PRETTY) {
202      json = new JsonCreatorDirect(osw, true, false);
203    } else {
204      json = new JsonCreatorDirect(osw, false, false); // use this instead of Gson because this preserves decimal formatting
205    }
206    json.beginObject();
207    composeResource(resource);
208    json.endObject();
209    json.finish();
210    osw.flush();
211  }
212
213  protected boolean customCompose(Resource resource) throws IOException {
214    if (customResourceHandlers.containsKey(resource.fhirType())) {
215      customResourceHandlers.get(resource.fhirType()).composerJson(json).composeResource(resource);
216      return true;
217    } else {
218      return false;
219    }
220  }
221
222  protected boolean customCompose(String name, Resource resource) {
223    if (customResourceHandlers.containsKey(resource.fhirType())) {
224      throw new Error("Not sorted yet");
225      // customResourceHandlers.get(resource.fhirType()).parser().composeResource(name, resource);
226      // return true;
227    } else {
228      return false;
229    }
230  }
231
232  protected Resource parseCustomResource(String t, JsonObject json) throws FHIRFormatError, IOException {
233    if (customResourceHandlers.containsKey(t)) {
234      return customResourceHandlers.get(t).parserJson(allowComments, allowUnknownContent).parse(json);
235    } else {
236      return null;
237    }
238  }
239
240    
241  /**
242   * Compose a resource using a pre-existing JsonWriter
243   * @throws IOException 
244   */
245  public void compose(JsonCreator writer, Resource resource) throws IOException {
246    json = writer;
247    composeResource(resource);
248  }
249  
250  @Override
251  public void compose(OutputStream stream, DataType type, String rootName) throws IOException {
252    OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8");
253    if (style == OutputStyle.CANONICAL) {
254      json = new JsonCreatorCanonical(osw);
255    } else if (style == OutputStyle.PRETTY) {
256      json = new JsonCreatorDirect(osw, true, false);
257    } else {
258      json = new JsonCreatorDirect(osw, false, false); // use this instead of Gson because this preserves decimal formatting
259    }
260    json.beginObject();
261    composeTypeInner(type);
262    json.endObject();
263    json.finish();
264    osw.flush();
265  }
266  
267  /* -- json routines --------------------------------------------------- */
268
269  protected JsonCreator json;
270  private boolean htmlPretty;
271  
272  private JsonObject loadJson(InputStream input) throws JsonSyntaxException, IOException {
273    // the GSON parser is the fastest, but the least robust 
274    if (allowComments || allowUnknownContent) {
275      return JsonTrackingParser.parse(FileUtilities.streamToString(input), null, allowUnknownContent, allowComments);      
276    } else {
277      return (JsonObject) com.google.gson.JsonParser.parseString(FileUtilities.streamToString(input));
278    }
279  }
280  
281  protected void parseElementProperties(JsonObject json, Element e) throws IOException, FHIRFormatError {
282    if (json != null && json.has("id"))
283      e.setId(json.get("id").getAsString());
284    if (!Utilities.noString(e.getId()))
285      idMap.put(e.getId(), e);
286    if (json.has("fhir_comments") && handleComments) {
287      JsonArray array = json.getAsJsonArray("fhir_comments");
288      for (int i = 0; i < array.size(); i++) {
289        e.getFormatCommentsPre().add(array.get(i).getAsString());
290      }
291    }
292  }
293  
294  protected XhtmlNode parseXhtml(String value) throws IOException, FHIRFormatError {
295    XhtmlParser prsr = new XhtmlParser();
296    try {
297                return prsr.parse(value, "div").getChildNodes().get(0);
298        } catch (org.hl7.fhir.exceptions.FHIRFormatError e) {
299                throw new FHIRFormatError(e.getMessage(), e);
300        }
301  }
302  
303  protected DomainResource parseDomainResource(JsonObject json) throws FHIRFormatError, IOException {
304          return (DomainResource) parseResource(json);
305  }
306
307        protected void writeNull(String name) throws IOException {
308                json.nullValue();
309        }
310        protected void prop(String name, String value) throws IOException {
311                if (name != null)
312                        json.name(name);
313                json.value(value);
314        }
315
316  protected void prop(String name, java.lang.Boolean value) throws IOException {
317    if (name != null)
318      json.name(name);
319    json.value(value);
320  }
321
322  protected void prop(String name, BigDecimal value) throws IOException {
323    if (name != null)
324      json.name(name);
325    json.value(value);
326  }
327
328  protected void propNum(String name, String value) throws IOException {
329    if (name != null)
330      json.name(name);
331    json.valueNum(value);
332  }
333
334  protected void prop(String name, java.lang.Integer value) throws IOException {
335    if (name != null)
336      json.name(name);
337    json.value(value);
338  }
339
340        protected void composeXhtml(String name, XhtmlNode html) throws IOException {
341                if (!Utilities.noString(xhtmlMessage)) {
342      prop(name, "<div>!-- "+xhtmlMessage+" --></div>");
343                } else {
344                XhtmlComposer comp = new XhtmlComposer(XhtmlComposer.XML, htmlPretty);
345                prop(name, comp.compose(html));
346                }
347        }
348
349        protected void open(String name) throws IOException {
350                if (name != null) 
351                        json.name(name);
352                json.beginObject();
353        }
354
355        protected void close() throws IOException {
356                json.endObject();
357        }
358
359        protected void openArray(String name) throws IOException {
360                if (name != null) 
361                        json.name(name);
362                json.beginArray();
363        }
364
365        protected void closeArray() throws IOException {
366                json.endArray();
367        }
368
369        protected void openObject(String name) throws IOException {
370                if (name != null) 
371                        json.name(name);
372                json.beginObject();
373        }
374
375        protected void closeObject() throws IOException {
376                json.endObject();
377        }
378
379//  protected void composeBinary(String name, Binary element) {
380//    if (element != null) {
381//      prop("resourceType", "Binary");
382//      if (element.getXmlId() != null)
383//        prop("id", element.getXmlId());
384//      prop("contentType", element.getContentType());
385//      prop("content", toString(element.getContent()));
386//    }    
387//    
388//  }
389
390  protected boolean anyHasExtras(List<? extends Element> list) {
391    for (Element e : list) {
392      if (e.hasExtension() || !Utilities.noString(e.getId()))
393        return true;
394    }
395    return false;
396  }
397
398  protected boolean anyHasValue(List<? extends PrimitiveType> list) {
399    for (PrimitiveType e : list) {
400      if (e.hasValue())
401        return true;
402    }
403    return false;
404  }
405
406        protected boolean makeComments(Element element) {
407                return handleComments && (style != OutputStyle.CANONICAL) && !(element.getFormatCommentsPre().isEmpty() && element.getFormatCommentsPost().isEmpty());
408        }
409        
410  protected void composeDomainResource(String name, DomainResource e) throws IOException {
411          openObject(name);
412          composeResource(e);
413          close();
414          
415  }
416
417  protected abstract void composeType(String prefix, DataType type) throws IOException;
418
419  
420  abstract void composeStringCore(String name, StringType value, boolean inArray) throws IOException;
421
422  protected void composeStringCore(String name, IIdType value, boolean inArray) throws IOException {
423          composeStringCore(name, new StringType(value.getValue()), inArray);
424  }    
425
426  abstract void composeStringExtras(String name, StringType value, boolean inArray) throws IOException;
427
428  protected void composeStringExtras(String name, IIdType value, boolean inArray) throws IOException {
429          composeStringExtras(name, new StringType(value.getValue()), inArray);
430  }    
431  
432  protected void parseElementProperties(JsonObject theAsJsonObject, IIdType theReferenceElement) throws FHIRFormatError, IOException {
433          parseElementProperties(theAsJsonObject, (Element)theReferenceElement);
434  }
435
436  protected void parseElementProperties(JsonObject theAsJsonObject, IdType theReferenceElement) throws FHIRFormatError, IOException {
437          parseElementProperties(theAsJsonObject, (Element)theReferenceElement);
438  }
439
440}