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.conv10_50.VersionConvertor_10_50;
012import org.hl7.fhir.convertors.conv14_50.VersionConvertor_14_50;
013import org.hl7.fhir.convertors.conv30_50.VersionConvertor_30_50;
014import org.hl7.fhir.convertors.factory.*;
015import org.hl7.fhir.exceptions.FHIRException;
016import org.hl7.fhir.r5.conformance.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.Resource;
027import org.hl7.fhir.r5.model.StructureDefinition;
028import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
029import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
030import org.hl7.fhir.r5.model.ValueSet;
031import org.hl7.fhir.r5.utils.FHIRPathEngine;
032import org.hl7.fhir.r5.utils.ToolingExtensions;
033import org.hl7.fhir.r5.utils.XVerExtensionManager;
034import org.hl7.fhir.utilities.Utilities;
035import org.hl7.fhir.utilities.VersionUtilities;
036import org.hl7.fhir.utilities.i18n.I18nConstants;
037import org.hl7.fhir.utilities.validation.ValidationMessage;
038import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
039import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
040import org.hl7.fhir.utilities.validation.ValidationOptions;
041import org.hl7.fhir.validation.BaseValidator;
042import org.hl7.fhir.validation.TimeTracker;
043import org.hl7.fhir.validation.instance.utils.NodeStack;
044
045public class StructureDefinitionValidator extends BaseValidator {
046
047  public class FhirPathSorter implements Comparator<ExpressionNode> {
048
049    @Override
050    public int compare(ExpressionNode arg0, ExpressionNode arg1) {
051      return arg0.toString().compareTo(arg1.toString());
052    }
053
054  }
055
056  private FHIRPathEngine fpe;
057  private boolean wantCheckSnapshotUnchanged;
058
059  public StructureDefinitionValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe, boolean wantCheckSnapshotUnchanged, XVerExtensionManager xverManager) {
060    super(context, xverManager);
061    source = Source.InstanceValidator;
062    this.fpe = fpe;
063    this.timeTracker = timeTracker;
064    this.wantCheckSnapshotUnchanged = wantCheckSnapshotUnchanged;
065  }
066  
067  public void validateStructureDefinition(List<ValidationMessage> errors, Element src, NodeStack stack)  {
068    StructureDefinition sd = null;
069    try {
070      sd = loadAsSD(src);
071      List<ElementDefinition> snapshot = sd.getSnapshot().getElement();
072      sd.setSnapshot(null);
073      StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
074      if (warning(errors, IssueType.NOTFOUND, stack.getLiteralPath(), base != null, I18nConstants.UNABLE_TO_FIND_BASE__FOR_, sd.getBaseDefinition(), "StructureDefinition, so can't check the differential")) {
075        if (rule(errors, IssueType.NOTFOUND, stack.getLiteralPath(), sd.hasDerivation(), I18nConstants.SD_MUST_HAVE_DERIVATION, sd.getUrl())) {
076          if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) {
077            List<ValidationMessage> msgs = new ArrayList<>();
078            ProfileUtilities pu = new ProfileUtilities(context, msgs, null);
079            pu.setXver(xverManager);
080            pu.generateSnapshot(base, sd, sd.getUrl(), "http://hl7.org/fhir", sd.getName());
081            if (msgs.size() > 0) {
082              for (ValidationMessage msg : msgs) {
083                // we need to set the location for the context 
084                String loc = msg.getLocation();
085                if (loc.contains("#")) {
086                  msg.setLocation(stack.getLiteralPath()+".differential.element.where(path = '"+loc.substring(loc.indexOf("#")+1)+"')");
087                } else {
088                  msg.setLocation(stack.getLiteralPath());
089                }
090                errors.add(msg);
091              }
092            }
093            if (!snapshot.isEmpty() && wantCheckSnapshotUnchanged) {
094              int was = snapshot.size();
095              int is = sd.getSnapshot().getElement().size();
096              rule(errors, IssueType.NOTFOUND, stack.getLiteralPath(), was == is, I18nConstants.SNAPSHOT_EXISTING_PROBLEM, was, is);
097            }
098          }
099        }
100        if ("constraint".equals(src.getChildValue("derivation"))) {
101          rule(errors, IssueType.NOTFOUND, stack.getLiteralPath(), base.getKindElement().primitiveValue().equals(src.getChildValue("kind")), 
102              I18nConstants.SD_DERIVATION_KIND_MISMATCH, base.getKindElement().primitiveValue(), src.getChildValue("kind"));
103        }
104      }
105    } catch (FHIRException | IOException e) {
106      rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.ERROR_GENERATING_SNAPSHOT, e.getMessage());
107    }
108    List<Element> differentials = src.getChildrenByName("differential");
109    List<Element> snapshots = src.getChildrenByName("snapshot");
110    for (Element differential : differentials) {
111      validateElementList(errors, differential, stack.push(differential, -1, null, null), false, snapshots.size() > 0, sd);
112    }
113    for (Element snapshot : snapshots) {
114      validateElementList(errors, snapshot, stack.push(snapshot, -1, null, null), true, true, sd);
115    }
116  }
117
118  private void validateElementList(List<ValidationMessage> errors, Element elementList, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd) {
119    List<Element> elements = elementList.getChildrenByName("element");
120    int cc = 0;
121    for (Element element : elements) {
122      validateElementDefinition(errors, element, stack.push(element, cc, null, null), snapshot, hasSnapshot, sd);
123      cc++;
124    }    
125  }
126
127  private void validateElementDefinition(List<ValidationMessage> errors, Element element, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd) {
128    boolean typeMustSupport = false;
129    List<Element> types = element.getChildrenByName("type");
130    Set<String> typeCodes = new HashSet<>();
131    for (Element type : types) {
132      if (hasMustSupportExtension(type)) {
133        typeMustSupport = true;
134      }
135      String tc = type.getChildValue("code");
136      if (type.hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type")) {
137        tc = type.getExtensionValue("http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type").primitiveValue();
138      }
139      if (Utilities.noString(tc) && type.hasChild("code")) {
140        if (type.getNamedChild("code").hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type")) {
141          tc = "*";
142        }
143      }
144      typeCodes.add(tc);
145      // check the stated profile - must be a constraint on the type 
146      if (snapshot || sd != null) {
147        validateElementType(errors, type, stack.push(type, -1, null, null), sd, element.getChildValue("path"));
148      }
149    }
150    if (typeMustSupport) {
151      if (snapshot) {
152        rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_SNAPSHOT, element.getNamedChildValue("path"));
153      } else {
154        hint(errors, IssueType.EXCEPTION, stack.getLiteralPath(), hasSnapshot || "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_DIFF, element.getNamedChildValue("path"));        
155      }
156    }
157    if (element.hasChild("binding")) {
158      Element binding = element.getNamedChild("binding");
159      validateBinding(errors, binding, stack.push(binding, -1, null, null), typeCodes, snapshot, element.getNamedChildValue("path"));
160    } else {
161      // 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
162//      String bt = boundType(typeCodes);
163//      hint(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || bt == null, I18nConstants.SD_ED_SHOULD_BIND, element.getNamedChildValue("path"), bt);              
164    }
165    // in a snapshot, we validate that fixedValue, pattern, and defaultValue, if present, are all of the right type
166    if (snapshot && (element.getIdBase() != null) && (element.getIdBase().contains("."))) {
167      if (rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), !typeCodes.isEmpty() || element.hasChild("contentReference"), I18nConstants.SD_NO_TYPES_OR_CONTENTREF, element.getIdBase())) {     
168        Element v = element.getNamedChild("defaultValue");
169        if (v != null) {
170          rule(errors, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "defaultValue", v.fhirType(), typeCodes);
171        }
172        v = element.getNamedChild("fixed");
173        if (v != null) {
174          rule(errors, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "fixed", v.fhirType(), typeCodes);
175        }
176        v = element.getNamedChild("pattern");
177        if (v != null) {
178          rule(errors, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "pattern", v.fhirType(), typeCodes);
179        }
180      }
181    }
182  }
183
184  private String boundType(Set<String> typeCodes) {
185    for (String tc : typeCodes) {
186      if (Utilities.existsInList(tc, "code", "Coding", "CodeableConcept", "Quantity", "CodeableReference")) {
187        return tc;
188      }
189    }
190    return null;
191  }
192
193  private String bindableType(Set<String> typeCodes) {
194    String ret = boundType(typeCodes);
195    if (ret != null) {
196      return ret;
197    }
198    for (String tc : typeCodes) {
199      if (Utilities.existsInList(tc, "string", "uri", "CodeableConcept", "Quantity", "CodeableReference")) {
200        return tc;
201      }
202      StructureDefinition sd = context.fetchTypeDefinition(tc);
203      if (sd != null) {
204        if (sd.hasExtension(ToolingExtensions.EXT_BINDING_METHOD)) {
205          return tc;          
206        }
207      }
208    }
209    return null;
210  }
211
212  private void validateBinding(List<ValidationMessage> errors, Element binding, NodeStack stack, Set<String> typeCodes, boolean snapshot, String path) {
213    rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || bindableType(typeCodes) != null, I18nConstants.SD_ED_BIND_NO_BINDABLE, path, typeCodes.toString());
214    if (binding.hasChild("valueSet")) {
215      Element valueSet = binding.getNamedChild("valueSet");
216      String ref = valueSet.hasPrimitiveValue() ? valueSet.primitiveValue() : valueSet.getNamedChildValue("reference");
217      if (warning(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || ref != null, I18nConstants.SD_ED_SHOULD_BIND_WITH_VS, path)) {
218        Resource vs = context.fetchResource(Resource.class, ref);
219        
220        // just because we can't resolve it directly doesn't mean that terminology server can't. Check with it
221        
222        if (warning(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs != null || serverSupportsValueSet(ref), I18nConstants.SD_ED_BIND_UNKNOWN_VS, path, ref)) {
223          if (vs != null) {
224            rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs instanceof ValueSet, I18nConstants.SD_ED_BIND_NOT_VS, path, ref, vs.fhirType());
225          }
226        }
227      }
228    } 
229  }
230
231  private boolean serverSupportsValueSet(String ref) {
232    ValidationResult vr = context.validateCode(new ValidationOptions().checkValueSetOnly().setVsAsUrl().noClient(), new Coding("http://loinc.org", "5792-7", null), new ValueSet().setUrl(ref));
233    return vr.getErrorClass() == null;
234  }
235
236  private void validateElementType(List<ValidationMessage> errors, Element type, NodeStack stack, StructureDefinition sd, String path) {
237    String code = type.getNamedChildValue("code");
238    if (code == null && path != null) {
239      code = getTypeCodeFromSD(sd, path);
240    }
241    if (code != null) {
242      List<Element> profiles = type.getChildrenByName("profile");
243      if (VersionUtilities.isR2Ver(context.getVersion()) || VersionUtilities.isR2BVer(context.getVersion()) ) {
244        for (Element profile : profiles) {
245          validateProfileTypeOrTarget(errors, profile, code, stack.push(profile, -1, null, null), path);
246        }
247        
248      } else {
249        for (Element profile : profiles) {
250          validateTypeProfile(errors, profile, code, stack.push(profile, -1, null, null), path);
251        }
252        profiles = type.getChildrenByName("targetProfile");
253        for (Element profile : profiles) {
254          validateTargetProfile(errors, profile, code, stack.push(profile, -1, null, null), path);
255        }
256      }
257    }
258  }
259
260  private void validateProfileTypeOrTarget(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) {
261    String p = profile.primitiveValue();
262    StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
263    if (code.equals("Reference")) {
264      if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
265        StructureDefinition t = determineBaseType(sd);
266        if (t == null) {
267          rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
268        } else {
269          rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
270        }
271      }
272    } else {
273      if (sd == null ) {
274        sd = getXverExt(errors, stack.getLiteralPath(), profile, p);
275      }
276      if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
277        StructureDefinition t = determineBaseType(sd);
278        if (t == null) {
279          rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
280        } else {
281          rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), isInstanceOf(t, code), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
282        }
283      }      
284    }
285  }
286
287  private String getTypeCodeFromSD(StructureDefinition sd, String path) {
288    ElementDefinition ed = null;
289    for (ElementDefinition t : sd.getSnapshot().getElement()) {
290      if (t.hasPath() && t.getPath().equals(path)) {
291        if (ed == null) {
292          ed = t;
293        } else {
294          return null; // more than one match, we don't know which is which
295        }
296      }
297    }
298    return ed != null && ed.getType().size() == 1 ? ed.getTypeFirstRep().getCode() : null;
299  }
300
301  private void validateTypeProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) {
302    String p = profile.primitiveValue();
303    StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
304    if (sd == null ) {
305      sd = getXverExt(errors, stack.getLiteralPath(), profile, p);
306    }
307    if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
308      StructureDefinition t = determineBaseType(sd);
309      if (t == null) {
310        rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
311      } else if (!isInstanceOf(t, code)) {
312        rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path);
313      }
314    }
315  }
316
317  private void validateTargetProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) {
318    String p = profile.primitiveValue();
319    StructureDefinition sd = context.fetchResource(StructureDefinition.class, p);
320    if (code.equals("Reference")) {
321      if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
322        StructureDefinition t = determineBaseType(sd);
323        if (t == null) {
324          rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
325        } else {
326          rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Resource");
327        }
328      }
329    } else if (code.equals("canonical")) {
330      if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) {
331        StructureDefinition t = determineBaseType(sd);
332        if (t == null) {
333          rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p);
334        } else if (!VersionUtilities.isR5Ver(context.getVersion())) {
335          rule(errors, 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");
336        } else {
337          rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(t.getType()), I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Canonical Resource");
338        }  
339      }
340    } else {
341      rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_NO_TARGET_PROFILE, code);
342    }
343  }
344
345  private boolean isInstanceOf(StructureDefinition sd, String code) {
346    while (sd != null) {
347      if (sd.getType().equals(code)) {
348        return true;
349      }
350      if (sd.getUrl().equals(code)) {
351        return true;
352      }
353      sd = sd.hasBaseDefinition() ? context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()) : null;
354      if (!(VersionUtilities.isR2Ver(context.getVersion()) || VersionUtilities.isR2BVer(context.getVersion())) && sd != null && !sd.getAbstract() && sd.getKind() != StructureDefinitionKind.LOGICAL) {
355        sd = null;
356      }
357    }
358    
359    return false;
360  }
361
362  private StructureDefinition determineBaseType(StructureDefinition sd) {
363    while (sd != null && sd.getDerivation() == TypeDerivationRule.CONSTRAINT) {
364      sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
365    }
366    return sd;
367  }
368
369  private boolean hasMustSupportExtension(Element type) {
370    if ("true".equals(getExtensionValue(type, ToolingExtensions.EXT_MUST_SUPPORT))) {
371      return true;
372    }
373    List<Element> profiles = type.getChildrenByName("profile");
374    for (Element profile : profiles) {
375      if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) {
376        return true;
377      }
378    }
379    profiles = type.getChildrenByName("targetProfile");
380    for (Element profile : profiles) {
381      if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) {
382        return true;
383      }
384    }
385    return false;
386  }
387
388  private String getExtensionValue(Element element, String url) {
389    List<Element> extensions = element.getChildrenByName("extension");
390    for (Element extension : extensions) {
391      if (url.equals(extension.getNamedChildValue("url"))) {
392        return extension.getNamedChildValue("value");
393      }
394    }
395    return null;
396  }
397
398  private StructureDefinition loadAsSD(Element src) throws FHIRException, IOException {
399    ByteArrayOutputStream bs = new ByteArrayOutputStream();
400    Manager.compose(context, src, bs, FhirFormat.JSON, OutputStyle.NORMAL, null);
401    if (VersionUtilities.isR2Ver(context.getVersion())) {
402      org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(bs.toByteArray());
403      return (StructureDefinition) VersionConvertorFactory_10_50.convertResource(r2);
404    }
405    if (VersionUtilities.isR2BVer(context.getVersion())) {
406      org.hl7.fhir.dstu2016may.model.Resource r2b = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(bs.toByteArray());
407      return (StructureDefinition) VersionConvertorFactory_14_50.convertResource(r2b);
408    }
409    if (VersionUtilities.isR3Ver(context.getVersion())) {
410      org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(bs.toByteArray());
411      return (StructureDefinition) VersionConvertorFactory_30_50.convertResource(r3);
412    }
413    if (VersionUtilities.isR4Ver(context.getVersion())) {
414      org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(bs.toByteArray());
415      return (StructureDefinition) VersionConvertorFactory_40_50.convertResource(r4);
416    }
417    return (StructureDefinition) new org.hl7.fhir.r5.formats.JsonParser().parse(bs.toByteArray());
418  }
419
420}