001package org.hl7.fhir.r4.terminologies; 002 003import static org.apache.commons.lang3.StringUtils.isNotBlank; 004 005import java.io.FileNotFoundException; 006import java.io.IOException; 007 008/* 009 * Copyright (c) 2011+, HL7, Inc 010 * All rights reserved. 011 * 012 * Redistribution and use in source and binary forms, with or without modification, 013 * are permitted provided that the following conditions are met: 014 * 015 * Redistributions of source code must retain the above copyright notice, this 016 * list of conditions and the following disclaimer. 017 * Redistributions in binary form must reproduce the above copyright notice, 018 * this list of conditions and the following disclaimer in the documentation 019 * and/or other materials provided with the distribution. 020 * Neither the name of HL7 nor the names of its contributors may be used to 021 * endorse or promote products derived from this software without specific 022 * prior written permission. 023 * 024 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 025 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 026 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 027 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 028 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 029 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 030 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 031 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 032 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 033 * POSSIBILITY OF SUCH DAMAGE. 034 * 035 */ 036 037import java.util.ArrayList; 038import java.util.HashMap; 039import java.util.HashSet; 040import java.util.List; 041import java.util.Map; 042import java.util.Set; 043import java.util.UUID; 044 045import org.apache.commons.lang3.NotImplementedException; 046import org.hl7.fhir.r4.context.IWorkerContext; 047import org.hl7.fhir.r4.model.BackboneElement; 048import org.hl7.fhir.r4.model.Base; 049import org.hl7.fhir.r4.model.CodeSystem; 050import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; 051import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; 052import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionDesignationComponent; 053import org.hl7.fhir.r4.model.DateTimeType; 054import org.hl7.fhir.r4.model.ExpansionProfile; 055import org.hl7.fhir.r4.model.Factory; 056import org.hl7.fhir.r4.model.PrimitiveType; 057import org.hl7.fhir.r4.model.Type; 058import org.hl7.fhir.r4.model.UriType; 059import org.hl7.fhir.r4.model.ValueSet; 060import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent; 061import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceDesignationComponent; 062import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; 063import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent; 064import org.hl7.fhir.r4.model.ValueSet.FilterOperator; 065import org.hl7.fhir.r4.model.ValueSet.ValueSetComposeComponent; 066import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent; 067import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent; 068import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionParameterComponent; 069import org.hl7.fhir.r4.utils.ToolingExtensions; 070import org.hl7.fhir.exceptions.FHIRException; 071import org.hl7.fhir.exceptions.FHIRFormatError; 072import org.hl7.fhir.exceptions.NoTerminologyServiceException; 073import org.hl7.fhir.exceptions.TerminologyServiceException; 074import org.hl7.fhir.utilities.Utilities; 075 076public class ValueSetExpanderSimple implements ValueSetExpander { 077 078 private List<ValueSetExpansionContainsComponent> codes = new ArrayList<ValueSet.ValueSetExpansionContainsComponent>(); 079 private List<ValueSetExpansionContainsComponent> roots = new ArrayList<ValueSet.ValueSetExpansionContainsComponent>(); 080 private Map<String, ValueSetExpansionContainsComponent> map = new HashMap<String, ValueSet.ValueSetExpansionContainsComponent>(); 081 private IWorkerContext context; 082 private boolean canBeHeirarchy = true; 083 private Set<String> excludeKeys = new HashSet<String>(); 084 private Set<String> excludeSystems = new HashSet<String>(); 085 private ValueSetExpanderFactory factory; 086 private ValueSet focus; 087 private int maxExpansionSize = 500; 088 089 private int total; 090 091 public ValueSetExpanderSimple(IWorkerContext context, ValueSetExpanderFactory factory) { 092 super(); 093 this.context = context; 094 this.factory = factory; 095 } 096 097 public void setMaxExpansionSize(int theMaxExpansionSize) { 098 maxExpansionSize = theMaxExpansionSize; 099 } 100 101 private ValueSetExpansionContainsComponent addCode(String system, String code, String display, ValueSetExpansionContainsComponent parent, List<ConceptDefinitionDesignationComponent> designations, 102 ExpansionProfile profile, boolean isAbstract, boolean inactive, List<ValueSet> filters) { 103 104 if (filters != null && !filters.isEmpty() && !filterContainsCode(filters, system, code)) 105 return null; 106 ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent(); 107 n.setSystem(system); 108 n.setCode(code); 109 if (isAbstract) 110 n.setAbstract(true); 111 if (inactive) 112 n.setInactive(true); 113 114 if (profile.getIncludeDesignations() && designations != null) { 115 for (ConceptDefinitionDesignationComponent t : designations) { 116 ToolingExtensions.addLanguageTranslation(n, t.getLanguage(), t.getValue()); 117 } 118 } 119 ConceptDefinitionDesignationComponent t = profile.hasLanguage() ? getMatchingLang(designations, profile.getLanguage()) : null; 120 if (t == null) 121 n.setDisplay(display); 122 else 123 n.setDisplay(t.getValue()); 124 125 String s = key(n); 126 if (map.containsKey(s) || excludeKeys.contains(s)) { 127 canBeHeirarchy = false; 128 } else { 129 codes.add(n); 130 map.put(s, n); 131 total++; 132 } 133 if (canBeHeirarchy && parent != null) { 134 parent.getContains().add(n); 135 } else { 136 roots.add(n); 137 } 138 return n; 139 } 140 141 private boolean filterContainsCode(List<ValueSet> filters, String system, String code) { 142 for (ValueSet vse : filters) 143 if (expansionContainsCode(vse.getExpansion().getContains(), system, code)) 144 return true; 145 return false; 146 } 147 148 private boolean expansionContainsCode(List<ValueSetExpansionContainsComponent> contains, String system, String code) { 149 for (ValueSetExpansionContainsComponent cc : contains) { 150 if (system.equals(cc.getSystem()) && code.equals(cc.getCode())) 151 return true; 152 if (expansionContainsCode(cc.getContains(), system, code)) 153 return true; 154 } 155 return false; 156 } 157 158 private ConceptDefinitionDesignationComponent getMatchingLang(List<ConceptDefinitionDesignationComponent> list, String lang) { 159 for (ConceptDefinitionDesignationComponent t : list) 160 if (t.getLanguage().equals(lang)) 161 return t; 162 for (ConceptDefinitionDesignationComponent t : list) 163 if (t.getLanguage().startsWith(lang)) 164 return t; 165 return null; 166 } 167 168 private void addCodeAndDescendents(CodeSystem cs, String system, ConceptDefinitionComponent def, ValueSetExpansionContainsComponent parent, ExpansionProfile profile, List<ValueSet> filters) 169 throws FHIRException { 170 if (!CodeSystemUtilities.isDeprecated(cs, def)) { 171 ValueSetExpansionContainsComponent np = null; 172 boolean abs = CodeSystemUtilities.isNotSelectable(cs, def); 173 boolean inc = CodeSystemUtilities.isInactive(cs, def); 174 if (canBeHeirarchy || !abs) 175 np = addCode(system, def.getCode(), def.getDisplay(), parent, def.getDesignation(), profile, abs, inc, filters); 176 for (ConceptDefinitionComponent c : def.getConcept()) 177 addCodeAndDescendents(cs, system, c, np, profile, filters); 178 } else { 179 for (ConceptDefinitionComponent c : def.getConcept()) 180 addCodeAndDescendents(cs, system, c, null, profile, filters); 181 } 182 183 } 184 185 private void addCodes(ValueSetExpansionComponent expand, List<ValueSetExpansionParameterComponent> params, ExpansionProfile profile, List<ValueSet> filters) throws ETooCostly, FHIRException { 186 if (expand.getContains().size() > maxExpansionSize) 187 throw new ETooCostly("Too many codes to display (>" + Integer.toString(expand.getContains().size()) + ")"); 188 for (ValueSetExpansionParameterComponent p : expand.getParameter()) { 189 if (!existsInParams(params, p.getName(), p.getValue())) 190 params.add(p); 191 } 192 193 copyImportContains(expand.getContains(), null, profile, filters); 194 } 195 196 private void excludeCode(String theSystem, String theCode) { 197 ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent(); 198 n.setSystem(theSystem); 199 n.setCode(theCode); 200 String s = key(n); 201 excludeKeys.add(s); 202 } 203 204 private void excludeCodes(ConceptSetComponent exc, List<ValueSetExpansionParameterComponent> params, String ctxt) throws FHIRException { 205 exc.checkNoModifiers("Compose.exclude", "expanding"); 206 if (exc.hasSystem() && exc.getConcept().size() == 0 && exc.getFilter().size() == 0) { 207 excludeSystems.add(exc.getSystem()); 208 } 209 210 if (exc.hasValueSet()) 211 throw new Error("Processing Value set references in exclude is not yet done in "+ctxt); 212 // importValueSet(imp.getValue(), params, profile); 213 214 CodeSystem cs = context.fetchCodeSystem(exc.getSystem()); 215 if ((cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) && context.supportsSystem(exc.getSystem())) { 216 excludeCodes(context.expandVS(exc, false), params); 217 return; 218 } 219 220 for (ConceptReferenceComponent c : exc.getConcept()) { 221 excludeCode(exc.getSystem(), c.getCode()); 222 } 223 224 if (exc.getFilter().size() > 0) 225 throw new NotImplementedException("not done yet"); 226 } 227 228 private void excludeCodes(ValueSetExpansionComponent expand, List<ValueSetExpansionParameterComponent> params) { 229 for (ValueSetExpansionContainsComponent c : expand.getContains()) { 230 excludeCode(c.getSystem(), c.getCode()); 231 } 232 } 233 234 private boolean existsInParams(List<ValueSetExpansionParameterComponent> params, String name, Type value) { 235 for (ValueSetExpansionParameterComponent p : params) { 236 if (p.getName().equals(name) && PrimitiveType.compareDeep(p.getValue(), value, false)) 237 return true; 238 } 239 return false; 240 } 241 242 @Override 243 public ValueSetExpansionOutcome expand(ValueSet source, ExpansionProfile profile) { 244 245 if (profile == null) 246 profile = makeDefaultExpansion(); 247 try { 248 source.checkNoModifiers("ValueSet", "expanding"); 249 focus = source.copy(); 250 focus.setExpansion(new ValueSet.ValueSetExpansionComponent()); 251 focus.getExpansion().setTimestampElement(DateTimeType.now()); 252 focus.getExpansion().setIdentifier(Factory.createUUID()); 253 if (!profile.getUrl().startsWith("urn:uuid:")) 254 focus.getExpansion().addParameter().setName("profile").setValue(new UriType(profile.getUrl())); 255 256 if (source.hasCompose()) 257 handleCompose(source.getCompose(), focus.getExpansion().getParameter(), profile, source.getUrl()); 258 259 if (canBeHeirarchy) { 260 for (ValueSetExpansionContainsComponent c : roots) { 261 focus.getExpansion().getContains().add(c); 262 } 263 } else { 264 for (ValueSetExpansionContainsComponent c : codes) { 265 if (map.containsKey(key(c)) && !c.getAbstract()) { // we may have added abstract codes earlier while we still thought it might be heirarchical, but later we gave up, so now ignore them 266 focus.getExpansion().getContains().add(c); 267 c.getContains().clear(); // make sure any heirarchy is wiped 268 } 269 } 270 } 271 272 if (total > 0) { 273 focus.getExpansion().setTotal(total); 274 } 275 276 return new ValueSetExpansionOutcome(focus); 277 } catch (RuntimeException e) { 278 // TODO: we should put something more specific instead of just Exception below, since 279 // it swallows bugs.. what would be expected to be caught there? 280 throw e; 281 } catch (NoTerminologyServiceException e) { 282 // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set 283 // that might fail too, but it might not, later. 284 return new ValueSetExpansionOutcome(new ValueSetCheckerSimple(source, factory, context), e.getMessage(), TerminologyServiceErrorClass.NOSERVICE); 285 } catch (Exception e) { 286 // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set 287 // that might fail too, but it might not, later. 288 return new ValueSetExpansionOutcome(new ValueSetCheckerSimple(source, factory, context), e.getMessage(), TerminologyServiceErrorClass.UNKNOWN); 289 } 290 } 291 292 private ExpansionProfile makeDefaultExpansion() { 293 ExpansionProfile res = new ExpansionProfile(); 294 res.setUrl("urn:uuid:" + UUID.randomUUID().toString().toLowerCase()); 295 res.setExcludeNested(true); 296 res.setIncludeDesignations(false); 297 return res; 298 } 299 300 private void addToHeirarchy(List<ValueSetExpansionContainsComponent> target, List<ValueSetExpansionContainsComponent> source) { 301 for (ValueSetExpansionContainsComponent s : source) { 302 target.add(s); 303 } 304 } 305 306 private String getCodeDisplay(CodeSystem cs, String code) throws TerminologyServiceException { 307 ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), code); 308 if (def == null) 309 throw new TerminologyServiceException("Unable to find code '" + code + "' in code system " + cs.getUrl()); 310 return def.getDisplay(); 311 } 312 313 private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> clist, String code) { 314 for (ConceptDefinitionComponent c : clist) { 315 if (code.equals(c.getCode())) 316 return c; 317 ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code); 318 if (v != null) 319 return v; 320 } 321 return null; 322 } 323 324 private void handleCompose(ValueSetComposeComponent compose, List<ValueSetExpansionParameterComponent> params, ExpansionProfile profile, String ctxt) 325 throws ETooCostly, FileNotFoundException, IOException, FHIRException { 326 compose.checkNoModifiers("ValueSet.compose", "expanding"); 327 // Exclude comes first because we build up a map of things to exclude 328 for (ConceptSetComponent inc : compose.getExclude()) 329 excludeCodes(inc, params, ctxt); 330 canBeHeirarchy = !profile.getExcludeNested() && excludeKeys.isEmpty() && excludeSystems.isEmpty(); 331 boolean first = true; 332 for (ConceptSetComponent inc : compose.getInclude()) { 333 if (first == true) 334 first = false; 335 else 336 canBeHeirarchy = false; 337 includeCodes(inc, params, profile); 338 } 339 340 } 341 342 private ValueSet importValueSet(String value, List<ValueSetExpansionParameterComponent> params, ExpansionProfile profile) 343 throws ETooCostly, TerminologyServiceException, FileNotFoundException, IOException, FHIRFormatError { 344 if (value == null) 345 throw new TerminologyServiceException("unable to find value set with no identity"); 346 ValueSet vs = context.fetchResource(ValueSet.class, value); 347 if (vs == null) 348 throw new TerminologyServiceException("Unable to find imported value set " + value); 349 ValueSetExpansionOutcome vso = factory.getExpander().expand(vs, profile); 350 if (vso.getError() != null) 351 throw new TerminologyServiceException("Unable to expand imported value set: " + vso.getError()); 352 if (vso.getService() != null) 353 throw new TerminologyServiceException("Unable to expand imported value set " + value); 354 if (vs.hasVersion()) 355 if (!existsInParams(params, "version", new UriType(vs.getUrl() + "|" + vs.getVersion()))) 356 params.add(new ValueSetExpansionParameterComponent().setName("version").setValue(new UriType(vs.getUrl() + "|" + vs.getVersion()))); 357 for (ValueSetExpansionParameterComponent p : vso.getValueset().getExpansion().getParameter()) { 358 if (!existsInParams(params, p.getName(), p.getValue())) 359 params.add(p); 360 } 361 canBeHeirarchy = false; // if we're importing a value set, we have to be combining, so we won't try for a heirarchy 362 return vso.getValueset(); 363 } 364 365 private void copyImportContains(List<ValueSetExpansionContainsComponent> list, ValueSetExpansionContainsComponent parent, ExpansionProfile profile, List<ValueSet> filter) throws FHIRException { 366 for (ValueSetExpansionContainsComponent c : list) { 367 c.checkNoModifiers("Imported Expansion in Code System", "expanding"); 368 ValueSetExpansionContainsComponent np = addCode(c.getSystem(), c.getCode(), c.getDisplay(), parent, null, profile, c.getAbstract(), c.getInactive(), filter); 369 copyImportContains(c.getContains(), np, profile, filter); 370 } 371 } 372 373 private void includeCodes(ConceptSetComponent inc, List<ValueSetExpansionParameterComponent> params, ExpansionProfile profile) throws ETooCostly, FileNotFoundException, IOException, FHIRException { 374 inc.checkNoModifiers("Compose.include", "expanding"); 375 List<ValueSet> imports = new ArrayList<ValueSet>(); 376 for (UriType imp : inc.getValueSet()) { 377 imports.add(importValueSet(imp.getValue(), params, profile)); 378 } 379 380 if (!inc.hasSystem()) { 381 if (imports.isEmpty()) // though this is not supposed to be the case 382 return; 383 ValueSet base = imports.get(0); 384 imports.remove(0); 385 base.checkNoModifiers("Imported ValueSet", "expanding"); 386 copyImportContains(base.getExpansion().getContains(), null, profile, imports); 387 } else { 388 CodeSystem cs = context.fetchCodeSystem(inc.getSystem()); 389 if ((cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) && context.supportsSystem(inc.getSystem())) { 390 addCodes(context.expandVS(inc, canBeHeirarchy), params, profile, imports); 391 return; 392 } 393 394 if (cs == null) { 395 if (context.isNoTerminologyServer()) 396 throw new NoTerminologyServiceException("unable to find code system " + inc.getSystem().toString()); 397 else 398 throw new TerminologyServiceException("unable to find code system " + inc.getSystem().toString()); 399 } 400 cs.checkNoModifiers("Code System", "expanding"); 401 if (cs.getContent() != CodeSystemContentMode.COMPLETE) 402 throw new TerminologyServiceException("Code system " + inc.getSystem().toString() + " is incomplete"); 403 if (cs.hasVersion()) 404 if (!existsInParams(params, "version", new UriType(cs.getUrl() + "|" + cs.getVersion()))) 405 params.add(new ValueSetExpansionParameterComponent().setName("version").setValue(new UriType(cs.getUrl() + "|" + cs.getVersion()))); 406 407 if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) { 408 // special case - add all the code system 409 for (ConceptDefinitionComponent def : cs.getConcept()) { 410 addCodeAndDescendents(cs, inc.getSystem(), def, null, profile, imports); 411 } 412 } 413 414 if (!inc.getConcept().isEmpty()) { 415 canBeHeirarchy = false; 416 for (ConceptReferenceComponent c : inc.getConcept()) { 417 c.checkNoModifiers("Code in Code System", "expanding"); 418 addCode(inc.getSystem(), c.getCode(), Utilities.noString(c.getDisplay()) ? getCodeDisplay(cs, c.getCode()) : c.getDisplay(), null, convertDesignations(c.getDesignation()), profile, false, 419 CodeSystemUtilities.isInactive(cs, c.getCode()), imports); 420 } 421 } 422 if (inc.getFilter().size() > 1) { 423 canBeHeirarchy = false; // which will bt the case if we get around to supporting this 424 throw new TerminologyServiceException("Multiple filters not handled yet"); // need to and them, and this isn't done yet. But this shouldn't arise in non loinc and snomed value sets 425 } 426 if (inc.getFilter().size() == 1) { 427 ConceptSetFilterComponent fc = inc.getFilter().get(0); 428 if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISA) { 429 // special: all codes in the target code system under the value 430 ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue()); 431 if (def == null) 432 throw new TerminologyServiceException("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'"); 433 addCodeAndDescendents(cs, inc.getSystem(), def, null, profile, imports); 434 } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.DESCENDENTOF) { 435 // special: all codes in the target code system under the value 436 ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue()); 437 if (def == null) 438 throw new TerminologyServiceException("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'"); 439 for (ConceptDefinitionComponent c : def.getConcept()) 440 addCodeAndDescendents(cs, inc.getSystem(), c, null, profile, imports); 441 } else if ("display".equals(fc.getProperty()) && fc.getOp() == FilterOperator.EQUAL) { 442 // gg; note: wtf is this: if the filter is display=v, look up the code 'v', and see if it's diplsay is 'v'? 443 canBeHeirarchy = false; 444 ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue()); 445 if (def != null) { 446 if (isNotBlank(def.getDisplay()) && isNotBlank(fc.getValue())) { 447 if (def.getDisplay().contains(fc.getValue())) { 448 addCode(inc.getSystem(), def.getCode(), def.getDisplay(), null, def.getDesignation(), profile, CodeSystemUtilities.isNotSelectable(cs, def), CodeSystemUtilities.isInactive(cs, def), 449 imports); 450 } 451 } 452 } 453 } else 454 throw new NotImplementedException("Search by property[" + fc.getProperty() + "] and op[" + fc.getOp() + "] is not supported yet"); 455 } 456 } 457 } 458 459 private List<ConceptDefinitionDesignationComponent> convertDesignations(List<ConceptReferenceDesignationComponent> list) { 460 List<ConceptDefinitionDesignationComponent> res = new ArrayList<CodeSystem.ConceptDefinitionDesignationComponent>(); 461 for (ConceptReferenceDesignationComponent t : list) { 462 ConceptDefinitionDesignationComponent c = new ConceptDefinitionDesignationComponent(); 463 c.setLanguage(t.getLanguage()); 464 c.setUse(t.getUse()); 465 c.setValue(t.getValue()); 466 } 467 return res; 468 } 469 470 private String key(String uri, String code) { 471 return "{" + uri + "}" + code; 472 } 473 474 private String key(ValueSetExpansionContainsComponent c) { 475 return key(c.getSystem(), c.getCode()); 476 } 477 478}