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.apache.commons.lang3.NotImplementedException;
072import org.hl7.fhir.exceptions.FHIRFormatError;
073import org.hl7.fhir.instance.model.api.IIdType;
074import org.hl7.fhir.r5.model.Base;
075import org.hl7.fhir.r5.model.DataType;
076import org.hl7.fhir.r5.model.DomainResource;
077import org.hl7.fhir.r5.model.Element;
078import org.hl7.fhir.r5.model.IdType;
079import org.hl7.fhir.r5.model.NamedElementExtension;
080import org.hl7.fhir.r5.model.PrimitiveType;
081import org.hl7.fhir.r5.model.Resource;
082import org.hl7.fhir.r5.model.StringType;
083import org.hl7.fhir.r5.test.utils.ClassesLoadedFlags;
084import org.hl7.fhir.utilities.FileUtilities;
085import org.hl7.fhir.utilities.Utilities;
086import org.hl7.fhir.utilities.json.JsonTrackingParser;
087import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
088import org.hl7.fhir.utilities.xhtml.XhtmlNode;
089import org.hl7.fhir.utilities.xhtml.XhtmlParser;
090import org.hl7.fhir.utilities.xml.IXMLWriter;
091
092import com.google.gson.JsonArray;
093import com.google.gson.JsonElement;
094import com.google.gson.JsonObject;
095import com.google.gson.JsonSyntaxException;
096
097/**
098 * General parser for JSON content. You instantiate an JsonParser of these, but you 
099 * actually use parse or parseGeneral defined on this class
100 * 
101 * The two classes are separated to keep generated and manually maintained code apart.
102 */
103public abstract class JsonParserBase extends ParserBase implements IParser {
104
105  static {
106//    LoggerFactory.getLogger("org.hl7.fhir.r5.formats.JsonParserBase").debug("JSON Parser is being loaded");
107    ClassesLoadedFlags.ourJsonParserBaseLoaded = true;
108  }
109
110  @Override
111  public ParserType getType() {
112          return ParserType.JSON;
113  }
114
115        // private static com.google.gson.JsonParser  parser = new com.google.gson.JsonParser();
116  
117  // -- in descendent generated code --------------------------------------
118  
119  abstract protected Resource parseResource(JsonObject json) throws IOException, FHIRFormatError;
120  abstract protected DataType parseType(JsonObject json, String type) throws IOException, FHIRFormatError;
121  abstract protected DataType parseAnyType(JsonObject json, String type) throws IOException, FHIRFormatError;
122  abstract protected DataType parseType(String prefix, JsonObject json) throws IOException, FHIRFormatError;
123  abstract protected boolean hasTypeName(JsonObject json, String prefix);
124  abstract protected void composeResource(Resource resource) throws IOException;
125  abstract protected void composeTypeInner(DataType type) throws IOException;
126
127  /* -- entry points --------------------------------------------------- */
128
129  protected Base parseBase(JsonObject json) throws IOException, FHIRFormatError {
130    throw new NotImplementedException("Still to do (for openEHR)");
131//    return parseType(json, null);
132  }
133  /**
134   * @throws FHIRFormatError 
135   * Parse content that is known to be a resource
136   * @throws IOException 
137   * @throws  
138   */
139  @Override
140  public Resource parse(InputStream input) throws IOException, FHIRFormatError {
141    JsonObject json = loadJson(input);
142    return parseResource(json);
143  }
144
145  /**
146   * parse xml that is known to be a resource, and that has already been read into a JSON object  
147   * @throws IOException 
148   * @throws FHIRFormatError 
149   */
150  public Resource parse(JsonObject json) throws FHIRFormatError, IOException {
151    return parseResource(json);
152  }
153
154  @Override
155  public DataType parseType(InputStream input, String type) throws IOException, FHIRFormatError {
156    JsonObject json = loadJson(input);
157    return parseType(json, type);
158  }
159
160  @Override
161  public DataType parseAnyType(InputStream input, String type) throws IOException, FHIRFormatError {
162    JsonObject json = loadJson(input);
163    return parseAnyType(json, type);
164  }
165
166  protected JsonObject getJObject(JsonObject parent, String name) throws IOException {
167    JsonElement j = parent.get(name);
168    if (j == null) { 
169      return null;
170    }
171    if (!(j instanceof JsonObject)) {
172      throw new IOException("property "+name+" is a "+j.getClass()+" looking for an object");
173    }
174    return (JsonObject) j;
175  }
176  
177  protected JsonArray getJArray(JsonObject parent, String name) throws IOException {
178    JsonElement j = parent.get(name);
179    if (j == null) { 
180      return null;
181    }
182    if (!(j instanceof JsonArray)) {
183      throw new IOException("property "+name+" is a "+j.getClass()+" looking for an Array");
184    }
185    return (JsonArray) j;
186  }
187  
188  protected JsonObject getJsonObjectFromArray(JsonArray array, int i) throws IOException {
189    JsonElement e = array.get(i);
190    if (e.isJsonObject()) {
191      return (JsonObject) e;
192    }
193    if (e.isJsonNull()) {
194      return new JsonObject();
195    }
196    throw new IOException("Array item "+i+" is a "+e.getClass()+" looking for an Object");
197  }
198  
199  /**
200   * 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)
201   * @throws IOException 
202   */
203  @Override
204  public void compose(OutputStream stream, Resource resource) throws IOException {
205    OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8");
206    if (style == OutputStyle.CANONICAL) {
207      json = new JsonCreatorCanonical(osw);
208    } else if (style == OutputStyle.PRETTY) {
209      json = new JsonCreatorDirect(osw, true, false);
210    } else {
211      json = new JsonCreatorDirect(osw, false, false); // use this instead of Gson because this preserves decimal formatting
212    }
213    json.beginObject();
214    composeResource(resource);
215    json.endObject();
216    json.finish();
217    osw.flush();
218  }
219
220  protected boolean customCompose(Resource resource) throws IOException {
221    if (customResourceHandlers.containsKey(resource.fhirType())) {
222      customResourceHandlers.get(resource.fhirType()).composerJson(json).composeResource(resource);
223      return true;
224    } else {
225      return false;
226    }
227  }
228
229  protected boolean customCompose(String name, Resource resource) {
230    if (customResourceHandlers.containsKey(resource.fhirType())) {
231      throw new Error("Not sorted yet");
232      // customResourceHandlers.get(resource.fhirType()).parser().composeResource(name, resource);
233      // return true;
234    } else {
235      return false;
236    }
237  }
238
239  protected Resource parseCustomResource(String t, JsonObject json) throws FHIRFormatError, IOException {
240    if (customResourceHandlers.containsKey(t)) {
241      return customResourceHandlers.get(t).parserJson(allowComments, allowUnknownContent).parse(json);
242    } else {
243      return null;
244    }
245  }
246
247    
248  /**
249   * Compose a resource using a pre-existing JsonWriter
250   * @throws IOException 
251   */
252  public void compose(JsonCreator writer, Resource resource) throws IOException {
253    json = writer;
254    composeResource(resource);
255  }
256  
257  @Override
258  public void compose(OutputStream stream, DataType type, String rootName) throws IOException {
259    OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8");
260    if (style == OutputStyle.CANONICAL) {
261      json = new JsonCreatorCanonical(osw);
262    } else if (style == OutputStyle.PRETTY) {
263      json = new JsonCreatorDirect(osw, true, false);
264    } else {
265      json = new JsonCreatorDirect(osw, false, false); // use this instead of Gson because this preserves decimal formatting
266    }
267    json.beginObject();
268    composeTypeInner(type);
269    json.endObject();
270    json.finish();
271    osw.flush();
272  }
273  
274  /* -- json routines --------------------------------------------------- */
275
276  protected JsonCreator json;
277  private boolean htmlPretty;
278  
279  private JsonObject loadJson(InputStream input) throws JsonSyntaxException, IOException {
280    // the GSON parser is the fastest, but the least robust 
281    if (allowComments || allowUnknownContent) {
282      return JsonTrackingParser.parse(FileUtilities.streamToString(input), null, allowUnknownContent, allowComments);      
283    } else {
284      return (JsonObject) com.google.gson.JsonParser.parseString(FileUtilities.streamToString(input));
285    }
286  }
287  
288  protected void parseElementProperties(JsonObject json, Element e) throws IOException, FHIRFormatError {
289    if (json != null && json.has("id"))
290      e.setId(json.get("id").getAsString());
291    if (!Utilities.noString(e.getId()))
292      idMap.put(e.getId(), e);
293    if (json.has("fhir_comments") && handleComments) {
294      JsonArray array = json.getAsJsonArray("fhir_comments");
295      for (int i = 0; i < array.size(); i++) {
296        e.getFormatCommentsPre().add(array.get(i).getAsString());
297      }
298    }
299  }
300  
301  protected XhtmlNode parseXhtml(String value) throws IOException, FHIRFormatError {
302    XhtmlParser prsr = new XhtmlParser();
303    try {
304                return prsr.parse(value, "div").getChildNodes().get(0);
305        } catch (org.hl7.fhir.exceptions.FHIRFormatError e) {
306                throw new FHIRFormatError(e.getMessage(), e);
307        }
308  }
309  
310  protected DomainResource parseDomainResource(JsonObject json) throws FHIRFormatError, IOException {
311          return (DomainResource) parseResource(json);
312  }
313
314        protected void writeNull(String name) throws IOException {
315                json.nullValue();
316        }
317        protected void prop(String name, String value) throws IOException {
318                if (name != null)
319                        json.name(name);
320                json.value(value);
321        }
322
323  protected void prop(String name, java.lang.Boolean value) throws IOException {
324    if (name != null)
325      json.name(name);
326    json.value(value);
327  }
328
329  protected void prop(String name, BigDecimal value) throws IOException {
330    if (name != null)
331      json.name(name);
332    json.value(value);
333  }
334
335  protected void propNum(String name, String value) throws IOException {
336    if (name != null)
337      json.name(name);
338    json.valueNum(value);
339  }
340
341  protected void prop(String name, java.lang.Integer value) throws IOException {
342    if (name != null)
343      json.name(name);
344    json.value(value);
345  }
346
347        protected void composeXhtml(String name, XhtmlNode html) throws IOException {
348                if (!Utilities.noString(xhtmlMessage)) {
349      prop(name, "<div>!-- "+xhtmlMessage+" --></div>");
350                } else {
351                XhtmlComposer comp = new XhtmlComposer(XhtmlComposer.XML, htmlPretty);
352                prop(name, comp.compose(html));
353                }
354        }
355
356        protected void open(String name) throws IOException {
357                if (name != null) 
358                        json.name(name);
359                json.beginObject();
360        }
361
362        protected void close() throws IOException {
363                json.endObject();
364        }
365
366        protected void openArray(String name) throws IOException {
367                if (name != null) 
368                        json.name(name);
369                json.beginArray();
370        }
371
372        protected void closeArray() throws IOException {
373                json.endArray();
374        }
375
376        protected void openObject(String name) throws IOException {
377                if (name != null) 
378                        json.name(name);
379                json.beginObject();
380        }
381
382        protected void closeObject() throws IOException {
383                json.endObject();
384        }
385
386//  protected void composeBinary(String name, Binary element) {
387//    if (element != null) {
388//      prop("resourceType", "Binary");
389//      if (element.getXmlId() != null)
390//        prop("id", element.getXmlId());
391//      prop("contentType", element.getContentType());
392//      prop("content", toString(element.getContent()));
393//    }    
394//    
395//  }
396
397  protected boolean anyHasExtras(List<? extends Element> list) {
398    for (Element e : list) {
399      if (e.hasExtension() || !Utilities.noString(e.getId()))
400        return true;
401    }
402    return false;
403  }
404
405  protected boolean anyHasValue(List<? extends PrimitiveType> list) {
406    for (PrimitiveType e : list) {
407      if (e.hasValue())
408        return true;
409    }
410    return false;
411  }
412
413        protected boolean makeComments(Element element) {
414                return handleComments && (style != OutputStyle.CANONICAL) && !(element.getFormatCommentsPre().isEmpty() && element.getFormatCommentsPost().isEmpty());
415        }
416        
417  protected void composeDomainResource(String name, DomainResource e) throws IOException {
418          openObject(name);
419          composeResource(e);
420          close();
421          
422  }
423
424  protected abstract void composeType(String prefix, DataType type) throws IOException;
425
426  protected void composeBase(String prefix, Base type) throws IOException {
427    throw new NotImplementedException("Still to do (for openEHR)");
428    // composeType(prefix, (DataType) type);
429  }
430
431  abstract void composeStringCore(String name, StringType value, boolean inArray) throws IOException;
432
433  protected void composeStringCore(String name, IIdType value, boolean inArray) throws IOException {
434          composeStringCore(name, new StringType(value.getValue()), inArray);
435  }    
436
437  abstract void composeStringExtras(String name, StringType value, boolean inArray) throws IOException;
438
439  protected void composeStringExtras(String name, IIdType value, boolean inArray) throws IOException {
440          composeStringExtras(name, new StringType(value.getValue()), inArray);
441  }    
442  
443  protected void parseElementProperties(JsonObject theAsJsonObject, IIdType theReferenceElement) throws FHIRFormatError, IOException {
444          parseElementProperties(theAsJsonObject, (Element)theReferenceElement);
445  }
446
447  protected void parseElementProperties(JsonObject theAsJsonObject, IdType theReferenceElement) throws FHIRFormatError, IOException {
448          parseElementProperties(theAsJsonObject, (Element)theReferenceElement);
449  }
450
451
452  protected DataType parseNativePrimitive(JsonObject json, String string) {
453    throw new NotImplementedException("Still to do (for openEHR)");   
454  }
455  
456  protected void composeNativePrimitive(String string, DataType defaultValue) {
457    throw new NotImplementedException("Still to do (for openEHR)");
458  }
459
460}