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}