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 if (codeSystem) { 116 return codeSystems.get(theSystem); 117 } else { 118 return valueSets.get(theSystem); 119 } 120 } 121 } 122 123 @SuppressWarnings("unchecked") 124 @Override 125 public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { 126 Validate.notBlank(theUri, "theUri must not be null or blank"); 127 128 if (theClass.equals(StructureDefinition.class)) { 129 return (T) fetchStructureDefinition(theContext, theUri); 130 } 131 132 if (theClass.equals(ValueSet.class) || theUri.startsWith(URL_PREFIX_VALUE_SET)) { 133 return (T) fetchValueSet(theContext, theUri); 134 } 135 136 return null; 137 } 138 139 @Override 140 public StructureDefinition fetchStructureDefinition(FhirContext theContext, String theUrl) { 141 String url = theUrl; 142 if (url.startsWith(URL_PREFIX_STRUCTURE_DEFINITION)) { 143 // no change 144 } else if (url.indexOf('/') == -1) { 145 url = URL_PREFIX_STRUCTURE_DEFINITION + url; 146 } else if (StringUtils.countMatches(url, '/') == 1) { 147 url = URL_PREFIX_STRUCTURE_DEFINITION_BASE + url; 148 } 149 return provideStructureDefinitionMap(theContext).get(url); 150 } 151 152 ValueSet fetchValueSet(FhirContext theContext, String theSystem) { 153 return (ValueSet) fetchCodeSystemOrValueSet(theContext, theSystem, false); 154 } 155 156 public void flush() { 157 myCodeSystems = null; 158 myStructureDefinitions = null; 159 } 160 161 @Override 162 public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) { 163 CodeSystem cs = fetchCodeSystem(theContext, theSystem); 164 return cs != null && cs.getContent() != CodeSystemContentMode.NOTPRESENT; 165 } 166 167 private void loadCodeSystems(FhirContext theContext, Map<String, CodeSystem> theCodeSystems, Map<String, ValueSet> theValueSets, String theClasspath) { 168 ourLog.info("Loading CodeSystem/ValueSet from classpath: {}", theClasspath); 169 InputStream inputStream = DefaultProfileValidationSupport.class.getResourceAsStream(theClasspath); 170 InputStreamReader reader = null; 171 if (inputStream != null) { 172 try { 173 reader = new InputStreamReader(inputStream, Constants.CHARSET_UTF8); 174 175 Bundle bundle = theContext.newXmlParser().parseResource(Bundle.class, reader); 176 for (BundleEntryComponent next : bundle.getEntry()) { 177 if (next.getResource() instanceof CodeSystem) { 178 CodeSystem nextValueSet = (CodeSystem) next.getResource(); 179 nextValueSet.getText().setDivAsString(""); 180 String system = nextValueSet.getUrl(); 181 if (isNotBlank(system)) { 182 theCodeSystems.put(system, nextValueSet); 183 } 184 } else if (next.getResource() instanceof ValueSet) { 185 ValueSet nextValueSet = (ValueSet) next.getResource(); 186 nextValueSet.getText().setDivAsString(""); 187 String system = nextValueSet.getUrl(); 188 if (isNotBlank(system)) { 189 theValueSets.put(system, nextValueSet); 190 } 191 } 192 } 193 } finally { 194 try { 195 if (reader != null) { 196 reader.close(); 197 } 198 inputStream.close(); 199 } catch (IOException e) { 200 ourLog.warn("Failure closing stream", e); 201 } 202 } 203 } else { 204 ourLog.warn("Unable to load resource: {}", theClasspath); 205 } 206 } 207 208 private void loadStructureDefinitions(FhirContext theContext, Map<String, StructureDefinition> theCodeSystems, String theClasspath) { 209 ourLog.info("Loading structure definitions from classpath: {}", theClasspath); 210 InputStream valuesetText = DefaultProfileValidationSupport.class.getResourceAsStream(theClasspath); 211 if (valuesetText != null) { 212 InputStreamReader reader = new InputStreamReader(valuesetText, Constants.CHARSET_UTF8); 213 214 Bundle bundle = theContext.newXmlParser().parseResource(Bundle.class, reader); 215 for (BundleEntryComponent next : bundle.getEntry()) { 216 if (next.getResource() instanceof StructureDefinition) { 217 StructureDefinition nextSd = (StructureDefinition) next.getResource(); 218 nextSd.getText().setDivAsString(""); 219 String system = nextSd.getUrl(); 220 if (isNotBlank(system)) { 221 theCodeSystems.put(system, nextSd); 222 } 223 } 224 } 225 } else { 226 ourLog.warn("Unable to load resource: {}", theClasspath); 227 } 228 } 229 230 private Map<String, StructureDefinition> provideStructureDefinitionMap(FhirContext theContext) { 231 Map<String, StructureDefinition> structureDefinitions = myStructureDefinitions; 232 if (structureDefinitions == null) { 233 structureDefinitions = new HashMap<>(); 234 235 loadStructureDefinitions(theContext, structureDefinitions, "/org/hl7/fhir/r4/model/profile/profiles-resources.xml"); 236 loadStructureDefinitions(theContext, structureDefinitions, "/org/hl7/fhir/r4/model/profile/profiles-types.xml"); 237 loadStructureDefinitions(theContext, structureDefinitions, "/org/hl7/fhir/r4/model/profile/profiles-others.xml"); 238 loadStructureDefinitions(theContext, structureDefinitions, "/org/hl7/fhir/r4/model/extension/extension-definitions.xml"); 239 240 myStructureDefinitions = structureDefinitions; 241 } 242 return structureDefinitions; 243 } 244 245 private CodeValidationResult testIfConceptIsInList(String theCode, List<ConceptDefinitionComponent> conceptList, boolean theCaseSensitive) { 246 String code = theCode; 247 if (theCaseSensitive == false) { 248 code = code.toUpperCase(); 249 } 250 251 return testIfConceptIsInListInner(conceptList, theCaseSensitive, code); 252 } 253 254 private CodeValidationResult testIfConceptIsInListInner(List<ConceptDefinitionComponent> conceptList, boolean theCaseSensitive, String code) { 255 CodeValidationResult retVal = null; 256 for (ConceptDefinitionComponent next : conceptList) { 257 String nextCandidate = next.getCode(); 258 if (theCaseSensitive == false) { 259 nextCandidate = nextCandidate.toUpperCase(); 260 } 261 if (nextCandidate.equals(code)) { 262 retVal = new CodeValidationResult(next); 263 break; 264 } 265 266 // recurse 267 retVal = testIfConceptIsInList(code, next.getConcept(), theCaseSensitive); 268 if (retVal != null) { 269 break; 270 } 271 } 272 273 return retVal; 274 } 275 276 @Override 277 public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { 278 CodeSystem cs = fetchCodeSystem(theContext, theCodeSystem); 279 if (cs != null) { 280 boolean caseSensitive = true; 281 if (cs.hasCaseSensitive()) { 282 caseSensitive = cs.getCaseSensitive(); 283 } 284 285 CodeValidationResult retVal = testIfConceptIsInList(theCode, cs.getConcept(), caseSensitive); 286 287 if (retVal != null) { 288 return retVal; 289 } 290 } 291 292 return new CodeValidationResult(IssueSeverity.WARNING, "Unknown code: " + theCodeSystem + " / " + theCode); 293 } 294 295}