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