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}