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