001package org.hl7.fhir.r5.elementmodel;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009
010 * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012 * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015 * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029
030 */
031
032
033
034import java.io.IOException;
035import java.io.InputStream;
036import java.io.OutputStream;
037import java.io.OutputStreamWriter;
038import java.math.BigDecimal;
039import java.util.ArrayList;
040import java.util.HashMap;
041import java.util.HashSet;
042import java.util.List;
043import java.util.Map;
044import java.util.Map.Entry;
045import java.util.Set;
046
047import org.hl7.fhir.exceptions.FHIRException;
048import org.hl7.fhir.exceptions.FHIRFormatError;
049import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
050import org.hl7.fhir.r5.context.ContextUtilities;
051import org.hl7.fhir.r5.context.IWorkerContext;
052import org.hl7.fhir.r5.elementmodel.Element.SpecialElement;
053import org.hl7.fhir.r5.formats.IParser.OutputStyle;
054import org.hl7.fhir.r5.formats.JsonCreator;
055import org.hl7.fhir.r5.formats.JsonCreatorCanonical;
056import org.hl7.fhir.r5.formats.JsonCreatorDirect;
057import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
058import org.hl7.fhir.r5.model.StructureDefinition;
059import org.hl7.fhir.r5.utils.FHIRPathEngine;
060import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
061import org.hl7.fhir.utilities.StringPair;
062import org.hl7.fhir.utilities.TextFile;
063import org.hl7.fhir.utilities.Utilities;
064import org.hl7.fhir.utilities.VersionUtilities;
065import org.hl7.fhir.utilities.i18n.I18nConstants;
066import org.hl7.fhir.utilities.json.model.JsonArray;
067import org.hl7.fhir.utilities.json.model.JsonComment;
068import org.hl7.fhir.utilities.json.model.JsonElement;
069import org.hl7.fhir.utilities.json.model.JsonNull;
070import org.hl7.fhir.utilities.json.model.JsonObject;
071import org.hl7.fhir.utilities.json.model.JsonPrimitive;
072import org.hl7.fhir.utilities.json.model.JsonProperty;
073import org.hl7.fhir.utilities.validation.ValidationMessage;
074import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
075import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
076import org.hl7.fhir.utilities.xhtml.XhtmlParser;
077
078
079public class JsonParser extends ParserBase {
080
081  private JsonCreator json;
082  private boolean allowComments;
083
084  private ProfileUtilities profileUtilities;
085  private Element baseElement;
086
087  public JsonParser(IWorkerContext context, ProfileUtilities utilities) {
088    super(context);
089
090    this.profileUtilities = utilities;
091  }
092
093  public JsonParser(IWorkerContext context) {
094    super(context);
095
096    this.profileUtilities = new ProfileUtilities(this.context, null, null, new FHIRPathEngine(context));
097  }
098
099  public Element parse(String source, String type) throws Exception {
100    JsonObject obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true); 
101    String path = "/"+type;
102    StructureDefinition sd = getDefinition(-1, -1, type);
103    if (sd == null)
104      return null;
105
106    Element result = new Element(type, new Property(context, sd.getSnapshot().getElement().get(0), sd, this.profileUtilities));
107    result.setPath(type);
108    checkObject(obj, result, path);
109    result.setType(type);
110    parseChildren(path, obj, result, true);
111    result.numberChildren();
112    return result;
113  }
114
115
116  @Override
117  public List<NamedElement> parse(InputStream stream) throws IOException, FHIRException {
118    // if we're parsing at this point, then we're going to use the custom parser
119    List<NamedElement> res = new ArrayList<>();
120    String source = TextFile.streamToString(stream);
121    JsonObject obj = null;
122    if (policy == ValidationPolicy.EVERYTHING) {
123      try {
124        obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true); 
125      } catch (Exception e) {
126        logError(ValidationMessage.NO_RULE_DATE, -1, -1,context.formatMessage(I18nConstants.DOCUMENT), IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_, e.getMessage()), IssueSeverity.FATAL);
127        return null;
128      }
129    } else {
130      obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true); 
131    }
132    Element e = parse(obj);
133    if (e != null) {
134      res.add(new NamedElement(null, e));
135    }
136    return res;
137  }
138
139  public Element parse(JsonObject object) throws FHIRException {
140    StructureDefinition sd = getLogical();
141    String name;
142    String path;      
143    if (sd == null) {
144      JsonElement rt = object.get("resourceType");
145      if (rt == null) {
146        logError(ValidationMessage.NO_RULE_DATE, line(object), col(object), "$", IssueType.INVALID, context.formatMessage(I18nConstants.UNABLE_TO_FIND_RESOURCETYPE_PROPERTY), IssueSeverity.FATAL);
147        return null;
148      } else if (!rt.isJsonString()) {
149        logError("2022-11-26", line(object), col(object), "$", IssueType.INVALID, context.formatMessage(I18nConstants.RESOURCETYPE_PROPERTY_WRONG_TYPE, rt.type().toName()), IssueSeverity.FATAL);
150        return null;
151      } else {
152        name = rt.asString();
153
154        sd = getDefinition(line(object), col(object), name);
155        if (sd == null) {
156         return null;
157        }
158      }
159      path = name;
160    } else {
161      name = sd.getType();
162      path = sd.getTypeTail();
163    }
164    baseElement = new Element(name, new Property(context, sd.getSnapshot().getElement().get(0), sd, this.profileUtilities));
165    checkObject(object, baseElement, path);
166    baseElement.markLocation(line(object), col(object));
167    baseElement.setType(name);
168    baseElement.setPath(baseElement.fhirTypeRoot());
169    parseChildren(path, object, baseElement, true);
170    baseElement.numberChildren();
171    return baseElement;
172  }
173
174  private void checkObject(JsonObject object, Element b, String path) {
175    checkComments(object, b, path);
176    if (policy == ValidationPolicy.EVERYTHING) {
177      if (object.getProperties().size() == 0) {
178        logError(ValidationMessage.NO_RULE_DATE, line(object), col(object), path, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_MUST_HAVE_SOME_CONTENT), IssueSeverity.ERROR);
179      }
180    }    
181  }
182
183  private void checkComments(JsonElement element, Element b, String path) throws FHIRFormatError {
184    if (element != null && element.hasComments()) {
185      if (allowComments) {
186        for (JsonComment c : element.getComments()) {
187          b.getComments().add(c.getContent());
188        }
189      } else {
190        for (JsonComment c : element.getComments()) {
191          logError("2022-11-26", c.getStart().getLine(), c.getStart().getCol(), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMENTS_NOT_ALLOWED), IssueSeverity.ERROR);
192        }        
193      }
194    }
195  }
196
197  private void parseChildren(String path, JsonObject object, Element element, boolean hasResourceType) throws FHIRException {
198    reapComments(object, element);
199    List<Property> properties = element.getProperty().getChildProperties(element.getName(), null);
200    Set<String> processed = new HashSet<String>();
201    if (hasResourceType) {
202      processed.add("resourceType");
203    }
204    Map<String, JsonProperty> recognisedChildren = new HashMap<>();
205    Set<String> unique = new HashSet<>();
206    for (JsonProperty p : object.getProperties()) {
207      if (p.isUnquotedName()) {
208        logError("2022-11-26", line(p.getValue()), col(p.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_PROPERTY_NO_QUOTES, p.getName()), IssueSeverity.ERROR);
209      }
210      if (p.isNoComma()) {
211        logError("2022-11-26", line(p.getValue()), col(p.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_MISSING), IssueSeverity.ERROR);        
212      }
213      if (unique.contains(p.getName())) {
214        logError("2022-11-26", line(p.getValue()), col(p.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.DUPLICATE_JSON_PROPERTY, p.getName()), IssueSeverity.ERROR);
215      } else {
216        unique.add(p.getName());
217        recognisedChildren.put(p.getName(), p);        
218      }
219    }
220
221    // note that we do not trouble ourselves to maintain the wire format order here - we don't even know what it was anyway
222    // first pass: process the properties
223    for (Property property : properties) {
224      parseChildItem(path, recognisedChildren, element, processed, property);
225    }
226
227    // second pass: check for things not processed
228    if (policy != ValidationPolicy.NONE) {
229      for (Entry<String, JsonProperty> e : recognisedChildren.entrySet()) {
230        if (!processed.contains(e.getKey())) {
231          StructureDefinition sd = element.getProperty().isLogical() ? new ContextUtilities(context).fetchByJsonName(e.getKey()) : null;
232          if (sd != null) {
233            Property property = new Property(context, sd.getSnapshot().getElementFirstRep(), sd, element.getProperty().getUtils());
234            parseChildItem(path, recognisedChildren, element, null, property);
235          } else if ("fhir_comments".equals(e.getKey()) && (VersionUtilities.isR2BVer(context.getVersion()) || VersionUtilities.isR2Ver(context.getVersion()))) {
236            if (!e.getValue().getValue().isJsonArray()) {
237              logError("2022-12-17", line(e.getValue().getValue()), col(e.getValue().getValue()), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.ILLEGAL_COMMENT_TYPE, e.getValue().getValue().type().toName()), IssueSeverity.ERROR);
238            } else {
239              for (JsonElement c : e.getValue().getValue().asJsonArray()) {
240                if (!c.isJsonString()) {
241                  logError("2022-12-17", line(e.getValue().getValue()), col(e.getValue().getValue()), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.ILLEGAL_COMMENT_TYPE, c.type().toName()), IssueSeverity.ERROR);
242                } else {
243                  element.getComments().add(c.asString());
244                }
245              }
246            }
247          } else {
248            logError(ValidationMessage.NO_RULE_DATE, line(e.getValue().getValue()), col(e.getValue().getValue()), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_, e.getKey()), IssueSeverity.ERROR);
249          }
250        }
251      }
252    }
253    if (object.isExtraComma()) {
254      logError("2022-11-26", object.getEnd().getLine(), object.getEnd().getCol(), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Object"), IssueSeverity.ERROR);
255    }
256
257  }
258
259  public void parseChildItem(String path, Map<String, JsonProperty> children, Element context, Set<String> processed, Property property) {
260    if (property.isChoice() || property.getDefinition().getPath().endsWith("data[x]")) {
261      if (property.isJsonPrimitiveChoice()) {
262        if (children.containsKey(property.getJsonName())) {
263          JsonElement je = children.get(property.getJsonName()).getValue();
264          if (processed != null) processed.add(property.getJsonName());
265          String type = getTypeFromJsonType(je);
266          if (type == null) {
267            logError(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);
268          } else if (property.hasType(type)) {
269            Property np = new Property(property.getContext(), property.getDefinition(), property.getStructure(), property.getUtils(), type);
270            parseChildPrimitive(children, context, processed, np, path, property.getName(), false);
271          } else {
272            logError(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);
273          }
274        }
275      } else {
276        for (TypeRefComponent type : property.getDefinition().getType()) {
277          String eName = property.getJsonName().substring(0, property.getName().length()-3) + Utilities.capitalize(type.getWorkingCode());
278          if (!isPrimitive(type.getWorkingCode()) && children.containsKey(eName)) {
279            parseChildComplex(path, children, context, processed, property, eName, false);
280            break;
281          } else if (isPrimitive(type.getWorkingCode()) && (children.containsKey(eName) || children.containsKey("_"+eName))) {
282            parseChildPrimitive(children, context, processed, property, path, eName, false);
283            break;
284          }
285        }
286      }
287    } else if (property.isPrimitive(property.getType(null))) {
288      parseChildPrimitive(children, context, processed, property, path, property.getJsonName(), property.hasJsonName());
289    } else if (children.containsKey(property.getJsonName())) {
290      parseChildComplex(path, children, context, processed, property, property.getJsonName(), property.hasJsonName());
291    }
292  }
293
294  private String getTypeFromJsonType(JsonElement je) {
295    if (je.isJsonPrimitive()) {
296      JsonPrimitive p = je.asJsonPrimitive();
297      if (p.isJsonString()) {
298        return "string";
299      } else if (p.isJsonBoolean()) {
300        return "boolean";
301      } else {
302        String s = p.asString();
303        if (Utilities.isInteger(s)) {
304          return "integer";
305        } else {
306          return "decimal";
307        }
308      }
309    } else {
310      return null;
311    }
312  }
313
314  private void parseChildComplex(String path, Map<String, JsonProperty> children, Element element, Set<String> processed, Property property, String name, boolean isJsonName) throws FHIRException {
315    if (processed != null) {
316      processed.add(name);
317    }
318    String npath = path+"."+property.getName();
319    String fpath = element.getPath()+"."+property.getName();
320    JsonProperty p = children.get(name);
321    JsonElement e = p == null ? null : p.getValue();
322    if (property.isList() && !property.isJsonKeyArray() && (e instanceof JsonArray)) {
323      JsonArray arr = (JsonArray) e;
324      if (arr.isExtraComma()) {
325        logError("2022-11-26", arr.getEnd().getLine(), arr.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Array"), IssueSeverity.ERROR);
326      }
327      if (arr.size() == 0) {
328        if (property.canBeEmpty()) {
329          // nothing
330        } else {
331          logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ARRAY_CANNOT_BE_EMPTY), IssueSeverity.ERROR);
332        }
333      }
334      int c = 0;
335      for (JsonElement am : arr) {
336        parseChildComplexInstance(npath+"["+c+"]", fpath+"["+c+"]", element, property, name, am, c == 0 ? arr : null, path);
337        c++;
338      }
339    } else if (property.isJsonKeyArray()) {
340      String code = property.getJsonKeyProperty();
341      List<Property> properties = property.getChildProperties(element.getName(), null);
342      if (properties.size() != 2) {
343        logError(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);               
344      } else {
345        Property propK = properties.get(0);
346        Property propV = properties.get(1);
347        if (!propK.getName().equals(code)) {
348          logError(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);                       
349        } else if (!propK.isPrimitive())  {
350          logError(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);                       
351        } else if (propV.isList())  {
352          logError(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);                       
353        } else if (propV.isChoice() && propV.getName().endsWith("[x]"))  {
354          logError(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);                       
355        } else if (!(e instanceof JsonObject)) {
356          logError(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);                       
357        } else {
358          JsonObject o = (JsonObject) e;
359          if (o.isExtraComma()) {
360            logError("2022-11-26", o.getEnd().getLine(), o.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Object"), IssueSeverity.ERROR);
361          }
362
363          int i = 0;
364          Set<String> names = new HashSet<>();
365          for (JsonProperty pv : o.getProperties()) {
366            if (names.contains(pv.getName())) {
367              logError("2022-11-26", line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.DUPLICATE_JSON_PROPERTY_KEY, pv.getName()), IssueSeverity.ERROR);                                     
368            } else {
369              names.add(pv.getName());
370            }
371            // create an array entry
372            String npathArr = path+"."+property.getName()+"["+i+"]";
373            String fpathArr = element.getPath()+"."+property.getName()+"["+i+"]";
374            
375            Element n = new Element(name, property).markLocation(line(pv.getValue()), col(pv.getValue()));
376            n.setPath(fpath);
377            element.getChildren().add(n);
378            // handle the key
379            String fpathKey = fpathArr+"."+propK.getName();
380            Element nKey = new Element(code, propK).markLocation(line(pv.getValue()), col(pv.getValue()));
381            checkComments(pv.getValue(), n, fpathArr);
382            nKey.setPath(fpathKey);
383            n.getChildren().add(nKey);
384            nKey.setValue(pv.getName());
385            
386
387            boolean ok = true;
388            Property pvl = propV;
389            if (propV.isJsonPrimitiveChoice()) {
390              ok = false;
391              String type = getTypeFromJsonType(pv.getValue());
392              if (type == null) {
393                logError(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);
394              } else if (propV.hasType(type)) {
395                pvl = new Property(propV.getContext(), propV.getDefinition(), propV.getStructure(), propV.getUtils(), type);
396                ok = true;
397              } else {
398                logError(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);
399              }
400            }
401            if (ok) {
402              // handle the value
403              String npathV = npathArr+"."+pvl.getName();
404              String fpathV = fpathArr+"."+pvl.getName();
405              if (propV.isPrimitive(pvl.getType(null))) {
406                parseChildPrimitiveInstance(n, pvl, pvl.getName(), false, npathV, fpathV, pv.getValue(), null);
407              } else if (pv.getValue() instanceof JsonObject || pv.getValue() instanceof JsonNull) {
408                parseChildComplexInstance(npathV, fpathV, n, pvl, pvl.getName(), pv.getValue(), null, null);
409              } else {
410                logError(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);                       
411              }
412            }
413            i++;
414          }
415        }
416      }
417    } else {
418      if (property.isList()) {
419        logError(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);
420      }
421      parseChildComplexInstance(npath, fpath, element, property, name, e, null, null);
422    }
423  }
424
425  private Object propNames(List<Property> properties) {
426    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
427    for (Property p: properties) {
428      b.append(p.getName());
429    }
430    return b.toString();
431  }
432
433  private void parseChildComplexInstance(String npath, String fpath, Element element, Property property, String name, JsonElement e, JsonElement commentContext, String commentPath) throws FHIRException {
434    if (property.hasTypeSpecifier()) {
435      FHIRPathEngine fpe = new FHIRPathEngine(context);
436      String type = null;
437      String cond = null;
438      for (StringPair sp : property.getTypeSpecifiers()) {
439        if (fpe.evaluateToBoolean(null, baseElement, baseElement, element, fpe.parse(sp.getName()))) {
440          type = sp.getValue();
441          cond = sp.getName();
442          break;
443        }
444      }
445      if (type != null) {
446        StructureDefinition sd = context.fetchResource(StructureDefinition.class, type);
447        if (sd == null) {
448          logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_ILLEGAL_TYPE, type, cond), IssueSeverity.ERROR);
449        } else {
450          if (sd.getAbstract()) {
451            logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_ABSTRACT_TYPE, type, cond), IssueSeverity.ERROR);
452          }
453          property = property.cloneToType(sd);
454        }
455      } else {
456        StructureDefinition sd = context.fetchTypeDefinition(property.getType());
457        if (sd == null) {
458          logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_NM_ILLEGAL_TYPE, property.getType()), IssueSeverity.ERROR);
459        } else if (sd.getAbstract()) {
460          logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_NM_ABSTRACT_TYPE, property.getType()), IssueSeverity.ERROR);
461        }        
462      }
463    }
464    if (e instanceof JsonObject) {
465      JsonObject child = (JsonObject) e;
466      Element n = new Element(name, property).markLocation(line(child), col(child));
467      n.setPath(fpath);
468      checkComments(commentContext, n, commentPath);        
469      checkObject(child, n, npath);
470      element.getChildren().add(n);
471      if (property.isResource()) {
472        parseResource(npath, child, n, property);
473      } else {
474        parseChildren(npath, child, n, false);
475      }
476    } else if (property.isNullable() && e instanceof JsonNull) {
477      // we create an element marked as a null element so we know something was present
478      JsonNull child = (JsonNull) e;
479      Element n = new Element(name, property).markLocation(line(child), col(child));
480      checkComments(commentContext, n, commentPath);        
481      checkComments(child, n, fpath);
482      n.setPath(fpath);
483      element.getChildren().add(n);
484      n.setNull(true);
485      // nothing to do, it's ok, but we treat it like it doesn't exist
486    } else {
487      logError(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);
488    }
489  }
490
491  private String describe(JsonElement e) {
492    if (e instanceof JsonArray) {
493      return "an Array";
494    }
495    if (e instanceof JsonObject) {
496      return "an Object";
497    }
498    if (e instanceof JsonNull) {
499      return "a Null";
500    }
501    if (e instanceof JsonPrimitive) {
502      return "a Primitive property";
503    }
504    return null;
505  }
506
507  private String describeType(JsonElement e) {
508    return e.type().toName();
509  }
510
511  private void parseChildPrimitive(Map<String, JsonProperty> children, Element element, Set<String> processed, Property property, String path, String name, boolean isJsonName) throws FHIRException {
512    String npath = path+"."+property.getName();
513    String fpath = element.getPath()+"."+property.getName();
514    processed.add(name);
515    processed.add("_"+name);
516    JsonProperty main = children.containsKey(name) ? children.get(name) : null;
517    JsonProperty fork = children.containsKey("_"+name) ? children.get("_"+name) : null;
518    if (main != null && main.getValue().isJsonString() && main.isUnquotedValue()) {
519      logError("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);
520    }
521    if (main != null || fork != null) {
522      if (property.isList()) {
523        boolean ok = true;
524        if (!(main == null || main.getValue() instanceof JsonArray)) {
525          logError(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);
526          ok = false;
527        }
528        if (!(fork == null || fork.getValue() instanceof JsonArray)) {
529          logError(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);
530          ok = false;
531        }
532        if (ok) {
533          JsonArray arr1 = (JsonArray) (main == null ? null : main.getValue());
534          JsonArray arr2 = (JsonArray) (fork == null ? null : fork.getValue());
535          if (arr1 != null && arr1.isExtraComma()) {
536            logError("2022-11-26", arr1.getEnd().getLine(), arr1.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Array"), IssueSeverity.ERROR);
537          }
538          if (arr2 != null && arr2.isExtraComma()) {
539            logError("2022-11-26", arr2.getEnd().getLine(), arr2.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Array"), IssueSeverity.ERROR);
540          }
541
542          for (int i = 0; i < Math.max(arrC(arr1), arrC(arr2)); i++) {
543            JsonElement m = arrI(arr1, i);
544            JsonElement f = arrI(arr2, i);
545            if (m != null && m.isJsonString() && arr1.isUnquoted(i)) {
546              logError("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);
547            }
548            parseChildPrimitiveInstance(element, property, name, isJsonName, npath, fpath, m, f);
549          }
550        }
551      } else {
552        parseChildPrimitiveInstance(element, property, name, isJsonName, npath, fpath, main == null ? null : main.getValue(), fork == null ? null : fork.getValue());
553      }
554    }
555  }
556
557  private JsonElement arrI(JsonArray arr, int i) {
558    return arr == null || i >= arr.size() || arr.get(i) instanceof JsonNull ? null : arr.get(i);
559  }
560
561  private int arrC(JsonArray arr) {
562    return arr == null ? 0 : arr.size();
563  }
564
565  private void parseChildPrimitiveInstance(Element element, Property property, String name, boolean isJsonName, String npath, String fpath, JsonElement main, JsonElement fork) throws FHIRException {
566    if (main != null && !(main.isJsonBoolean() || main.isJsonNumber() || main.isJsonString())) {
567      logError(ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(
568          I18nConstants.THIS_PROPERTY_MUST_BE_AN_SIMPLE_VALUE_NOT_, describe(main), name, npath), IssueSeverity.ERROR);
569    } else if (fork != null && !(fork instanceof JsonObject)) {
570      logError(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);
571    } else {
572      Element n = new Element(isJsonName ? property.getName() : name, property).markLocation(line(main != null ? main : fork), col(main != null ? main : fork));
573      if (main != null) {
574        checkComments(main, n, npath);
575      }
576      if (fork != null) {
577        checkComments(fork, n, npath);
578      }
579      n.setPath(fpath);
580      element.getChildren().add(n);
581      if (main != null) {
582        JsonPrimitive p = (JsonPrimitive) main;
583        n.setValue(property.hasImpliedPrefix() ? property.getImpliedPrefix()+p.asString() : p.asString());
584        if (!n.getProperty().isChoice() && n.getType().equals("xhtml")) {
585          try {
586            XhtmlParser xhtml = new XhtmlParser();
587            n.setXhtml(xhtml.setXmlMode(true).parse(n.getValue(), null).getDocumentElement());
588            if (policy == ValidationPolicy.EVERYTHING) {
589              for (StringPair s : xhtml.getValidationIssues()) {
590                logError("2022-11-17", line(main), col(main), npath, IssueType.INVALID, context.formatMessage(s.getName(), s.getValue()), IssueSeverity.ERROR);                
591              }
592            }
593          } catch (Exception e) {
594            logError(ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_XHTML_, e.getMessage()), IssueSeverity.ERROR);
595          }
596        }
597        if (policy == ValidationPolicy.EVERYTHING) {
598          // now we cross-check the primitive format against the stated type
599          if (Utilities.existsInList(n.getType(), "boolean")) {
600            if (!p.isJsonBoolean()) {
601              logError(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);
602            }
603          } else if (Utilities.existsInList(n.getType(), "integer", "unsignedInt", "positiveInt", "decimal")) {
604            if (!p.isJsonNumber())
605              logError(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);
606          } else if (!p.isJsonString()) {
607            logError(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);
608          }
609        }
610      }
611      if (fork != null) {
612        JsonObject child = (JsonObject) fork;
613        checkObject(child, n, npath);
614        parseChildren(npath, child, n, false);
615      }
616    }
617  }
618
619
620  private void parseResource(String npath, JsonObject res, Element parent, Property elementProperty) throws FHIRException {
621    JsonElement rt = res.get("resourceType");
622    if (rt == null) {
623      logError(ValidationMessage.NO_RULE_DATE, line(res), col(res), npath, IssueType.INVALID, context.formatMessage(I18nConstants.UNABLE_TO_FIND_RESOURCETYPE_PROPERTY), IssueSeverity.FATAL);
624    } else if (!rt.isJsonString()) {
625      logError("2022-11-26", line(res), col(res), npath, IssueType.INVALID, context.formatMessage(I18nConstants.RESOURCETYPE_PROPERTY_WRONG_TYPE, rt.type().toName()), IssueSeverity.FATAL);
626    } else {
627      String name = rt.asString();
628      StructureDefinition sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(name, null));
629      if (sd == null) {
630        logError(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);                            
631      } else {
632        parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd, this.profileUtilities), SpecialElement.fromProperty(parent.getProperty()), elementProperty);
633        parent.setType(name);
634        parseChildren(npath, res, parent, true);
635      }
636    }
637    if (res.isExtraComma()) {
638      logError("2022-11-26", res.getEnd().getLine(), res.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Object"), IssueSeverity.ERROR);
639    }
640
641  }
642
643  private void reapComments(JsonObject object, Element context) {
644    // todo
645  }
646
647  private int line(JsonElement e) {
648    return e.getStart().getLine();
649  }
650
651  private int col(JsonElement e) {
652    return e.getEnd().getCol();
653  }
654
655
656  protected void prop(String name, String value, String link) throws IOException {
657    json.link(link);
658    if (name != null)
659      json.name(name);
660    json.value(value);
661  }
662
663  protected void open(String name, String link) throws IOException {
664    json.link(link);
665    if (name != null)
666      json.name(name);
667    json.beginObject();
668  }
669
670  protected void close() throws IOException {
671    json.endObject();
672  }
673
674  protected void openArray(String name, String link) throws IOException {
675    json.link(link);
676    if (name != null)
677      json.name(name);
678    json.beginArray();
679  }
680
681  protected void closeArray() throws IOException {
682    json.endArray();
683  }
684
685
686  @Override
687  public void compose(Element e, OutputStream stream, OutputStyle style, String identity) throws FHIRException, IOException {
688    if (e.getPath() == null) {
689      e.populatePaths(null);
690    }
691
692    OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8");
693    if (style == OutputStyle.CANONICAL) {
694      json = new JsonCreatorCanonical(osw);
695    } else if (style == OutputStyle.PRETTY) {
696      json = new JsonCreatorDirect(osw, true, allowComments);
697    } else {
698      json = new JsonCreatorDirect(osw, false, allowComments);
699    }
700    checkComposeComments(e);
701    json.beginObject();
702    prop("resourceType", e.getType(), null);
703    Set<String> done = new HashSet<String>();
704    for (Element child : e.getChildren()) {
705      compose(e.getName(), e, done, child);
706    }
707    json.endObject();
708    json.finish();
709    osw.flush();
710  }
711
712  private void checkComposeComments(Element e) {
713    for (String s : e.getComments()) {
714      json.comment(s);
715    }
716  }
717
718  public void compose(Element e, JsonCreator json) throws Exception {
719    if (e.getPath() == null) {
720      e.populatePaths(null);
721    }
722    
723    this.json = json;
724    checkComposeComments(e);
725    json.beginObject();
726
727    prop("resourceType", e.getType(), linkResolver == null ? null : linkResolver.resolveProperty(e.getProperty()));
728    Set<String> done = new HashSet<String>();
729    for (Element child : e.getChildren()) {
730      compose(e.getName(), e, done, child);
731    }
732    json.endObject();
733    json.finish();
734  }
735
736  private void compose(String path, Element e, Set<String> done, Element child) throws IOException {
737    checkComposeComments(child);
738    if (wantCompose(path, child)) {
739      boolean isList = child.hasElementProperty() ? child.getElementProperty().isList() : child.getProperty().isList();
740      if (!isList) {// for specials, ignore the cardinality of the stated type
741        compose(path, child);
742      } else if (!done.contains(child.getName())) {
743        done.add(child.getName());
744        List<Element> list = e.getChildrenByName(child.getName());
745        composeList(path, list);
746      }
747    }
748  }
749
750
751  private void composeList(String path, List<Element> list) throws IOException {
752    // there will be at least one element
753    String name = list.get(0).getName();
754    boolean complex = true;
755    if (list.get(0).isPrimitive()) {
756      boolean prim = false;
757      complex = false;
758      for (Element item : list) {
759        if (item.hasValue())
760          prim = true;
761        if (item.hasChildren())
762          complex = true;
763      }
764      if (prim) {
765        openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty()));
766        for (Element item : list) {
767          if (item.hasValue())
768            primitiveValue(null, item);
769          else
770            json.nullValue();
771        }
772        closeArray();
773      }
774      name = "_"+name;
775    }
776    if (complex) {
777      openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty()));
778      for (Element item : list) {
779        if (item.hasChildren()) {
780          open(null,null);
781          if (item.getProperty().isResource()) {
782            prop("resourceType", item.getType(), linkResolver == null ? null : linkResolver.resolveType(item.getType()));
783          }
784          Set<String> done = new HashSet<String>();
785          for (Element child : item.getChildren()) {
786            compose(path+"."+name+"[]", item, done, child);
787          }
788          close();
789        } else
790          json.nullValue();
791      }
792      closeArray();
793    }
794  }
795
796  private void primitiveValue(String name, Element item) throws IOException {
797    if (name != null) {
798      if (linkResolver != null)
799        json.link(linkResolver.resolveProperty(item.getProperty()));
800      json.name(name);
801    }
802    String type = item.getType();
803    if (Utilities.existsInList(type, "boolean"))
804      json.value(item.getValue().trim().equals("true") ? new Boolean(true) : new Boolean(false));
805    else if (Utilities.existsInList(type, "integer", "unsignedInt", "positiveInt"))
806      json.value(new Integer(item.getValue()));
807    else if (Utilities.existsInList(type, "decimal"))
808      try {
809        json.value(new BigDecimal(item.getValue()));
810      } catch (Exception e) {
811        throw new NumberFormatException(context.formatMessage(I18nConstants.ERROR_WRITING_NUMBER__TO_JSON, item.getValue()));
812      }
813    else
814      json.value(item.getValue());
815  }
816
817  private void compose(String path, Element element) throws IOException {
818    String name = element.getName();
819    if (element.isPrimitive() || isPrimitive(element.getType())) {
820      if (element.hasValue())
821        primitiveValue(name, element);
822      name = "_"+name;
823      if (element.getType().equals("xhtml"))
824        json.anchor("end-xhtml");
825    }
826    if (element.hasChildren()) {
827      open(name, linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty()));
828      if (element.getProperty().isResource()) {
829        prop("resourceType", element.getType(), linkResolver == null ? null : linkResolver.resolveType(element.getType()));
830      }
831      Set<String> done = new HashSet<String>();
832      for (Element child : element.getChildren()) {
833        compose(path+"."+element.getName(), element, done, child);
834      }
835      close();
836    }
837  }
838
839  public boolean isAllowComments() {
840    return allowComments;
841  }
842
843  public JsonParser setAllowComments(boolean allowComments) {
844    this.allowComments = allowComments;
845    return this;
846  }
847
848
849}