001package org.hl7.fhir.validation.instance.type;
002
003import java.io.ByteArrayOutputStream;
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.Comparator;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Set;
010
011import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50;
012import org.hl7.fhir.convertors.factory.VersionConvertorFactory_14_50;
013import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50;
014import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
015import org.hl7.fhir.exceptions.FHIRException;
016import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
017import org.hl7.fhir.r5.context.IWorkerContext;
018import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
019import org.hl7.fhir.r5.elementmodel.Element;
020import org.hl7.fhir.r5.elementmodel.Manager;
021import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
022import org.hl7.fhir.r5.formats.IParser.OutputStyle;
023import org.hl7.fhir.r5.model.Coding;
024import org.hl7.fhir.r5.model.ElementDefinition;
025import org.hl7.fhir.r5.model.ExpressionNode;
026import org.hl7.fhir.r5.model.Extension;
027import org.hl7.fhir.r5.model.Resource;
028import org.hl7.fhir.r5.model.StructureDefinition;
029import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
030import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
031import org.hl7.fhir.r5.model.ValueSet;
032import org.hl7.fhir.r5.utils.FHIRPathEngine;
033import org.hl7.fhir.r5.utils.ToolingExtensions;
034import org.hl7.fhir.r5.utils.XVerExtensionManager;
035import org.hl7.fhir.utilities.Utilities;
036import org.hl7.fhir.utilities.VersionUtilities;
037import org.hl7.fhir.utilities.i18n.I18nConstants;
038import org.hl7.fhir.utilities.validation.ValidationMessage;
039import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
040import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
041import org.hl7.fhir.utilities.validation.ValidationOptions;
042import org.hl7.fhir.validation.BaseValidator;
043import org.hl7.fhir.validation.TimeTracker;
044import org.hl7.fhir.validation.instance.utils.NodeStack;
045
046public class StructureDefinitionValidator extends BaseValidator {
047
048  public class FhirPathSorter implements Comparator<ExpressionNode> {
049
050    @Override
051    public int compare(ExpressionNode arg0, ExpressionNode arg1) {
052      return arg0.toString().compareTo(arg1.toString());
053    }
054
055  }
056
057  private FHIRPathEngine fpe;
058  private boolean wantCheckSnapshotUnchanged;
059
060  public StructureDefinitionValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe, boolean wantCheckSnapshotUnchanged, XVerExtensionManager xverManager, Coding jurisdiction) {
061    super(context, xverManager);
062    source = Source.InstanceValidator;
063    this.fpe = fpe;
064    this.timeTracker = timeTracker;
065    this.wantCheckSnapshotUnchanged = wantCheckSnapshotUnchanged;
066    this.jurisdiction = jurisdiction;
067  }
068  
069  public boolean validateStructureDefinition(List<ValidationMessage> errors, Element src, NodeStack stack)  {
070    boolean ok = true;
071    StructureDefinition sd = null;
072    String typeName = null;
073    try {
074      sd = loadAsSD(src);
075      List<ElementDefinition> snapshot = sd.getSnapshot().getElement();
076      sd.setSnapshot(null);
077      typeName = sd.getTypeName();
078      StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
079      if (warning(errors, NO_RULE_DATE, IssueType.NOTFOUND, stack.getLiteralPath(), base != null, I18nConstants.UNABLE_TO_FIND_BASE__FOR_, sd.getBaseDefinition(), "StructureDefinition, so can't check the differential")) {
080        if (rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, stack.getLiteralPath(), sd.hasDerivation(), I18nConstants.SD_MUST_HAVE_DERIVATION, sd.getUrl())) {
081          boolean bok = base.getAbstract() || sd.hasKind() && sd.getKind() == base.getKind();
082          rule(errors, "2022-11-02", IssueType.NOTFOUND, stack.getLiteralPath(), bok, I18nConstants.SD_CONSTRAINED_KIND_NO_MATCH, sd.getKind().toCode(), base.getKind().toCode(), base.getType(), base.getUrl());
083          if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) {
084            rule(errors, "2022-11-02", IssueType.NOTFOUND, stack.getLiteralPath(), sd.hasType() && sd.getType().equals(base.getType()), I18nConstants.SD_CONSTRAINED_TYPE_NO_MATCH, sd.getType(), base.getType());
085            List<ValidationMessage> msgs = new ArrayList<>();
086            ProfileUtilities pu = new ProfileUtilities(context, msgs, null);
087            pu.setXver(xverManager);
088            pu.setNewSlicingProcessing(!sd.hasFhirVersion() || VersionUtilities.isR4Plus(sd.getFhirVersion().toCode()));
089            pu.generateSnapshot(base, sd, sd.getUrl(), "http://hl7.org/fhir/R4/", sd.getName());
090            if (msgs.size() > 0) {
091              for (ValidationMessage msg : msgs) {
092                // we need to set the location for the context 
093                String loc = msg.getLocation();
094                if (loc.contains("#")) {
095                  msg.setLocation(stack.getLiteralPath()+".differential.element.where(path = '"+loc.substring(loc.indexOf("#")+1)+"')");
096                } else {
097                  msg.setLocation(stack.getLiteralPath());
098                }
099                errors.add(msg);
100                ok = false;
101              }
102            }
103            if (!snapshot.isEmpty() && wantCheckSnapshotUnchanged) {
104              int was = snapshot.size();
105              int is = sd.getSnapshot().getElement().size();
106              ok = rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, stack.getLiteralPath(), was == is, I18nConstants.SNAPSHOT_EXISTING_PROBLEM, was, is) && ok;
107            }
108          } else {
109            rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, stack.getLiteralPath(), sd.hasType() && !sd.getType().equals(base.getType()), I18nConstants.SD_SPECIALIZED_TYPE_MATCHES, sd.getType(), base.getType());
110          }
111        } else {
112          ok = false;
113        }
114        if ("constraint".equals(src.getChildValue("derivation"))) {
115          ok = rule(errors, "2022-11-02", IssueType.NOTFOUND, stack.getLiteralPath(), base.getKindElement().primitiveValue().equals(src.getChildValue("kind")), 
116              I18nConstants.SD_DERIVATION_KIND_MISMATCH, base.getKindElement().primitiveValue(), src.getChildValue("kind")) && ok;
117        }
118      }
119    } catch (FHIRException | IOException e) {
120      rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.ERROR_GENERATING_SNAPSHOT, e.getMessage());
121      ok = false;
122    }
123    List<Element> differentials = src.getChildrenByName("differential");
124    List<Element> snapshots = src.getChildrenByName("snapshot");
125    for (Element differential : differentials) {
126      ok = validateElementList(errors, differential, stack.push(differential, -1, null, null), false, snapshots.size() > 0, sd, typeName) && ok;
127    }
128    for (Element snapshot : snapshots) {
129      ok = validateElementList(errors, snapshot, stack.push(snapshot, -1, null, null), true, true, sd, typeName) && ok;
130    }
131    return ok;
132  }
133  
134  private boolean validateElementList(List<ValidationMessage> errors, Element elementList, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd, String typeName) {
135    boolean ok = true;
136    List<Element> elements = elementList.getChildrenByName("element");
137    int cc = 0;
138    for (Element element : elements) {
139      ok = validateElementDefinition(errors, element, stack.push(element, cc, null, null), snapshot, hasSnapshot, sd, typeName) && ok;
140      cc++;
141    }    
142    return ok;
143  }
144
145  private boolean validateElementDefinition(List<ValidationMessage> errors, Element element, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd, String typeName) {
146    boolean ok = true;
147    boolean typeMustSupport = false;
148    String path = element.getNamedChildValue("path");
149    rule(errors, "2022-11-02", IssueType.NOTFOUND, stack.getLiteralPath(), typeName == null || path == null || path.equals(typeName) || path.startsWith(typeName+"."), I18nConstants.SD_PATH_TYPE_MISMATCH, typeName, path);
150    if (!snapshot) {
151      rule(errors, "2023-01-17", IssueType.INVALID, stack.getLiteralPath(), path.contains(".") || !element.hasChild("slicing"), I18nConstants.SD_NO_SLICING_ON_ROOT, path);
152      
153    }
154    List<Element> types = element.getChildrenByName("type");
155    Set<String> typeCodes = new HashSet<>();
156    Set<String> characteristics = new HashSet<>();
157    
158    for (Element type : types) {
159      if (hasMustSupportExtension(type)) {
160        typeMustSupport = true;
161      }
162      String tc = type.getChildValue("code");
163      if (type.hasExtension(ToolingExtensions.EXT_FHIR_TYPE)) {
164        tc = type.getExtensionValue(ToolingExtensions.EXT_FHIR_TYPE).primitiveValue();
165      }
166      if (Utilities.noString(tc) && type.hasChild("code")) {
167        if (VersionUtilities.isR4Plus(context.getVersion())) {
168          throw new Error("Snapshot for " + sd.getId() +" element " + path + " has type.code without a value ");          
169        }
170      }
171      if (!Utilities.noString(tc)) {
172        typeCodes.add(tc);
173        Set<String> tcharacteristics = new HashSet<>();
174        StructureDefinition tsd = context.fetchTypeDefinition(tc);
175        if (tsd != null && tsd.hasExtension(ToolingExtensions.EXT_TYPE_CHARACTERISTICS)) {
176          for (Extension ext : tsd.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_CHARACTERISTICS)) {
177            tcharacteristics.add(ext.getValue().primitiveValue());
178          }
179        } else {
180          // nothing specified, so infer from known types
181          addCharacteristics(tcharacteristics, tc);
182        }
183        characteristics.addAll(tcharacteristics);
184        if (type.hasChildren("targetProfile")) {
185          ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), tcharacteristics.contains("has-target") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "targetProfile", tc) && ok;
186        }
187        // check the stated profile - must be a constraint on the type 
188        if (snapshot || sd != null) {
189          ok = validateElementType(errors, type, stack.push(type, -1, null, null), sd, path) && ok;
190        }
191      }
192    }
193    if (typeMustSupport) {
194      if (snapshot) {
195        ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_SNAPSHOT, path) && ok;
196      } else {
197        hint(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), hasSnapshot || "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_DIFF, path);        
198      }
199    }
200    if (element.hasChild("binding")) {
201      if (!typeCodes.isEmpty()) {
202        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("can-bind") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "Binding", typeCodes) && ok;
203      }
204      Element binding = element.getNamedChild("binding");
205      ok = validateBinding(errors, binding, stack.push(binding, -1, null, null), typeCodes, snapshot, path) && ok;
206    } else {
207      // this is a good idea but there's plenty of cases where the rule isn't met; maybe one day it's worth investing the time to exclude these cases and bring this rule back
208//      String bt = boundType(typeCodes);
209//      hint(errors, UNKNOWN_DATE_TIME, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || bt == null, I18nConstants.SD_ED_SHOULD_BIND, element.getNamedChildValue("path"), bt);              
210    }
211    if (!typeCodes.isEmpty()) {
212      if (element.hasChild("maxLength")) {
213        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-length") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "MaxLength", typeCodes) && ok;      
214      }
215      if (element.hasExtension(ToolingExtensions.EXT_MIN_LENGTH)) {
216        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-length") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "MinLength Extension", typeCodes) && ok;      
217      }
218      if (element.hasChild("minValue")) {
219        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-range") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "MinValue", typeCodes) && ok;      
220      }
221      if (element.hasChild("maxValue")) {
222        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-range") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "MaxValue", typeCodes) && ok;      
223      }
224      if (element.hasExtension(ToolingExtensions.EXT_MAX_DECIMALS)) {
225        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("is-continuous") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "Max Decimal Places Extension", typeCodes) && ok;      
226      }
227      if (element.hasExtension(ToolingExtensions.EXT_MAX_SIZE)) {
228        ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-size") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "Max Size", typeCodes) && ok;      
229      }
230    }
231    // in a snapshot, we validate that fixedValue, pattern, and defaultValue, if present, are all of the right type
232    if (snapshot && (element.getIdBase() != null) && (element.getIdBase().contains("."))) {
233      if (rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), !typeCodes.isEmpty() || element.hasChild("contentReference"), I18nConstants.SD_NO_TYPES_OR_CONTENTREF, element.getIdBase())) {     
234        // if we see fixed[x] or pattern[x] applied to a repeating element, we'll give the user a hint
235        boolean repeating = !Utilities.existsInList(element.getChildValue("max"), "0", "1");
236        
237        Element v = element.getNamedChild("defaultValue");
238        if (v != null) {
239          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "defaultValue", v.fhirType(), typeCodes) && ok;
240        }
241        v = element.getNamedChild("fixed");
242        if (v != null) {
243          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "fixed", v.fhirType(), typeCodes) && ok;
244          hint(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), !repeating, I18nConstants.SD_VALUE_TYPE_REPEAT_HINT, element.getIdBase(), "fixed");
245          if (isPrimitiveType(v.fhirType())) {
246            warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), !repeating, I18nConstants.SD_VALUE_TYPE_REPEAT_WARNING_DOTNET, element.getIdBase(), "fixed");
247          } else {
248            warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), false, I18nConstants.SD_VALUE_COMPLEX_FIXED, v.fhirType());            
249          }
250        }
251        v = element.getNamedChild("pattern");
252        if (v != null) {
253          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "pattern", v.fhirType(), typeCodes) && ok;
254          hint(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), !repeating, I18nConstants.SD_VALUE_TYPE_REPEAT_HINT, element.getIdBase(), "pattern");
255          if (isPrimitiveType(v.fhirType())) {
256            warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), !repeating, I18nConstants.SD_VALUE_TYPE_REPEAT_WARNING_DOTNET, element.getIdBase(), "pattern");
257          }
258        }
259      }
260      // if we see fixed[x] or pattern[x] applied to a repeating element, we'll give the user a hint
261    }
262    return ok;
263  }
264  
265  private boolean addCharacteristics(Set<String> set, String tc) {
266    switch (tc) {
267    case "boolean" : return addCharacteristicsForType(set);
268    case "integer" : return addCharacteristicsForType(set, "has-range", "has-length");
269    case "integer64" : return addCharacteristicsForType(set, "has-range", "has-length");
270    case "decimal" :return  addCharacteristicsForType(set, "has-range", "is-continuous", "has-length");
271    case "base64Binary" : return addCharacteristicsForType(set, "has-size");
272    case "instant" : return addCharacteristicsForType(set, "has-range", "is-continuous", "has-length");
273    case "string" : return addCharacteristicsForType(set, "has-length", "do-translations", "can-bind");
274    case "uri" : return addCharacteristicsForType(set, "has-length", "can-bind");
275    case "date" :return  addCharacteristicsForType(set, "has-range", "has-length");
276    case "dateTime" : return addCharacteristicsForType(set, "has-range", "is-continuous", "has-length");
277    case "time" :return  addCharacteristicsForType(set, "has-range", "is-continuous", "has-length");
278    case "canonical" :return  addCharacteristicsForType(set, "has-target", "has-length");
279    case "code" :return  addCharacteristicsForType(set, "has-length", "can-bind");
280    case "id" :return  addCharacteristicsForType(set, "has-length");
281    case "markdown" :return  addCharacteristicsForType(set, "do-translations", "has-length");
282    case "oid" :return  addCharacteristicsForType(set, "has-length", "can-bind");
283    case "positiveInt" :return  addCharacteristicsForType(set, "has-range", "has-length");
284    case "unsignedInt" :return  addCharacteristicsForType(set, "has-range", "has-length");
285    case "url" :return  addCharacteristicsForType(set, "has-length", "can-bind");
286    case "uuid" :return  addCharacteristicsForType(set, "has-length", "can-bind");
287    case "xhtml" :return  addCharacteristicsForType(set);
288    case "Address" :return  addCharacteristicsForType(set, "do-translations");
289    case "Age" : return addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units");
290    case "Annotation" :return  addCharacteristicsForType(set);
291    case "Attachment" :return  addCharacteristicsForType(set, "has-size", "do-translations");
292    case "CodeableConcept" :return  addCharacteristicsForType(set, "can-bind", "do-translations");
293    case "CodeableReference" : return addCharacteristicsForType(set, "has-target", "can-bind", "do-translations");
294    case "Coding" : return addCharacteristicsForType(set, "can-bind", "do-translations");
295    case "ContactPoint" :return  addCharacteristicsForType(set);
296    case "Count" :return  addCharacteristicsForType(set, "has-range");
297    case "Distance" :return  addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units");
298    case "Duration" : return addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units");
299    case "HumanName" :return  addCharacteristicsForType(set);
300    case "Identifier" : return addCharacteristicsForType(set);
301    case "Money" : return addCharacteristicsForType(set, "has-range", "is-continuous");
302    case "Period" : return addCharacteristicsForType(set);
303    case "Quantity" :return  addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units");
304    case "Range" :return  addCharacteristicsForType(set, "has-units", "can-bind", "has-units");
305    case "Ratio" :return  addCharacteristicsForType(set, "has-units");
306    case "RatioRange" : return addCharacteristicsForType(set, "has-units");
307    case "Reference" : return addCharacteristicsForType(set, "has-target");
308    case "SampledData" :return  addCharacteristicsForType(set);
309    case "Signature" : return addCharacteristicsForType(set);
310    case "Timing" : return addCharacteristicsForType(set);
311    case "ContactDetail" :return  addCharacteristicsForType(set);
312    case "Contributor" :return  addCharacteristicsForType(set);
313    case "DataRequirement" :return  addCharacteristicsForType(set);
314    case "Expression" : return addCharacteristicsForType(set);
315    case "ParameterDefinition" : return addCharacteristicsForType(set);
316    case "RelatedArtifact" :return  addCharacteristicsForType(set);
317    case "TriggerDefinition" :return  addCharacteristicsForType(set);
318    case "UsageContext" :return  addCharacteristicsForType(set);
319    case "Dosage" : return addCharacteristicsForType(set);
320    case "Meta" :return  addCharacteristicsForType(set);
321    case "Resource" :return  addCharacteristicsForType(set);
322    case "Extension" :return  addCharacteristicsForType(set, "can-bind");
323    case "Narrative" :return  addCharacteristicsForType(set);
324    case "MoneyQuantity" :return  addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units");
325    case "SimpleQuantity" :return  addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units");
326    case "MarketingStatus" :return  addCharacteristicsForType(set);
327    case "ExtendedContactDetail" :return  addCharacteristicsForType(set);
328    case "VirtualServiceDetail" :return  addCharacteristicsForType(set);
329    case "Availability" :return  addCharacteristicsForType(set);
330    case "MonetaryComponent" :return  addCharacteristicsForType(set);
331    case "ElementDefinition" :return  addCharacteristicsForType(set);
332
333    case "BackboneElement" :return  addCharacteristicsForType(set);
334    case "Element" :return  addCharacteristicsForType(set);
335    case "Base" :return  addCharacteristicsForType(set);
336    default:
337//      if (!context.getResourceNames().contains(tc)) {
338//        System.out.println("Unhandled data type in addCharacteristics: "+tc);        
339//      }
340      return addCharacteristicsForType(set);
341    }
342  }
343
344
345  private boolean addCharacteristicsForType(Set<String> set, String... cl) {
346    for (String c : cl) {
347      set.add(c);
348    }
349    return true;
350  }
351
352  private boolean isPrimitiveType(String fhirType) {
353    StructureDefinition sd = context.fetchTypeDefinition(fhirType);
354    return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
355  }
356
357  private String boundType(Set<String> typeCodes) {
358    for (String tc : typeCodes) {
359      if (Utilities.existsInList(tc, "code", "Coding", "CodeableConcept", "Quantity", "CodeableReference")) {
360        return tc;
361      }
362    }
363    return null;
364  }
365
366  private String bindableType(Set<String> typeCodes) {
367    String ret = boundType(typeCodes);
368    if (ret != null) {
369      return ret;
370    }
371    for (String tc : typeCodes) {
372      if (Utilities.existsInList(tc, "string", "uri", "CodeableConcept", "Quantity", "CodeableReference")) {
373        return tc;
374      }
375      StructureDefinition sd = context.fetchTypeDefinition(tc);
376      if (sd != null) {
377        if (sd.hasExtension(ToolingExtensions.EXT_BINDING_STYLE)) {
378          return tc;          
379        }
380        for (Extension ext : sd.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_CHARACTERISTICS)) {
381          if ("can-bind".equals(ext.getValue().primitiveValue())) {
382            return tc;
383          }
384        }
385      }
386    }
387    return null;
388  }
389
390  private boolean validateBinding(List<ValidationMessage> errors, Element binding, NodeStack stack, Set<String> typeCodes, boolean snapshot, String path) {
391    boolean ok = true;
392    if (bindableType(typeCodes) == null) {
393      ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot, I18nConstants.SD_ED_BIND_NO_BINDABLE, path, typeCodes.toString()) && ok;
394    } 
395    if (!snapshot) {
396      Set<String> bindables = getListofBindableTypes(typeCodes);    
397      hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), bindables.size() <= 1, I18nConstants.SD_ED_BIND_MULTIPLE_TYPES, path, typeCodes.toString());
398    }
399    
400    if (binding.hasChild("valueSet")) {
401      Element valueSet = binding.getNamedChild("valueSet");
402      String ref = valueSet.hasPrimitiveValue() ? valueSet.primitiveValue() : valueSet.getNamedChildValue("reference");
403      if (warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || ref != null, I18nConstants.SD_ED_SHOULD_BIND_WITH_VS, path)) {
404        Resource vs = context.fetchResource(Resource.class, ref);
405        
406        // just because we can't resolve it directly doesn't mean that terminology server can't. Check with it
407        
408        if (warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs != null || serverSupportsValueSet(ref), I18nConstants.SD_ED_BIND_UNKNOWN_VS, path, ref)) {
409          if (vs != null) {
410            ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs instanceof ValueSet, I18nConstants.SD_ED_BIND_NOT_VS, path, ref, vs.fhirType()) && ok;
411          }
412        }
413      }
414    } 
415    return ok;
416  }
417
418  private Set<String> getListofBindableTypes(Set<String> types) {
419    Set<String> res = new HashSet<>();
420    for (String s : types) {
421      if (Utilities.existsInList(s, "code", "string", "url", "uri", "Coding", "CodeableConcept", "Quantity", "CodeableReference")) {
422        res.add(s);
423      }
424    }
425    return res;
426  }
427
428  private boolean serverSupportsValueSet(String ref) {
429    ValidationResult vr = context.validateCode(new ValidationOptions().checkValueSetOnly().setVsAsUrl().noClient(), new Coding("http://loinc.org", "5792-7", null), new ValueSet().setUrl(ref));
430    return vr.getErrorClass() == null;
431  }
432
433  private boolean validateElementType(List<ValidationMessage> errors, Element type, NodeStack stack, StructureDefinition sd, String path) {
434    boolean ok = true;
435    String code = type.getNamedChildValue("code");
436    if (code == null && path != null) {
437      code = getTypeCodeFromSD(sd, path);
438    }
439    if (code != null) {
440      List<Element> profiles = type.getChildrenByName("profile");
441      if (VersionUtilities.isR2Ver(context.getVersion()) || VersionUtilities.isR2BVer(context.getVersion()) ) {
442        for (Element profile : profiles) {
443          ok = validateProfileTypeOrTarget(errors, profile, code, stack.push(profile, -1, null, null), path) && ok;
444        }
445        
446      } else {
447        for (Element profile : profiles) {
448          ok = validateTypeProfile(errors, profile, code, stack.push(profile, -1, null, null), path) && ok;
449        }
450        profiles = type.getChildrenByName("targetProfile");
451        for (Element profile : profiles) {
452          ok = validateTargetProfile(errors, profile, code, stack.push(profile, -1, null, null), path) && ok;
453        }
454      }
455    }
456    return true;
457  }
458
459  private boolean validateProfileTypeOrTarget(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) {
460    boolean ok = true;
461    String p = profile.primitiveValue();
462    StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
463    if (code.equals("Reference")) {
464      if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
465        StructureDefinition t = determineBaseType(sd);
466        if (t == null) {
467          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok;
468        } else {
469          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path) && ok;
470        }
471      }
472    } else {
473      if (sd == null ) {
474        sd = getXverExt(errors, stack.getLiteralPath(), profile, p);
475      }
476      if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
477        StructureDefinition t = determineBaseType(sd);
478        if (t == null) {
479          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok;
480        } else {
481          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), isInstanceOf(t, code), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path) && ok;
482          if (t.getType().equals("Extension")) {
483            boolean isModifierDefinition = checkIsModifierExtension(sd);
484            boolean isModifierContext = path.endsWith(".modifierExtension");
485            if (isModifierDefinition) {
486              ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), isModifierContext, I18nConstants.SD_ED_TYPE_PROFILE_NOT_MODIFIER, p, t, code, path) && ok;            
487            } else {
488              ok =rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), !isModifierContext, I18nConstants.SD_ED_TYPE_PROFILE_IS_MODIFIER, p, t, code, path) && ok;
489            }          
490          }
491        }
492      }      
493    }
494    return ok;
495  }
496
497  private String getTypeCodeFromSD(StructureDefinition sd, String path) {
498    ElementDefinition ed = null;
499    for (ElementDefinition t : sd.getSnapshot().getElement()) {
500      if (t.hasPath() && t.getPath().equals(path)) {
501        if (ed == null) {
502          ed = t;
503        } else {
504          return null; // more than one match, we don't know which is which
505        }
506      }
507    }
508    return ed != null && ed.getType().size() == 1 ? ed.getTypeFirstRep().getCode() : null;
509  }
510
511  private boolean validateTypeProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) {
512    boolean ok = true;
513    String p = profile.primitiveValue();
514    StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
515    if (sd == null ) {
516      sd = getXverExt(errors, stack.getLiteralPath(), profile, p);
517    }
518    if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
519      StructureDefinition t = determineBaseType(sd);
520      if (t == null) {
521        ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok;
522      } else if (!isInstanceOf(t, code)) {
523        ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path) && ok;
524      } else {
525        if (t.getType().equals("Extension")) {
526          boolean isModifierDefinition = checkIsModifierExtension(sd);
527          boolean isModifierContext = path.endsWith(".modifierExtension");
528          if (isModifierDefinition) {
529            ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), isModifierContext, I18nConstants.SD_ED_TYPE_PROFILE_NOT_MODIFIER, p, t, code, path) && ok;            
530          } else {
531            ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), !isModifierContext, I18nConstants.SD_ED_TYPE_PROFILE_IS_MODIFIER, p, t, code, path) && ok;
532          }          
533        }
534      }
535    }
536    return ok;
537  }
538
539  private boolean checkIsModifierExtension(StructureDefinition t) {
540    return t.getSnapshot().getElementFirstRep().getIsModifier();
541  }
542
543  private boolean validateTargetProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) {
544    boolean ok = true;
545    String p = profile.primitiveValue();
546    StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
547    if (code.equals("Reference") || code.equals("CodeableReference")) {
548      if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
549        StructureDefinition t = determineBaseType(sd);
550        if (t == null) {
551          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok;
552        } else {
553          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Resource") && ok;
554        }
555      }
556    } else if (code.equals("canonical")) {
557      if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
558        StructureDefinition t = determineBaseType(sd);
559        if (t == null) {
560          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok;
561        } else if (!VersionUtilities.isR5Ver(context.getVersion())) {
562          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(t.getType()) || "Resource".equals(t.getType()), I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Canonical Resource") && ok;
563        } else {
564          ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), "CanonicalResource".equals(t.getType()) || VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(t.getType()), I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Canonical Resource") && ok;
565        }  
566      }
567    } else {
568      ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_NO_TARGET_PROFILE, code) && ok;
569    }
570    return ok;
571  }
572
573  private boolean isInstanceOf(StructureDefinition sd, String code) {
574    while (sd != null) {
575      if (sd.getType().equals(code)) {
576        return true;
577      }
578      if (sd.getUrl().equals(code)) {
579        return true;
580      }
581      sd = sd.hasBaseDefinition() ? context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()) : null;
582      if (!(VersionUtilities.isR2Ver(context.getVersion()) || VersionUtilities.isR2BVer(context.getVersion())) && sd != null && !sd.getAbstract() && sd.getKind() != StructureDefinitionKind.LOGICAL) {
583        sd = null;
584      }
585    }
586    
587    return false;
588  }
589
590  private StructureDefinition determineBaseType(StructureDefinition sd) {
591    while (sd != null && sd.getDerivation() == TypeDerivationRule.CONSTRAINT) {
592      sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
593    }
594    return sd;
595  }
596
597  private boolean hasMustSupportExtension(Element type) {
598    if ("true".equals(getExtensionValue(type, ToolingExtensions.EXT_MUST_SUPPORT))) {
599      return true;
600    }
601    List<Element> profiles = type.getChildrenByName("profile");
602    for (Element profile : profiles) {
603      if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) {
604        return true;
605      }
606    }
607    profiles = type.getChildrenByName("targetProfile");
608    for (Element profile : profiles) {
609      if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) {
610        return true;
611      }
612    }
613    return false;
614  }
615
616  private String getExtensionValue(Element element, String url) {
617    List<Element> extensions = element.getChildrenByName("extension");
618    for (Element extension : extensions) {
619      if (url.equals(extension.getNamedChildValue("url"))) {
620        return extension.getNamedChildValue("value");
621      }
622    }
623    return null;
624  }
625
626  private StructureDefinition loadAsSD(Element src) throws FHIRException, IOException {
627    ByteArrayOutputStream bs = new ByteArrayOutputStream();
628    Manager.compose(context, src, bs, FhirFormat.JSON, OutputStyle.NORMAL, null);
629    if (VersionUtilities.isR2Ver(context.getVersion())) {
630      org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(bs.toByteArray());
631      return (StructureDefinition) VersionConvertorFactory_10_50.convertResource(r2);
632    }
633    if (VersionUtilities.isR2BVer(context.getVersion())) {
634      org.hl7.fhir.dstu2016may.model.Resource r2b = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(bs.toByteArray());
635      return (StructureDefinition) VersionConvertorFactory_14_50.convertResource(r2b);
636    }
637    if (VersionUtilities.isR3Ver(context.getVersion())) {
638      org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(bs.toByteArray());
639      return (StructureDefinition) VersionConvertorFactory_30_50.convertResource(r3);
640    }
641    if (VersionUtilities.isR4Ver(context.getVersion())) {
642      org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(bs.toByteArray());
643      return (StructureDefinition) VersionConvertorFactory_40_50.convertResource(r4);
644    }
645    return (StructureDefinition) new org.hl7.fhir.r5.formats.JsonParser().parse(bs.toByteArray());
646  }
647
648}