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}