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