001package org.hl7.fhir.r4.hapi.ctx;
002
003import ca.uhn.fhir.context.FhirContext;
004import ca.uhn.fhir.rest.api.Constants;
005import org.apache.commons.lang3.StringUtils;
006import org.apache.commons.lang3.Validate;
007import org.hl7.fhir.instance.model.api.IBaseResource;
008import org.hl7.fhir.r4.model.Bundle;
009import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
010import org.hl7.fhir.r4.model.CodeSystem;
011import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode;
012import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
013import org.hl7.fhir.r4.model.CodeType;
014import org.hl7.fhir.r4.model.DomainResource;
015import org.hl7.fhir.r4.model.StructureDefinition;
016import org.hl7.fhir.r4.model.UriType;
017import org.hl7.fhir.r4.model.ValueSet;
018import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent;
019import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
020import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent;
021import org.hl7.fhir.r4.terminologies.ValueSetExpander;
022import org.hl7.fhir.r4.terminologies.ValueSetExpanderSimple;
023import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
024
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.util.ArrayList;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034
035import static org.apache.commons.lang3.StringUtils.defaultString;
036import static org.apache.commons.lang3.StringUtils.isBlank;
037import static org.apache.commons.lang3.StringUtils.isNotBlank;
038
039public class DefaultProfileValidationSupport implements IValidationSupport {
040
041  private static final String URL_PREFIX_VALUE_SET = "http://hl7.org/fhir/ValueSet/";
042  private static final String URL_PREFIX_STRUCTURE_DEFINITION = "http://hl7.org/fhir/StructureDefinition/";
043  private static final String URL_PREFIX_STRUCTURE_DEFINITION_BASE = "http://hl7.org/fhir/";
044
045  private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultProfileValidationSupport.class);
046
047  private Map<String, CodeSystem> myCodeSystems;
048  private Map<String, StructureDefinition> myStructureDefinitions;
049  private Map<String, ValueSet> myValueSets;
050
051  private void addConcepts(ConceptSetComponent theInclude, ValueSetExpansionComponent theRetVal, Set<String> theWantCodes, List<ConceptDefinitionComponent> theConcepts) {
052    for (ConceptDefinitionComponent next : theConcepts) {
053      if (theWantCodes.isEmpty() || theWantCodes.contains(next.getCode())) {
054        theRetVal
055          .addContains()
056          .setSystem(theInclude.getSystem())
057          .setCode(next.getCode())
058          .setDisplay(next.getDisplay());
059      }
060      addConcepts(theInclude, theRetVal, theWantCodes, next.getConcept());
061    }
062  }
063
064  @Override
065  public ValueSetExpander.ValueSetExpansionOutcome expandValueSet(FhirContext theContext, ConceptSetComponent theInclude) {
066    ValueSetExpander.ValueSetExpansionOutcome retVal = new ValueSetExpander.ValueSetExpansionOutcome(new ValueSet());
067
068    Set<String> wantCodes = new HashSet<>();
069    for (ConceptReferenceComponent next : theInclude.getConcept()) {
070      wantCodes.add(next.getCode());
071    }
072
073    CodeSystem system = fetchCodeSystem(theContext, theInclude.getSystem());
074    if (system != null) {
075      List<ConceptDefinitionComponent> concepts = system.getConcept();
076      addConcepts(theInclude, retVal.getValueset().getExpansion(), wantCodes, concepts);
077    }
078
079    for (UriType next : theInclude.getValueSet()) {
080      ValueSet vs = myValueSets.get(defaultString(next.getValueAsString()));
081      if (vs != null) {
082        for (ConceptSetComponent nextInclude : vs.getCompose().getInclude()) {
083          ValueSetExpander.ValueSetExpansionOutcome contents = expandValueSet(theContext, nextInclude);
084          retVal.getValueset().getExpansion().getContains().addAll(contents.getValueset().getExpansion().getContains());
085        }
086      }
087    }
088
089    return retVal;
090  }
091
092  @Override
093  public List<IBaseResource> fetchAllConformanceResources(FhirContext theContext) {
094    ArrayList<IBaseResource> retVal = new ArrayList<>();
095    retVal.addAll(myCodeSystems.values());
096    retVal.addAll(myStructureDefinitions.values());
097    retVal.addAll(myValueSets.values());
098    return retVal;
099  }
100
101  @Override
102  public List<StructureDefinition> fetchAllStructureDefinitions(FhirContext theContext) {
103    return new ArrayList<>(provideStructureDefinitionMap(theContext).values());
104  }
105
106
107  @Override
108  public CodeSystem fetchCodeSystem(FhirContext theContext, String theSystem) {
109    return (CodeSystem) fetchCodeSystemOrValueSet(theContext, theSystem, true);
110  }
111
112  private DomainResource fetchCodeSystemOrValueSet(FhirContext theContext, String theSystem, boolean codeSystem) {
113    synchronized (this) {
114      Map<String, CodeSystem> codeSystems = myCodeSystems;
115      Map<String, ValueSet> valueSets = myValueSets;
116      if (codeSystems == null || valueSets == null) {
117        codeSystems = new HashMap<>();
118        valueSets = new HashMap<>();
119
120        loadCodeSystems(theContext, codeSystems, valueSets, "/org/hl7/fhir/r4/model/valueset/valuesets.xml");
121        loadCodeSystems(theContext, codeSystems, valueSets, "/org/hl7/fhir/r4/model/valueset/v2-tables.xml");
122        loadCodeSystems(theContext, codeSystems, valueSets, "/org/hl7/fhir/r4/model/valueset/v3-codesystems.xml");
123
124        myCodeSystems = codeSystems;
125        myValueSets = valueSets;
126      }
127
128      // System can take the form "http://url|version"
129      String system = theSystem;
130      if (system.contains("|")) {
131        String version = system.substring(system.indexOf('|') + 1);
132        if (version.matches("^[0-9.]+$")) {
133          system = system.substring(0, system.indexOf('|'));
134        }
135      }
136
137      if (codeSystem) {
138        return codeSystems.get(system);
139      } else {
140        return valueSets.get(system);
141      }
142    }
143  }
144
145  @SuppressWarnings("unchecked")
146  @Override
147  public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
148    Validate.notBlank(theUri, "theUri must not be null or blank");
149
150    if (theClass.equals(StructureDefinition.class)) {
151      return (T) fetchStructureDefinition(theContext, theUri);
152    }
153
154    if (theClass.equals(ValueSet.class) || theUri.startsWith(URL_PREFIX_VALUE_SET)) {
155      return (T) fetchValueSet(theContext, theUri);
156    }
157
158    return null;
159  }
160
161  @Override
162  public StructureDefinition fetchStructureDefinition(FhirContext theContext, String theUrl) {
163    String url = theUrl;
164    if (url.startsWith(URL_PREFIX_STRUCTURE_DEFINITION)) {
165      // no change
166    } else if (url.indexOf('/') == -1) {
167      url = URL_PREFIX_STRUCTURE_DEFINITION + url;
168    } else if (StringUtils.countMatches(url, '/') == 1) {
169      url = URL_PREFIX_STRUCTURE_DEFINITION_BASE + url;
170    }
171    return provideStructureDefinitionMap(theContext).get(url);
172  }
173
174  @Override
175  public ValueSet fetchValueSet(FhirContext theContext, String uri) {
176    return (ValueSet) fetchCodeSystemOrValueSet(theContext, uri, false);
177  }
178
179  public void flush() {
180    myCodeSystems = null;
181    myStructureDefinitions = null;
182  }
183
184  @Override
185  public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) {
186    if (isBlank(theSystem) || Constants.codeSystemNotNeeded(theSystem)) {
187      return false;
188    }
189    CodeSystem cs = fetchCodeSystem(theContext, theSystem);
190    return cs != null && cs.getContent() != CodeSystemContentMode.NOTPRESENT;
191  }
192
193  @Override
194  public boolean isValueSetSupported(FhirContext theContext, String theValueSetUrl) {
195    return isNotBlank(theValueSetUrl) && fetchValueSet(theContext, theValueSetUrl) != null;
196  }
197
198  @Override
199  public StructureDefinition generateSnapshot(StructureDefinition theInput, String theUrl, String theWebUrl, String theProfileName) {
200    return null;
201  }
202
203  private void loadCodeSystems(FhirContext theContext, Map<String, CodeSystem> theCodeSystems, Map<String, ValueSet> theValueSets, String theClasspath) {
204    ourLog.info("Loading CodeSystem/ValueSet from classpath: {}", theClasspath);
205    InputStream inputStream = DefaultProfileValidationSupport.class.getResourceAsStream(theClasspath);
206    InputStreamReader reader = null;
207    if (inputStream != null) {
208      try {
209        reader = new InputStreamReader(inputStream, Constants.CHARSET_UTF8);
210
211        Bundle bundle = theContext.newXmlParser().parseResource(Bundle.class, reader);
212        for (BundleEntryComponent next : bundle.getEntry()) {
213          if (next.getResource() instanceof CodeSystem) {
214            CodeSystem nextValueSet = (CodeSystem) next.getResource();
215            nextValueSet.getText().setDivAsString("");
216            String system = nextValueSet.getUrl();
217            if (isNotBlank(system)) {
218              theCodeSystems.put(system, nextValueSet);
219            }
220          } else if (next.getResource() instanceof ValueSet) {
221            ValueSet nextValueSet = (ValueSet) next.getResource();
222            nextValueSet.getText().setDivAsString("");
223            String system = nextValueSet.getUrl();
224            if (isNotBlank(system)) {
225              theValueSets.put(system, nextValueSet);
226            }
227          }
228        }
229      } finally {
230        try {
231          if (reader != null) {
232            reader.close();
233          }
234          inputStream.close();
235        } catch (IOException e) {
236          ourLog.warn("Failure closing stream", e);
237        }
238      }
239    } else {
240      ourLog.warn("Unable to load resource: {}", theClasspath);
241    }
242  }
243
244  private void loadStructureDefinitions(FhirContext theContext, Map<String, StructureDefinition> theCodeSystems, String theClasspath) {
245    ourLog.info("Loading structure definitions from classpath: {}", theClasspath);
246    InputStream valuesetText = DefaultProfileValidationSupport.class.getResourceAsStream(theClasspath);
247    if (valuesetText != null) {
248      InputStreamReader reader = new InputStreamReader(valuesetText, Constants.CHARSET_UTF8);
249
250      Bundle bundle = theContext.newXmlParser().parseResource(Bundle.class, reader);
251      for (BundleEntryComponent next : bundle.getEntry()) {
252        if (next.getResource() instanceof StructureDefinition) {
253          StructureDefinition nextSd = (StructureDefinition) next.getResource();
254          nextSd.getText().setDivAsString("");
255          String system = nextSd.getUrl();
256          if (isNotBlank(system)) {
257            theCodeSystems.put(system, nextSd);
258          }
259        }
260      }
261    } else {
262      ourLog.warn("Unable to load resource: {}", theClasspath);
263    }
264  }
265
266  private Map<String, StructureDefinition> provideStructureDefinitionMap(FhirContext theContext) {
267    Map<String, StructureDefinition> structureDefinitions = myStructureDefinitions;
268    if (structureDefinitions == null) {
269      structureDefinitions = new HashMap<>();
270
271      loadStructureDefinitions(theContext, structureDefinitions, "/org/hl7/fhir/r4/model/profile/profiles-resources.xml");
272      loadStructureDefinitions(theContext, structureDefinitions, "/org/hl7/fhir/r4/model/profile/profiles-types.xml");
273      loadStructureDefinitions(theContext, structureDefinitions, "/org/hl7/fhir/r4/model/profile/profiles-others.xml");
274      loadStructureDefinitions(theContext, structureDefinitions, "/org/hl7/fhir/r4/model/extension/extension-definitions.xml");
275
276      myStructureDefinitions = structureDefinitions;
277    }
278    return structureDefinitions;
279  }
280
281  private CodeValidationResult testIfConceptIsInList(CodeSystem theCodeSystem, String theCode, List<ConceptDefinitionComponent> conceptList, boolean theCaseSensitive) {
282    String code = theCode;
283    if (theCaseSensitive == false) {
284      code = code.toUpperCase();
285    }
286
287    return testIfConceptIsInListInner(theCodeSystem, conceptList, theCaseSensitive, code);
288  }
289
290  private CodeValidationResult testIfConceptIsInListInner(CodeSystem theCodeSystem, List<ConceptDefinitionComponent> conceptList, boolean theCaseSensitive, String code) {
291    CodeValidationResult retVal = null;
292    for (ConceptDefinitionComponent next : conceptList) {
293      String nextCandidate = next.getCode();
294      if (theCaseSensitive == false) {
295        nextCandidate = nextCandidate.toUpperCase();
296      }
297      if (nextCandidate.equals(code)) {
298        retVal = new CodeValidationResult(null, null, next, next.getDisplay());
299        break;
300      }
301
302      // recurse
303      retVal = testIfConceptIsInList(theCodeSystem, code, next.getConcept(), theCaseSensitive);
304      if (retVal != null) {
305        break;
306      }
307    }
308
309    if (retVal != null) {
310      retVal.setCodeSystemName(theCodeSystem.getName());
311      retVal.setCodeSystemVersion(theCodeSystem.getVersion());
312    }
313
314    return retVal;
315  }
316
317  @Override
318  public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
319    if (isNotBlank(theValueSetUrl)) {
320      ValueSetExpander expander = new ValueSetExpanderSimple(new HapiWorkerContext(theContext, this));
321      try {
322        ValueSet valueSet = fetchValueSet(theContext, theValueSetUrl);
323        if (valueSet != null) {
324          ValueSetExpander.ValueSetExpansionOutcome expanded = expander.expand(valueSet, null);
325          ValueSetExpansionComponent expansion = expanded.getValueset().getExpansion();
326          for (ValueSet.ValueSetExpansionContainsComponent nextExpansionCode : expansion.getContains()) {
327
328            if (theCode.equals(nextExpansionCode.getCode())) {
329              if (Constants.codeSystemNotNeeded(theCodeSystem) || nextExpansionCode.getSystem().equals(theCodeSystem)) {
330                return new CodeValidationResult(new CodeSystem.ConceptDefinitionComponent(new CodeType(theCode)));
331              }
332            }
333          }
334
335        }
336      } catch (Exception e) {
337        return new CodeValidationResult(IssueSeverity.WARNING, e.getMessage());
338      }
339
340      return null;
341    }
342
343    if (theCodeSystem != null) {
344      CodeSystem cs = fetchCodeSystem(theContext, theCodeSystem);
345      if (cs != null) {
346        boolean caseSensitive = true;
347        if (cs.hasCaseSensitive()) {
348          caseSensitive = cs.getCaseSensitive();
349        }
350
351        CodeValidationResult retVal = testIfConceptIsInList(cs, theCode, cs.getConcept(), caseSensitive);
352
353        if (retVal != null) {
354          return retVal;
355        }
356      }
357    }
358
359    return new CodeValidationResult(IssueSeverity.WARNING, "Unknown code: " + theCodeSystem + " / " + theCode);
360  }
361
362  @Override
363  public LookupCodeResult lookupCode(FhirContext theContext, String theSystem, String theCode) {
364    return validateCode(theContext, theSystem, theCode, null, (String) null).asLookupCodeResult(theSystem, theCode);
365  }
366
367}