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