001package org.hl7.fhir.r5.elementmodel;
002
003import java.io.ByteArrayInputStream;
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.io.OutputStream;
039import java.io.OutputStreamWriter;
040import java.math.BigDecimal;
041import java.nio.charset.StandardCharsets;
042import java.util.ArrayList;
043import java.util.HashMap;
044import java.util.HashSet;
045import java.util.List;
046import java.util.Map;
047import java.util.Map.Entry;
048import java.util.Set;
049
050import org.hl7.fhir.exceptions.FHIRException;
051import org.hl7.fhir.exceptions.FHIRFormatError;
052import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
053import org.hl7.fhir.r5.context.ContextUtilities;
054import org.hl7.fhir.r5.context.IWorkerContext;
055import org.hl7.fhir.r5.elementmodel.Element.SpecialElement;
056import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
057import org.hl7.fhir.r5.fhirpath.FHIRPathEngine;
058import org.hl7.fhir.r5.formats.IParser.OutputStyle;
059import org.hl7.fhir.r5.formats.JsonCreator;
060import org.hl7.fhir.r5.formats.JsonCreatorCanonical;
061import org.hl7.fhir.r5.formats.JsonCreatorDirect;
062import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
063import org.hl7.fhir.r5.model.ElementDefinition;
064import org.hl7.fhir.r5.model.StructureDefinition;
065import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
066import org.hl7.fhir.utilities.StringPair;
067import org.hl7.fhir.utilities.TextFile;
068import org.hl7.fhir.utilities.Utilities;
069import org.hl7.fhir.utilities.VersionUtilities;
070import org.hl7.fhir.utilities.i18n.I18nConstants;
071import org.hl7.fhir.utilities.json.model.JsonArray;
072import org.hl7.fhir.utilities.json.model.JsonComment;
073import org.hl7.fhir.utilities.json.model.JsonElement;
074import org.hl7.fhir.utilities.json.model.JsonNull;
075import org.hl7.fhir.utilities.json.model.JsonObject;
076import org.hl7.fhir.utilities.json.model.JsonPrimitive;
077import org.hl7.fhir.utilities.json.model.JsonProperty;
078import org.hl7.fhir.utilities.validation.ValidationMessage;
079import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
080import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
081import org.hl7.fhir.utilities.xhtml.XhtmlParser;
082
083
084public class JsonParser extends ParserBase {
085
086  private JsonCreator json;
087  private boolean allowComments;
088
089  private Element baseElement;
090
091  public JsonParser(IWorkerContext context, ProfileUtilities utilities) {
092    super(context, utilities);
093
094  }
095
096  public JsonParser(IWorkerContext context) {
097    super(context);
098  }
099
100  public Element parse(String source, String type) throws Exception {
101    return parse(source, type, false);
102  }
103  
104  public Element parse(String source, String type, boolean inner) throws Exception {
105    ValidatedFragment focusFragment = new ValidatedFragment(ValidatedFragment.FOCUS_NAME, "json", source.getBytes(StandardCharsets.UTF_8), false);
106    JsonObject obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true); 
107    String path = "/"+type;
108    StructureDefinition sd = getDefinition(focusFragment.getErrors(), -1, -1, type);
109    if (sd == null) {
110      return null;
111    }
112
113    if (inner) {
114      // we have an anonymous wrapper that has an arbitrarily named property with the specified type. We're going to invent a snapshot for that 
115      sd = new StructureDefinition();
116      sd.setType("Wrapper");
117      ElementDefinition bEd = sd.getSnapshot().addElement();
118      ElementDefinition nEd = sd.getSnapshot().addElement();
119      bEd.setPath("Wrapper");
120      nEd.setPath("Wrapper."+obj.getProperties().get(0).getName());
121      nEd.addType().setCode(type);
122      nEd.setMax(obj.getProperties().get(0).getValue().isJsonArray() ? "*" : "1"); 
123    }
124    Element result = new Element(type, new Property(context, sd.getSnapshot().getElement().get(0), sd, this.getProfileUtilities(), this.getContextUtilities())).setFormat(FhirFormat.JSON);
125    result.setPath(type);
126    checkObject(focusFragment.getErrors(), obj, result, path);
127    result.setType(type);
128    parseChildren(focusFragment.getErrors(), path, obj, result, true, null);
129    result.numberChildren();
130    return result;
131  }
132
133
134  @Override
135  public List<ValidatedFragment> parse(InputStream inStream) throws IOException, FHIRException {
136//    long start = System.currentTimeMillis();
137    byte[] content = TextFile.streamToBytes(inStream);
138    ValidatedFragment focusFragment = new ValidatedFragment(ValidatedFragment.FOCUS_NAME, "json", content, false);
139    
140    ByteArrayInputStream stream = new ByteArrayInputStream(content);
141    
142    // if we're parsing at this point, then we're going to use the custom parser
143    String source = TextFile.streamToString(stream);
144    JsonObject obj = null;
145    
146    if (policy == ValidationPolicy.EVERYTHING) {
147      try {
148        obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true); 
149      } catch (Exception e) {
150        logError(focusFragment.getErrors(), ValidationMessage.NO_RULE_DATE, -1, -1,context.formatMessage(I18nConstants.DOCUMENT), IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_, e.getMessage()), IssueSeverity.FATAL);
151      }
152    } else {
153      obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true); 
154    }
155    
156    if (obj != null) {
157      focusFragment.setElement(parse(focusFragment.getErrors(), obj));
158    }
159    List<ValidatedFragment> res = new ArrayList<>();
160    res.add(focusFragment);
161
162//    long  t =System.currentTimeMillis()-start;
163//    System.out.println("json parser: "+(t)+"ms, "+(content.length/1024)+"kb "+(t == 0 ? "" : " @ "+(content.length / t)+"kb/s"));
164    return res;
165  }
166
167  public Element parse(List<ValidationMessage> errors, JsonObject object) throws FHIRException {
168    return parse(errors, object, null);
169  }
170  
171  public Element parse(List<ValidationMessage> errors, JsonObject object, String statedPath) throws FHIRException {
172    if (object == null) {
173      System.out.println("What?");
174    }
175    StructureDefinition sd = getLogical();
176    String name;
177    String path;      
178    if (sd == null) {
179      JsonElement rt = object.get("resourceType");
180      if (rt == null) {
181        logError(errors, ValidationMessage.NO_RULE_DATE, line(object), col(object), "$", IssueType.INVALID, context.formatMessage(I18nConstants.UNABLE_TO_FIND_RESOURCETYPE_PROPERTY), IssueSeverity.FATAL);
182        return null;
183      } else if (!rt.isJsonString()) {
184        logError(errors, "2022-11-26", line(object), col(object), "$", IssueType.INVALID, context.formatMessage(I18nConstants.RESOURCETYPE_PROPERTY_WRONG_TYPE, rt.type().toName()), IssueSeverity.FATAL);
185        return null;
186      } else {
187        name = rt.asString();
188
189        sd = getDefinition(errors, line(object), col(object), name);
190        if (sd == null) {
191         return null;
192        }
193      }
194      path = statedPath == null ? name : statedPath;
195    } else {
196      name = sd.getType();
197      path = statedPath == null ? sd.getTypeTail() : statedPath;
198    }
199    baseElement = new Element(name, new Property(context, sd.getSnapshot().getElement().get(0), sd, this.getProfileUtilities(), this.getContextUtilities())).setFormat(FhirFormat.JSON);
200    checkObject(errors, object, baseElement, path);
201    baseElement.markLocation(line(object), col(object));
202    baseElement.setType(name);
203    baseElement.setPath(statedPath == null ? baseElement.fhirTypeRoot() : statedPath);
204    parseChildren(errors, path, object, baseElement, true, null);
205    baseElement.numberChildren();
206    return baseElement;
207  }
208
209  private void checkObject(List<ValidationMessage> errors, JsonObject object, Element b, String path) {
210    b.setNativeObject(object);
211    checkComments(errors, object, b, path);
212    if (policy == ValidationPolicy.EVERYTHING) {
213      if (object.getProperties().size() == 0) {
214        logError(errors, ValidationMessage.NO_RULE_DATE, line(object), col(object), path, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_MUST_HAVE_SOME_CONTENT), IssueSeverity.ERROR);
215      }
216    }    
217  }
218
219  private void checkComments(List<ValidationMessage> errors, JsonElement element, Element b, String path) throws FHIRFormatError {
220    if (element != null && element.hasComments()) {
221      if (allowComments) {
222        for (JsonComment c : element.getComments()) {
223          b.getComments().add(c.getContent());
224        }
225      } else {
226        for (JsonComment c : element.getComments()) {
227          logError(errors, "2022-11-26", c.getStart().getLine(), c.getStart().getCol(), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMENTS_NOT_ALLOWED), IssueSeverity.ERROR);
228        }        
229      }
230    }
231  }
232
233  private List<Property> parseChildren(List<ValidationMessage> errors, String path, JsonObject object, Element element, boolean hasResourceType, List<Property> properties) throws FHIRException {
234    if (properties == null) {
235      // save time refetching these if we're in a loop
236      properties = element.getProperty().getChildProperties(element.getName(), null);
237    }
238    processChildren(errors, path, object);
239
240    // first pass: process the properties
241    for (Property property : properties) {
242      parseChildItem(errors, path, object.getProperties(), element, property);
243    }
244
245    // second pass: check for things not processed (including duplicates)
246    checkNotProcessed(errors, path, element, hasResourceType, object.getProperties());
247    
248    
249    if (object.isExtraComma()) {
250      logError(errors, "2022-11-26", object.getEnd().getLine(), object.getEnd().getCol(), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Object"), IssueSeverity.ERROR);
251    }
252    return properties;
253  }
254
255  private void checkNotProcessed(List<ValidationMessage> errors, String path, Element element, boolean hasResourceType, List<JsonProperty> children) {
256    if (policy != ValidationPolicy.NONE) {
257      for (JsonProperty e : children) {
258        if (e.getTag() == 0) {
259          StructureDefinition sd = element.getProperty().isLogical() ? getContextUtilities().fetchByJsonName(e.getName()) : null;
260          if (sd != null) {
261            Property property = new Property(context, sd.getSnapshot().getElementFirstRep(), sd, element.getProperty().getUtils(), element.getProperty().getContextUtils());
262            parseChildItem(errors, path, children, element, property);
263          } else if ("fhir_comments".equals(e.getName()) && (VersionUtilities.isR2BVer(context.getVersion()) || VersionUtilities.isR2Ver(context.getVersion()))) {
264            if (!e.getValue().isJsonArray()) {
265              logError(errors, "2022-12-17", line(e.getValue()), col(e.getValue()), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.ILLEGAL_COMMENT_TYPE, e.getValue().type().toName()), IssueSeverity.ERROR);
266            } else {
267              for (JsonElement c : e.getValue().asJsonArray()) {
268                if (!c.isJsonString()) {
269                  logError(errors, "2022-12-17", line(e.getValue()), col(e.getValue()), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.ILLEGAL_COMMENT_TYPE, c.type().toName()), IssueSeverity.ERROR);
270                } else {
271                  element.getComments().add(c.asString());
272                }
273              }
274            }
275          } else if (hasResourceType && "resourceType".equals(e.getName())) {
276            // nothing
277          } else {
278            JsonProperty p = getFoundJsonPropertyByName(e.getName(), children);
279            if (p != null) {
280              logError(errors, "2022-11-26", line(e.getValue()), col(e.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.DUPLICATE_JSON_PROPERTY_KEY, e.getName()), IssueSeverity.ERROR);            
281            } else {
282              logError(errors, ValidationMessage.NO_RULE_DATE, line(e.getValue()), col(e.getValue()), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_, e.getName()), IssueSeverity.ERROR);
283            }
284          }
285        }
286      }
287    }    
288  }
289
290  private JsonProperty getFoundJsonPropertyByName(String name, List<JsonProperty> children) {
291    int hash = name.hashCode();
292    for (JsonProperty p : children) {
293      if (p.getTag() == 1 && hash == p.getNameHash()) {
294        return p;
295      }
296    }
297    return null;
298  }
299
300  private JsonProperty getJsonPropertyByName(String name, List<JsonProperty> children) {
301    int hash = name.hashCode();
302    for (JsonProperty p : children) {
303      if (p.getTag() == 0 && hash == p.getNameHash()) {
304        return p;
305      }
306    }
307    return null;
308  }
309  
310  private JsonProperty getJsonPropertyByBaseName(String name, List<JsonProperty> children) {
311    for (JsonProperty p : children) {
312      if (p.getTag() == 0 && p.getName().startsWith(name)) {
313        return p;
314      }
315    }
316    return null;
317  }
318  
319  private void processChildren(List<ValidationMessage> errors, String path, JsonObject object) {
320    for (JsonProperty p : object.getProperties()) {
321      if (p.isUnquotedName()) {
322        logError(errors, "2022-11-26", line(p.getValue()), col(p.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_PROPERTY_NO_QUOTES, p.getName()), IssueSeverity.ERROR);
323      }
324      if (p.isNoComma()) {
325        logError(errors, "2022-11-26", line(p.getValue()), col(p.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_MISSING), IssueSeverity.ERROR);        
326      }
327    }
328  }
329
330  public void parseChildItem(List<ValidationMessage> errors, String path, List<JsonProperty> children, Element context, Property property) {
331    JsonProperty jp = getJsonPropertyByName(property.getJsonName(), children);
332    if (property.isChoice() || property.getDefinition().getPath().endsWith("data[x]")) {
333      if (property.isJsonPrimitiveChoice()) {
334        if (jp != null) {
335          jp.setTag(1);
336          JsonElement je = jp.getValue();
337          String type = getTypeFromJsonType(je);
338          if (type == null) {
339            logError(errors, ValidationMessage.NO_RULE_DATE, line(je), col(je), path, IssueType.STRUCTURE, this.context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_TYPE, describeType(je), property.getName(), property.typeSummary()), IssueSeverity.ERROR);
340          } else if (property.hasType(type)) {
341            Property np = new Property(property.getContext(), property.getDefinition(), property.getStructure(), property.getUtils(), property.getContextUtils(), type);
342            parseChildPrimitive(errors, jp, getJsonPropertyByName("_"+property.getJsonName(), children), context, np, path, property.getName(), false);
343          } else {
344            logError(errors, ValidationMessage.NO_RULE_DATE, line(je), col(je), path, IssueType.STRUCTURE, this.context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_TYPE_WRONG, describeType(je), property.getName(), type, property.typeSummary()), IssueSeverity.ERROR);
345          }
346        }
347      } else { 
348        String baseName = property.getJsonName().substring(0, property.getName().length()-3);
349        jp = getJsonPropertyByBaseName(baseName, children);
350        JsonProperty jp1 = getJsonPropertyByBaseName("_"+baseName, children);
351        if (jp != null || jp1 != null) {
352          for (TypeRefComponent type : property.getDefinition().getType()) {
353            String eName = baseName + Utilities.capitalize(type.getWorkingCode());
354            if ((jp != null && jp.getName().equals(eName) || (jp1 != null && jp1.getName().equals("_"+eName)))) {
355              if (!isPrimitive(type.getWorkingCode()) && jp != null) {
356                parseChildComplex(errors, path, jp, context, property, eName, false);
357                break;
358              } else if (isPrimitive(type.getWorkingCode()) && (jp != null || jp1 != null)) {
359                parseChildPrimitive(errors, jp, jp1, context, property, path, eName, false);
360                break;
361              }
362            }
363          }
364        }
365      }
366    } else if (property.isPrimitive(property.getType(null))) {
367      parseChildPrimitive(errors, jp, getJsonPropertyByName("_"+property.getJsonName(), children), context, property, path, property.getJsonName(), property.hasJsonName());
368    } else if (jp != null) {
369      parseChildComplex(errors, path, jp, context, property, property.getJsonName(), property.hasJsonName());
370    }
371  }
372
373
374  private String getTypeFromJsonType(JsonElement je) {
375    if (je.isJsonPrimitive()) {
376      JsonPrimitive p = je.asJsonPrimitive();
377      if (p.isJsonString()) {
378        return "string";
379      } else if (p.isJsonBoolean()) {
380        return "boolean";
381      } else {
382        String s = p.asString();
383        if (Utilities.isInteger(s)) {
384          return "integer";
385        } else {
386          return "decimal";
387        }
388      }
389    } else {
390      return null;
391    }
392  }
393
394  private void parseChildComplex(List<ValidationMessage> errors, String path, JsonProperty p, Element element, Property property, String name, boolean isJsonName) throws FHIRException {
395    String npath = path+"."+property.getName();
396    String fpath = element.getPath()+"."+property.getName();
397    if (p != null) { p.setTag(1); }
398    JsonElement e = p == null ? null : p.getValue();
399    if (property.isList() && !property.isJsonKeyArray() && (e instanceof JsonArray)) {
400      JsonArray arr = (JsonArray) e;
401      if (arr.isExtraComma()) {
402        logError(errors, "2022-11-26", arr.getEnd().getLine(), arr.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Array"), IssueSeverity.ERROR);
403      }
404      if (arr.size() == 0) {
405        if (property.canBeEmpty()) {
406          // nothing
407        } else {
408          logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ARRAY_CANNOT_BE_EMPTY), IssueSeverity.ERROR);
409        }
410      }
411      int c = 0;
412      List<Property> properties = null;
413      for (JsonElement am : arr) {
414        properties = parseChildComplexInstance(errors, npath+"["+c+"]", fpath+"["+c+"]", element, property, name, am, c == 0 ? arr : null, path, properties);
415        c++;
416      }
417    } else if (property.isJsonKeyArray()) {
418      String code = property.getJsonKeyProperty();
419      List<Property> properties = property.getChildProperties(element.getName(), null);
420      if (properties.size() != 2) {
421        logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_CHILD_COUNT, propNames(properties)), IssueSeverity.ERROR);               
422      } else {
423        Property propK = properties.get(0);
424        Property propV = properties.get(1);
425        if (!propK.getName().equals(code)) {
426          logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_PROP_NAME, propNames(properties)), IssueSeverity.ERROR);                       
427        } else if (!propK.isPrimitive())  {
428          logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_PROP_TYPE, propNames(properties), propK.typeSummary()), IssueSeverity.ERROR);                       
429        } else if (propV.isList())  {
430          logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_NO_LIST, propV.getName()), IssueSeverity.ERROR);                       
431        } else if (propV.isChoice() && propV.getName().endsWith("[x]"))  {
432          logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_NO_CHOICE, propV.getName()), IssueSeverity.ERROR);                       
433        } else if (!(e instanceof JsonObject)) {
434          logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_OBJECT_NOT_, describe(e)), IssueSeverity.ERROR);                       
435        } else {
436          JsonObject o = (JsonObject) e;
437          if (o.isExtraComma()) {
438            logError(errors, "2022-11-26", o.getEnd().getLine(), o.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Object"), IssueSeverity.ERROR);
439          }
440
441          int i = 0;
442          Set<String> names = new HashSet<>();
443          for (JsonProperty pv : o.getProperties()) {
444            if (names.contains(pv.getName())) {
445              logError(errors, "2022-11-26", line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.DUPLICATE_JSON_PROPERTY_KEY, pv.getName()), IssueSeverity.ERROR);                                     
446            } else {
447              names.add(pv.getName());
448              pv.setTag(1);
449            }
450            // create an array entry
451            String npathArr = path+"."+property.getName()+"["+i+"]";
452            String fpathArr = element.getPath()+"."+property.getName()+"["+i+"]";
453            
454            Element n = new Element(name, property).markLocation(line(pv.getValue()), col(pv.getValue())).setFormat(FhirFormat.JSON);
455            n.setPath(fpath);
456            element.getChildren().add(n);
457            // handle the key
458            String fpathKey = fpathArr+"."+propK.getName();
459            Element nKey = new Element(code, propK).markLocation(line(pv.getValue()), col(pv.getValue())).setFormat(FhirFormat.JSON);
460            checkComments(errors, pv.getValue(), n, fpathArr);
461            nKey.setPath(fpathKey);
462            n.getChildren().add(nKey);
463            nKey.setValue(pv.getName());
464            
465
466            boolean ok = true;
467            Property pvl = propV;
468            if (propV.isJsonPrimitiveChoice()) {
469              ok = false;
470              String type = getTypeFromJsonType(pv.getValue());
471              if (type == null) {
472                logError(errors, ValidationMessage.NO_RULE_DATE, line(pv.getValue()), col(pv.getValue()), path, IssueType.STRUCTURE, this.context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_TYPE, describeType(pv.getValue()), propV.getName(), propV.typeSummary()), IssueSeverity.ERROR);
473              } else if (propV.hasType(type)) {
474                pvl = new Property(propV.getContext(), propV.getDefinition(), propV.getStructure(), propV.getUtils(), propV.getContextUtils(), type);
475                ok = true;
476              } else {
477                logError(errors, ValidationMessage.NO_RULE_DATE, line(pv.getValue()), col(pv.getValue()), path, IssueType.STRUCTURE, this.context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_TYPE_WRONG, describeType(pv.getValue()), propV.getName(), type, propV.typeSummary()), IssueSeverity.ERROR);
478              }
479            }
480            if (ok) {
481              // handle the value
482              String npathV = npathArr+"."+pvl.getName();
483              String fpathV = fpathArr+"."+pvl.getName();
484              if (propV.isPrimitive(pvl.getType(null))) {
485                parseChildPrimitiveInstance(errors, n, pvl, pvl.getName(), false, npathV, fpathV, pv.getValue(), null);
486              } else if (pv.getValue() instanceof JsonObject || pv.getValue() instanceof JsonNull) {
487                parseChildComplexInstance(errors, npathV, fpathV, n, pvl, pvl.getName(), pv.getValue(), null, null, null);
488              } else {
489                logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_OBJECT_NOT_, describe(pv.getValue())), IssueSeverity.ERROR);                       
490              }
491            }
492            i++;
493          }
494        }
495      }
496    } else {
497      if (property.isList()) {
498        logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_ARRAY_NOT_, describe(e), name, path), IssueSeverity.ERROR);
499      }
500      parseChildComplexInstance(errors, npath, fpath, element, property, name, e, null, null, null);
501    }
502  }
503
504  private Object propNames(List<Property> properties) {
505    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
506    for (Property p: properties) {
507      b.append(p.getName());
508    }
509    return b.toString();
510  }
511
512  private List<Property> parseChildComplexInstance(List<ValidationMessage> errors, String npath, String fpath, Element element, Property property, String name, JsonElement e, JsonElement commentContext, String commentPath, List<Property> properties) throws FHIRException {
513    if (property.hasTypeSpecifier()) {
514      FHIRPathEngine fpe = new FHIRPathEngine(context);
515      String type = null;
516      String cond = null;
517      for (StringPair sp : property.getTypeSpecifiers()) {
518        if (fpe.evaluateToBoolean(null, baseElement, baseElement, element, fpe.parse(sp.getName()))) {
519          type = sp.getValue();
520          cond = sp.getName();
521          break;
522        }
523      }
524      if (type != null) {
525        StructureDefinition sd = context.fetchResource(StructureDefinition.class, type);
526        if (sd == null) {
527          logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_ILLEGAL_TYPE, type, cond), IssueSeverity.ERROR);
528        } else {
529          if (sd.getAbstract()) {
530            logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_ABSTRACT_TYPE, type, cond), IssueSeverity.ERROR);
531          }
532          property = property.cloneToType(sd);
533        }
534      } else {
535        StructureDefinition sd = context.fetchTypeDefinition(property.getType());
536        if (sd == null) {
537          logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_NM_ILLEGAL_TYPE, property.getType()), IssueSeverity.ERROR);
538        } else if (sd.getAbstract()) {
539          logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_NM_ABSTRACT_TYPE, property.getType()), IssueSeverity.ERROR);
540        }        
541      }
542    }
543    if (e instanceof JsonObject) {
544      JsonObject child = (JsonObject) e;
545      Element n = new Element(name, property).markLocation(line(child), col(child)).setFormat(FhirFormat.JSON);
546      n.setPath(fpath);
547      checkComments(errors, commentContext, n, commentPath);        
548      checkObject(errors, child, n, npath);
549      element.getChildren().add(n);
550      if (property.isResource()) {
551        parseResource(errors, npath, child, n, property);
552      } else {
553        return parseChildren(errors, npath, child, n, false, properties);
554      }
555    } else if (property.isNullable() && e instanceof JsonNull) {
556      // we create an element marked as a null element so we know something was present
557      JsonNull child = (JsonNull) e;
558      Element n = new Element(name, property).markLocation(line(child), col(child)).setFormat(FhirFormat.JSON);
559      checkComments(errors, commentContext, n, commentPath);        
560      checkComments(errors, child, n, fpath);
561      n.setPath(fpath);
562      element.getChildren().add(n);
563      n.setNull(true);
564      // nothing to do, it's ok, but we treat it like it doesn't exist
565    } else {
566      logError(errors, ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE__NOT_, (property.isList() ? "an Array" : "an Object"), describe(e), name, npath), IssueSeverity.ERROR);
567    }
568    return null;
569  }
570
571  private String describe(JsonElement e) {
572    if (e instanceof JsonArray) {
573      return "an Array";
574    }
575    if (e instanceof JsonObject) {
576      return "an Object";
577    }
578    if (e instanceof JsonNull) {
579      return "a Null";
580    }
581    if (e instanceof JsonPrimitive) {
582      return "a Primitive property";
583    }
584    return null;
585  }
586
587  private String describeType(JsonElement e) {
588    return e.type().toName();
589  }
590
591//  JsonProperty main = children.containsKey(name) ? children.get(name) : null;
592//  JsonProperty fork = children.containsKey("_"+name) ? children.get("_"+name) : null;
593  private void parseChildPrimitive(List<ValidationMessage> errors, JsonProperty main, JsonProperty fork, Element element, Property property, String path, String name, boolean isJsonName) throws FHIRException {
594    String npath = path+"."+property.getName();
595    String fpath = element.getPath()+"."+property.getName();
596    if (main != null) { main.setTag(1); }
597    if (fork != null) { fork.setTag(1); }
598    
599    if (main != null && main.getValue().isJsonString() && main.isUnquotedValue()) {
600      logError(errors, "2022-11-26", line(main.getValue()), col(main.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_PROPERTY_VALUE_NO_QUOTES, main.getName(), main.getValue().asString()), IssueSeverity.ERROR);
601    }
602    if (main != null || fork != null) {
603      if (property.isList()) {
604        boolean ok = true;
605        if (!(main == null || main.getValue() instanceof JsonArray)) {
606          logError(errors, ValidationMessage.NO_RULE_DATE, line(main.getValue()), col(main.getValue()), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_ARRAY_NOT_, describe(main.getValue()), name, path), IssueSeverity.ERROR);
607          ok = false;
608        }
609        if (!(fork == null || fork.getValue() instanceof JsonArray)) {
610          logError(errors, ValidationMessage.NO_RULE_DATE, line(fork.getValue()), col(fork.getValue()), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_BASE_PROPERTY_MUST_BE_AN_ARRAY_NOT_, describe(main.getValue()), name, path), IssueSeverity.ERROR);
611          ok = false;
612        }
613        if (ok) {
614          JsonArray arr1 = (JsonArray) (main == null ? null : main.getValue());
615          JsonArray arr2 = (JsonArray) (fork == null ? null : fork.getValue());
616          if (arr1 != null && arr1.isExtraComma()) {
617            logError(errors, "2022-11-26", arr1.getEnd().getLine(), arr1.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Array"), IssueSeverity.ERROR);
618          }
619          if (arr2 != null && arr2.isExtraComma()) {
620            logError(errors, "2022-11-26", arr2.getEnd().getLine(), arr2.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Array"), IssueSeverity.ERROR);
621          }
622
623          for (int i = 0; i < Math.max(arrC(arr1), arrC(arr2)); i++) {
624            JsonElement m = arrI(arr1, i);
625            JsonElement f = arrI(arr2, i);
626            if (m != null && m.isJsonString() && arr1.isUnquoted(i)) {
627              logError(errors, "2022-11-26", line(m), col(m), path+"."+name+"["+i+"]", IssueType.INVALID, context.formatMessage(I18nConstants.JSON_PROPERTY_VALUE_NO_QUOTES, "item", m.asString()), IssueSeverity.ERROR);
628            }
629            parseChildPrimitiveInstance(errors, element, property, name, isJsonName, npath, fpath, m, f);
630          }
631        }
632      } else {
633        parseChildPrimitiveInstance(errors, element, property, name, isJsonName, npath, fpath, main == null ? null : main.getValue(), fork == null ? null : fork.getValue());
634      }
635    }
636  }
637
638  private JsonElement arrI(JsonArray arr, int i) {
639    return arr == null || i >= arr.size() || arr.get(i) instanceof JsonNull ? null : arr.get(i);
640  }
641
642  private int arrC(JsonArray arr) {
643    return arr == null ? 0 : arr.size();
644  }
645
646  private void parseChildPrimitiveInstance(List<ValidationMessage> errors, Element element, Property property, String name, boolean isJsonName, String npath, String fpath, JsonElement main, JsonElement fork) throws FHIRException {
647    if (main != null && !(main.isJsonBoolean() || main.isJsonNumber() || main.isJsonString())) {
648      logError(errors, ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(
649          I18nConstants.THIS_PROPERTY_MUST_BE_AN_SIMPLE_VALUE_NOT_, describe(main), name, npath), IssueSeverity.ERROR);
650    } else if (fork != null && !(fork instanceof JsonObject)) {
651      logError(errors, ValidationMessage.NO_RULE_DATE, line(fork), col(fork), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_OBJECT_NOT_, describe(fork), name, npath), IssueSeverity.ERROR);
652    } else {
653      Element n = new Element(isJsonName ? property.getName() : name, property).markLocation(line(main != null ? main : fork), col(main != null ? main : fork)).setFormat(FhirFormat.JSON);
654      if (main != null) {
655        checkComments(errors, main, n, npath);
656      }
657      if (fork != null) {
658        checkComments(errors, fork, n, npath);
659      }
660      n.setPath(fpath);
661      element.getChildren().add(n);
662      if (main != null) {
663        JsonPrimitive p = (JsonPrimitive) main;
664        n.setValue(property.hasImpliedPrefix() ? property.getImpliedPrefix()+p.asString() : p.asString());
665        if (!n.getProperty().isChoice() && n.getType().equals("xhtml")) {
666          try {
667            XhtmlParser xhtml = new XhtmlParser();
668            n.setXhtml(xhtml.setXmlMode(true).parse(n.getValue(), null).getDocumentElement());
669            if (policy == ValidationPolicy.EVERYTHING) {
670              for (StringPair s : xhtml.getValidationIssues()) {
671                logError(errors, "2022-11-17", line(main), col(main), npath, IssueType.INVALID, context.formatMessage(s.getName(), s.getValue()), IssueSeverity.ERROR);                
672              }
673            }
674          } catch (Exception e) {
675            logError(errors, ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_XHTML_, e.getMessage()), IssueSeverity.ERROR);
676          }
677        }
678        if (policy == ValidationPolicy.EVERYTHING) {
679          // now we cross-check the primitive format against the stated type
680          if (Utilities.existsInList(n.getType(), "boolean")) {
681            if (!p.isJsonBoolean()) {
682              logError(errors, ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_THE_PRIMITIVE_VALUE_MUST_BE_A_BOOLEAN), IssueSeverity.ERROR);
683            }
684          } else if (Utilities.existsInList(n.getType(), "integer", "unsignedInt", "positiveInt", "decimal")) {
685            if (!p.isJsonNumber())
686              logError(errors, ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_THE_PRIMITIVE_VALUE_MUST_BE_A_NUMBER), IssueSeverity.ERROR);
687          } else if (!p.isJsonString()) {
688            logError(errors, ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_THE_PRIMITIVE_VALUE_MUST_BE_A_STRING), IssueSeverity.ERROR);
689          }
690        }
691      }
692      if (fork != null) {
693        JsonObject child = (JsonObject) fork;
694        checkObject(errors, child, n, npath);
695        parseChildren(errors, npath, child, n, false, null);
696      }
697    }
698  }
699
700
701  private void parseResource(List<ValidationMessage> errors, String npath, JsonObject res, Element parent, Property elementProperty) throws FHIRException {
702    JsonElement rt = res.get("resourceType");
703    if (rt == null) {
704      logError(errors, ValidationMessage.NO_RULE_DATE, line(res), col(res), npath, IssueType.INVALID, context.formatMessage(I18nConstants.UNABLE_TO_FIND_RESOURCETYPE_PROPERTY), IssueSeverity.FATAL);
705    } else if (!rt.isJsonString()) {
706      logError(errors, "2022-11-26", line(res), col(res), npath, IssueType.INVALID, context.formatMessage(I18nConstants.RESOURCETYPE_PROPERTY_WRONG_TYPE, rt.type().toName()), IssueSeverity.FATAL);
707    } else {
708      String name = rt.asString();
709      StructureDefinition sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(name, null));
710      if (sd == null) {
711        logError(errors, ValidationMessage.NO_RULE_DATE, line(res), col(res), npath, IssueType.INVALID, context.formatMessage(I18nConstants.CONTAINED_RESOURCE_DOES_NOT_APPEAR_TO_BE_A_FHIR_RESOURCE_UNKNOWN_NAME_, name), IssueSeverity.FATAL);                            
712      } else {
713        parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd, this.getProfileUtilities(), this.getContextUtilities()), SpecialElement.fromProperty(parent.getProperty()), elementProperty);
714        parent.setType(name);
715        parseChildren(errors, npath, res, parent, true, null);
716      }
717    }
718    if (res.isExtraComma()) {
719      logError(errors, "2022-11-26", res.getEnd().getLine(), res.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Object"), IssueSeverity.ERROR);
720    }
721
722  }
723
724  private int line(JsonElement e) {
725    return e.getStart().getLine();
726  }
727
728  private int col(JsonElement e) {
729    return e.getEnd().getCol();
730  }
731
732
733  protected void prop(String name, String value, String link) throws IOException {
734    json.link(link);
735    if (name != null)
736      json.name(name);
737    json.value(value);
738  }
739
740  protected void open(String name, String link) throws IOException {
741    json.link(link);
742    if (name != null)
743      json.name(name);
744    json.beginObject();
745  }
746
747  protected void close() throws IOException {
748    json.endObject();
749  }
750
751  protected void openArray(String name, String link) throws IOException {
752    json.link(link);
753    if (name != null)
754      json.name(name);
755    json.beginArray();
756  }
757
758  protected void closeArray() throws IOException {
759    json.endArray();
760  }
761
762
763  @Override
764  public void compose(Element e, OutputStream stream, OutputStyle style, String identity) throws FHIRException, IOException {
765    if (e.getPath() == null) {
766      e.populatePaths(null);
767    }
768
769    OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8");
770    if (style == OutputStyle.CANONICAL) {
771      json = new JsonCreatorCanonical(osw);
772    } else if (style == OutputStyle.PRETTY) {
773      json = new JsonCreatorDirect(osw, true, allowComments);
774    } else {
775      json = new JsonCreatorDirect(osw, false, allowComments);
776    }
777    checkComposeComments(e);
778    json.beginObject();
779    prop("resourceType", e.getType(), null);
780    Set<String> done = new HashSet<String>();
781    for (Element child : e.getChildren()) {
782      compose(e.getName(), e, done, child);
783    }
784    json.endObject();
785    json.finish();
786    osw.flush();
787  }
788
789  private void checkComposeComments(Element e) {
790    for (String s : e.getComments()) {
791      json.comment(s);
792    }
793  }
794
795  public void compose(Element e, JsonCreator json) throws Exception {
796    if (e.getPath() == null) {
797      e.populatePaths(null);
798    }
799    
800    this.json = json;
801    checkComposeComments(e);
802    json.beginObject();
803
804    prop("resourceType", e.getType(), linkResolver == null ? null : linkResolver.resolveProperty(e.getProperty()));
805    Set<String> done = new HashSet<String>();
806    for (Element child : e.getChildren()) {
807      compose(e.getName(), e, done, child);
808    }
809    json.endObject();
810    json.finish();
811  }
812
813  private void compose(String path, Element e, Set<String> done, Element child) throws IOException {
814    checkComposeComments(child);
815    if (wantCompose(path, child)) {
816      boolean isList = child.hasElementProperty() ? child.getElementProperty().isList() : child.getProperty().isList();
817      if (!isList) {// for specials, ignore the cardinality of the stated type
818        compose(path, child);
819      } else if (!done.contains(child.getName())) {
820        done.add(child.getName());
821        List<Element> list = e.getChildrenByName(child.getName());
822        composeList(path, list);
823      }
824    }
825  }
826
827
828  private void composeList(String path, List<Element> list) throws IOException {
829    // there will be at least one element
830    String name = list.get(0).getName();
831    boolean complex = true;
832    if (list.get(0).isPrimitive()) {
833      boolean prim = false;
834      complex = false;
835      for (Element item : list) {
836        if (item.hasValue())
837          prim = true;
838        if (item.hasChildren())
839          complex = true;
840      }
841      if (prim) {
842        openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty()));
843        for (Element item : list) {
844          if (item.hasValue()) {
845            if (linkResolver != null && item.getProperty().isReference()) {
846              String ref = linkResolver.resolveReference(getReferenceForElement(item));
847              if (ref != null) {
848                json.externalLink(ref);
849              }
850            }
851            primitiveValue(null, item);
852          } else
853            json.nullValue();
854        }
855        closeArray();
856      }
857      name = "_"+name;
858    }
859    if (complex) {
860      openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty()));
861      for (Element item : list) {
862        if (item.hasChildren()) {
863          open(null,null);
864          if (item.getProperty().isResource()) {
865            prop("resourceType", item.getType(), linkResolver == null ? null : linkResolver.resolveType(item.getType()));
866          }
867          if (linkResolver != null && item.getProperty().isReference()) {
868            String ref = linkResolver.resolveReference(getReferenceForElement(item));
869            if (ref != null) {
870              json.externalLink(ref);
871            }
872          }
873          Set<String> done = new HashSet<String>();
874          for (Element child : item.getChildren()) {
875            compose(path+"."+name+"[]", item, done, child);
876          }
877          close();
878        } else
879          json.nullValue();
880      }
881      closeArray();
882    }
883  }
884
885  private void primitiveValue(String name, Element item) throws IOException {
886    if (name != null) {
887      if (linkResolver != null)
888        json.link(linkResolver.resolveProperty(item.getProperty()));
889      json.name(name);
890    }
891    String type = item.getType();
892    if (Utilities.existsInList(type, "boolean"))
893      json.value(item.getValue().trim().equals("true") ? new Boolean(true) : new Boolean(false));
894    else if (Utilities.existsInList(type, "integer", "unsignedInt", "positiveInt"))
895      json.value(new Integer(item.getValue()));
896    else if (Utilities.existsInList(type, "decimal"))
897      try {
898        json.value(new BigDecimal(item.getValue()));
899      } catch (Exception e) {
900        throw new NumberFormatException(context.formatMessage(I18nConstants.ERROR_WRITING_NUMBER__TO_JSON, item.getValue()));
901      }
902    else
903      json.value(item.getValue());
904  }
905
906  private void compose(String path, Element element) throws IOException {
907    String name = element.getName();
908    if (element.isPrimitive() || isPrimitive(element.getType())) {
909      if (element.hasValue())
910        primitiveValue(name, element);
911      name = "_"+name;
912      if (element.getType().equals("xhtml"))
913        json.anchor("end-xhtml");
914    }
915    if (element.hasChildren()) {
916      open(name, linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty()));
917      if (element.getProperty().isResource()) {
918        prop("resourceType", element.getType(), linkResolver == null ? null : linkResolver.resolveType(element.getType()));
919      }
920      if (linkResolver != null && element.getProperty().isReference()) {
921        String ref = linkResolver.resolveReference(getReferenceForElement(element));
922        if (ref != null) {
923          json.externalLink(ref);
924        }
925      }
926      Set<String> done = new HashSet<String>();
927      for (Element child : element.getChildren()) {
928        compose(path+"."+element.getName(), element, done, child);
929      }
930      close();
931    }
932  }
933
934
935  public boolean isAllowComments() {
936    return allowComments;
937  }
938
939  public JsonParser setAllowComments(boolean allowComments) {
940    this.allowComments = allowComments;
941    return this;
942  }
943
944
945}