001package org.hl7.fhir.r5.terminologies; 002 003/* 004 Copyright (c) 2011+, HL7, Inc. 005 All rights reserved. 006 007 Redistribution and use in source and binary forms, with or without modification, 008 are permitted provided that the following conditions are met: 009 010 * Redistributions of source code must retain the above copyright notice, this 011 list of conditions and the following disclaimer. 012 * Redistributions in binary form must reproduce the above copyright notice, 013 this list of conditions and the following disclaimer in the documentation 014 and/or other materials provided with the distribution. 015 * Neither the name of HL7 nor the names of its contributors may be used to 016 endorse or promote products derived from this software without specific 017 prior written permission. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 022 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 024 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 025 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 026 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 027 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 028 POSSIBILITY OF SUCH DAMAGE. 029 030 */ 031 032 033 034import static org.apache.commons.lang3.StringUtils.isNotBlank; 035 036import java.io.FileNotFoundException; 037import java.io.IOException; 038 039/* 040 * Copyright (c) 2011+, HL7, Inc 041 * All rights reserved. 042 * 043 * Redistribution and use in source and binary forms, with or without modification, 044 * are permitted provided that the following conditions are met: 045 * 046 * Redistributions of source code must retain the above copyright notice, this 047 * list of conditions and the following disclaimer. 048 * Redistributions in binary form must reproduce the above copyright notice, 049 * this list of conditions and the following disclaimer in the documentation 050 * and/or other materials provided with the distribution. 051 * Neither the name of HL7 nor the names of its contributors may be used to 052 * endorse or promote products derived from this software without specific 053 * prior written permission. 054 * 055 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 056 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 057 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 058 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 059 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 060 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 061 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 062 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 063 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 064 * POSSIBILITY OF SUCH DAMAGE. 065 * 066 */ 067 068import java.util.ArrayList; 069import java.util.Calendar; 070import java.util.Collection; 071import java.util.GregorianCalendar; 072import java.util.HashMap; 073import java.util.HashSet; 074import java.util.List; 075import java.util.Map; 076import java.util.Set; 077 078import org.apache.commons.lang3.NotImplementedException; 079import org.hl7.fhir.exceptions.FHIRException; 080import org.hl7.fhir.exceptions.FHIRFormatError; 081import org.hl7.fhir.exceptions.NoTerminologyServiceException; 082import org.hl7.fhir.exceptions.TerminologyServiceException; 083import org.hl7.fhir.r5.context.IWorkerContext; 084import org.hl7.fhir.r5.model.BooleanType; 085import org.hl7.fhir.r5.model.CodeSystem; 086import org.hl7.fhir.r5.model.CodeSystem.CodeSystemContentMode; 087import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 088import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent; 089import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; 090import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent; 091import org.hl7.fhir.r5.model.CodeSystem.PropertyType; 092import org.hl7.fhir.r5.model.DataType; 093import org.hl7.fhir.r5.model.DateTimeType; 094import org.hl7.fhir.r5.model.Enumerations.FilterOperator; 095import org.hl7.fhir.r5.model.Extension; 096import org.hl7.fhir.r5.model.Factory; 097import org.hl7.fhir.r5.model.PackageInformation; 098import org.hl7.fhir.r5.model.Parameters; 099import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; 100import org.hl7.fhir.r5.model.PrimitiveType; 101import org.hl7.fhir.r5.model.UriType; 102import org.hl7.fhir.r5.model.ValueSet; 103import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; 104import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent; 105import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 106import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 107import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent; 108import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent; 109import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 110import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent; 111import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent; 112import org.hl7.fhir.r5.utils.ToolingExtensions; 113import org.hl7.fhir.utilities.Utilities; 114 115import com.google.errorprone.annotations.NoAllocation; 116 117public class ValueSetExpanderSimple extends ValueSetWorker implements ValueSetExpander { 118 119 public class PropertyFilter implements IConceptFilter { 120 121 private ConceptSetFilterComponent filter; 122 private PropertyComponent property; 123 124 public PropertyFilter(ConceptSetFilterComponent fc, PropertyComponent propertyDefinition) { 125 this.filter = fc; 126 this.property = propertyDefinition; 127 } 128 129 @Override 130 public boolean includeConcept(CodeSystem cs, ConceptDefinitionComponent def) { 131 ConceptPropertyComponent pc = getPropertyForConcept(def); 132 if (pc != null) { 133 String v = pc.getValue().isPrimitive() ? pc.getValue().primitiveValue() : null; 134 switch (filter.getOp()) { 135 case DESCENDENTOF: throw fail("not supported yet: "+filter.getOp().toCode()); 136 case EQUAL: return filter.getValue().equals(v); 137 case EXISTS: throw fail("not supported yet: "+filter.getOp().toCode()); 138 case GENERALIZES: throw fail("not supported yet: "+filter.getOp().toCode()); 139 case IN: throw fail("not supported yet: "+filter.getOp().toCode()); 140 case ISA: throw fail("not supported yet: "+filter.getOp().toCode()); 141 case ISNOTA: throw fail("not supported yet: "+filter.getOp().toCode()); 142 case NOTIN: throw fail("not supported yet: "+filter.getOp().toCode()); 143 case NULL: throw fail("not supported yet: "+filter.getOp().toCode()); 144 case REGEX: throw fail("not supported yet: "+filter.getOp().toCode()); 145 default: 146 throw fail("Shouldn't get here"); 147 } 148 } else if (property.getType() == PropertyType.BOOLEAN && filter.getOp() == FilterOperator.EQUAL) { 149 return "false".equals(filter.getValue()); 150 } else { 151 return false; 152 } 153 } 154 155 private ConceptPropertyComponent getPropertyForConcept(ConceptDefinitionComponent def) { 156 for (ConceptPropertyComponent pc : def.getProperty()) { 157 if (pc.getCode().equals(property.getCode())) { 158 return pc; 159 } 160 } 161 return null; 162 } 163 164 } 165 166 public class AllConceptsFilter implements IConceptFilter { 167 168 @Override 169 public boolean includeConcept(CodeSystem cs, ConceptDefinitionComponent def) { 170 return true; 171 } 172 } 173 174 public interface IConceptFilter { 175 176 boolean includeConcept(CodeSystem cs, ConceptDefinitionComponent def); 177 178 } 179 180 private List<ValueSetExpansionContainsComponent> codes = new ArrayList<ValueSet.ValueSetExpansionContainsComponent>(); 181 private List<ValueSetExpansionContainsComponent> roots = new ArrayList<ValueSet.ValueSetExpansionContainsComponent>(); 182 private Map<String, ValueSetExpansionContainsComponent> map = new HashMap<String, ValueSet.ValueSetExpansionContainsComponent>(); 183 private IWorkerContext context; 184 private boolean canBeHeirarchy = true; 185 private boolean includeAbstract = true; 186 private Set<String> excludeKeys = new HashSet<String>(); 187 private Set<String> excludeSystems = new HashSet<String>(); 188 private ValueSet focus; 189 private int maxExpansionSize = 500; 190 private List<String> allErrors = new ArrayList<>(); 191 192 private int total; 193 private boolean checkCodesWhenExpanding; 194 195 public ValueSetExpanderSimple(IWorkerContext context) { 196 super(); 197 this.context = context; 198 } 199 200 public ValueSetExpanderSimple(IWorkerContext context, List<String> allErrors) { 201 super(); 202 this.context = context; 203 this.allErrors = allErrors; 204 } 205 206 public void setMaxExpansionSize(int theMaxExpansionSize) { 207 maxExpansionSize = theMaxExpansionSize; 208 } 209 210 private ValueSetExpansionContainsComponent addCode(String system, String code, String display, ValueSetExpansionContainsComponent parent, List<ConceptDefinitionDesignationComponent> designations, Parameters expParams, 211 boolean isAbstract, boolean inactive, List<ValueSet> filters, boolean noInactive, boolean deprecated, List<ValueSetExpansionPropertyComponent> vsProp) { 212 213 if (filters != null && !filters.isEmpty() && !filterContainsCode(filters, system, code)) 214 return null; 215 if (noInactive && inactive) { 216 return null; 217 } 218 219 ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent(); 220 n.setSystem(system); 221 n.setCode(code); 222 if (isAbstract) 223 n.setAbstract(true); 224 if (inactive) 225 n.setInactive(true); 226 if (deprecated) { 227 ValueSetUtilities.setDeprecated(vsProp, n); 228 } 229 230 if (expParams.getParameterBool("includeDesignations") && designations != null) { 231 for (ConceptDefinitionDesignationComponent t : designations) { 232 if (t.getLanguage() != null || t.getValue() != null) { 233 ConceptReferenceDesignationComponent d = n.addDesignation(); 234 if (t.getLanguage() != null) { 235 d.setLanguage(t.getLanguage().trim()); 236 } 237 if (t.getValue() != null) { 238 d.setValue(t.getValue().trim()); 239 } 240 } 241 } 242 } 243 ConceptDefinitionDesignationComponent t = expParams.hasLanguage() ? getMatchingLang(designations, expParams.getLanguage()) : null; 244 if (t == null) 245 n.setDisplay(display); 246 else 247 n.setDisplay(t.getValue()); 248 249 String s = key(n); 250 if (map.containsKey(s) || excludeKeys.contains(s)) { 251 canBeHeirarchy = false; 252 } else { 253 codes.add(n); 254 map.put(s, n); 255 total++; 256 } 257 if (canBeHeirarchy && parent != null) { 258 parent.getContains().add(n); 259 } else { 260 roots.add(n); 261 } 262 return n; 263 } 264 265 private boolean filterContainsCode(List<ValueSet> filters, String system, String code) { 266 for (ValueSet vse : filters) 267 if (expansionContainsCode(vse.getExpansion().getContains(), system, code)) 268 return true; 269 return false; 270 } 271 272 private boolean expansionContainsCode(List<ValueSetExpansionContainsComponent> contains, String system, String code) { 273 for (ValueSetExpansionContainsComponent cc : contains) { 274 if (system.equals(cc.getSystem()) && code.equals(cc.getCode())) 275 return true; 276 if (expansionContainsCode(cc.getContains(), system, code)) 277 return true; 278 } 279 return false; 280 } 281 282 private ConceptDefinitionDesignationComponent getMatchingLang(List<ConceptDefinitionDesignationComponent> list, String lang) { 283 for (ConceptDefinitionDesignationComponent t : list) 284 if (t.getLanguage().equals(lang)) 285 return t; 286 for (ConceptDefinitionDesignationComponent t : list) 287 if (t.getLanguage().startsWith(lang)) 288 return t; 289 return null; 290 } 291 292 private void addCodeAndDescendents(ValueSetExpansionContainsComponent focus, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filters, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps) throws FHIRException { 293 focus.checkNoModifiers("Expansion.contains", "expanding"); 294 ValueSetExpansionContainsComponent np = addCode(focus.getSystem(), focus.getCode(), focus.getDisplay(), parent, 295 convert(focus.getDesignation()), expParams, focus.getAbstract(), focus.getInactive(), filters, noInactive, false, vsProps); 296 for (ValueSetExpansionContainsComponent c : focus.getContains()) 297 addCodeAndDescendents(focus, np, expParams, filters, noInactive, vsProps); 298 } 299 300 private List<ConceptDefinitionDesignationComponent> convert(List<ConceptReferenceDesignationComponent> designations) { 301 List<ConceptDefinitionDesignationComponent> list = new ArrayList<ConceptDefinitionDesignationComponent>(); 302 for (ConceptReferenceDesignationComponent d : designations) { 303 ConceptDefinitionDesignationComponent n = new ConceptDefinitionDesignationComponent(); 304 n.setLanguage(d.getLanguage()); 305 n.setUse(d.getUse()); 306 n.setValue(d.getValue()); 307 list.add(n); 308 } 309 return list; 310 } 311 312 private void addCodeAndDescendents(CodeSystem cs, String system, ConceptDefinitionComponent def, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filters, ConceptDefinitionComponent exclusion, IConceptFilter filterFunc, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps) throws FHIRException { 313 def.checkNoModifiers("Code in Code System", "expanding"); 314 if (exclusion != null) { 315 if (exclusion.getCode().equals(def.getCode())) 316 return; // excluded. 317 } 318 ValueSetExpansionContainsComponent np = null; 319 boolean abs = CodeSystemUtilities.isNotSelectable(cs, def); 320 boolean inc = CodeSystemUtilities.isInactive(cs, def); 321 boolean dep = CodeSystemUtilities.isDeprecated(cs, def, false); 322 if ((includeAbstract || !abs) && filterFunc.includeConcept(cs, def)) { 323 np = addCode(system, def.getCode(), def.getDisplay(), parent, def.getDesignation(), expParams, abs, inc, filters, noInactive, dep, vsProps); 324 } 325 for (ConceptDefinitionComponent c : def.getConcept()) { 326 addCodeAndDescendents(cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps); 327 } 328 if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { 329 List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); 330 for (ConceptDefinitionComponent c : children) 331 addCodeAndDescendents(cs, system, c, np, expParams, filters, exclusion, filterFunc, noInactive, vsProps); 332 } 333 334 } 335 336 private void addCodes(ValueSetExpansionComponent expand, List<ValueSetExpansionParameterComponent> params, Parameters expParams, List<ValueSet> filters, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps) throws ETooCostly, FHIRException { 337 if (expand != null) { 338 if (expand.getContains().size() > maxExpansionSize) 339 throw failCostly("Too many codes to display (>" + Integer.toString(expand.getContains().size()) + ")"); 340 for (ValueSetExpansionParameterComponent p : expand.getParameter()) { 341 if (!existsInParams(params, p.getName(), p.getValue())) 342 params.add(p); 343 } 344 345 copyImportContains(expand.getContains(), null, expParams, filters, noInactive, vsProps); 346 } 347 } 348 349 private void excludeCode(String theSystem, String theCode) { 350 ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent(); 351 n.setSystem(theSystem); 352 n.setCode(theCode); 353 String s = key(n); 354 excludeKeys.add(s); 355 } 356 357 private void excludeCodes(ConceptSetComponent exc, List<ValueSetExpansionParameterComponent> params, String ctxt) throws FHIRException { 358 exc.checkNoModifiers("Compose.exclude", "expanding"); 359 if (exc.hasSystem() && exc.getConcept().size() == 0 && exc.getFilter().size() == 0) { 360 excludeSystems.add(exc.getSystem()); 361 } 362 363 if (exc.hasValueSet()) 364 throw fail("Processing Value set references in exclude is not yet done in "+ctxt); 365 // importValueSet(imp.getValue(), params, expParams); 366 367 CodeSystem cs = context.fetchCodeSystem(exc.getSystem()); 368 if ((cs == null || cs.getContent() != CodeSystemContentMode.COMPLETE) && context.supportsSystem(exc.getSystem())) { 369 ValueSetExpansionOutcome vse = context.expandVS(exc, false, false); 370 ValueSet valueset = vse.getValueset(); 371 if (valueset == null) 372 throw failTSE("Error Expanding ValueSet: "+vse.getError()); 373 excludeCodes(valueset.getExpansion(), params); 374 return; 375 } 376 377 for (ConceptReferenceComponent c : exc.getConcept()) { 378 excludeCode(exc.getSystem(), c.getCode()); 379 } 380 381 if (exc.getFilter().size() > 0) 382 throw fail("not done yet - multiple filters"); 383 } 384 385 386 387 private void excludeCodes(ValueSetExpansionComponent expand, List<ValueSetExpansionParameterComponent> params) { 388 for (ValueSetExpansionContainsComponent c : expand.getContains()) { 389 excludeCode(c.getSystem(), c.getCode()); 390 } 391 } 392 393 private boolean existsInParams(List<ValueSetExpansionParameterComponent> params, String name, DataType value) { 394 for (ValueSetExpansionParameterComponent p : params) { 395 if (p.getName().equals(name) && PrimitiveType.compareDeep(p.getValue(), value, false)) 396 return true; 397 } 398 return false; 399 } 400 401 @Override 402 public ValueSetExpansionOutcome expand(ValueSet source, Parameters expParams) { 403 allErrors.clear(); 404 try { 405 return expandInternal(source, expParams); 406 } catch (NoTerminologyServiceException e) { 407 // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set 408 // that might fail too, but it might not, later. 409 return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.NOSERVICE, allErrors); 410 } catch (Exception e) { 411 // well, we couldn't expand, so we'll return an interface to a checker that can check membership of the set 412 // that might fail too, but it might not, later. 413 return new ValueSetExpansionOutcome(e.getMessage(), TerminologyServiceErrorClass.UNKNOWN, allErrors); 414 } 415 } 416 417 public ValueSetExpansionOutcome expandInternal(ValueSet source, Parameters expParams) throws FHIRException, FileNotFoundException, ETooCostly, IOException { 418 return doExpand(source, expParams); 419 } 420 421 public ValueSetExpansionOutcome doExpand(ValueSet source, Parameters expParams) throws FHIRException, ETooCostly, FileNotFoundException, IOException { 422 if (expParams == null) 423 expParams = makeDefaultExpansion(); 424 source.checkNoModifiers("ValueSet", "expanding"); 425 focus = source.copy(); 426 focus.setExpansion(new ValueSet.ValueSetExpansionComponent()); 427 focus.getExpansion().setTimestampElement(DateTimeType.now()); 428 focus.getExpansion().setIdentifier(Factory.createUUID()); 429 for (ParametersParameterComponent p : expParams.getParameter()) { 430 if (Utilities.existsInList(p.getName(), "includeDesignations", "excludeNested", "activeOnly")) 431 focus.getExpansion().addParameter().setName(p.getName()).setValue(p.getValue()); 432 } 433 434 if (source.hasCompose()) { 435 handleCompose(source.getCompose(), focus.getExpansion(), expParams, source.getUrl(), focus.getExpansion().getExtension(), source); 436 } 437 438 if (canBeHeirarchy) { 439 for (ValueSetExpansionContainsComponent c : roots) { 440 focus.getExpansion().getContains().add(c); 441 } 442 } else { 443 for (ValueSetExpansionContainsComponent c : codes) { 444 if (map.containsKey(key(c)) && (includeAbstract || !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 445 focus.getExpansion().getContains().add(c); 446 c.getContains().clear(); // make sure any heirarchy is wiped 447 } 448 } 449 } 450 451 if (total > 0) { 452 focus.getExpansion().setTotal(total); 453 } 454 455 return new ValueSetExpansionOutcome(focus); 456 } 457 458 private Parameters makeDefaultExpansion() { 459 Parameters res = new Parameters(); 460 res.addParameter("excludeNested", true); 461 res.addParameter("includeDesignations", false); 462 return res; 463 } 464 465 private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> clist, String code) { 466 for (ConceptDefinitionComponent c : clist) { 467 if (code.equals(c.getCode())) 468 return c; 469 ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code); 470 if (v != null) 471 return v; 472 } 473 return null; 474 } 475 476 private void handleCompose(ValueSetComposeComponent compose, ValueSetExpansionComponent exp, Parameters expParams, String ctxt, List<Extension> extensions, ValueSet valueSet) 477 throws ETooCostly, FileNotFoundException, IOException, FHIRException { 478 compose.checkNoModifiers("ValueSet.compose", "expanding"); 479 // Exclude comes first because we build up a map of things to exclude 480 for (ConceptSetComponent inc : compose.getExclude()) 481 excludeCodes(inc, exp.getParameter(), ctxt); 482 canBeHeirarchy = !expParams.getParameterBool("excludeNested") && excludeKeys.isEmpty() && excludeSystems.isEmpty(); 483 includeAbstract = !expParams.getParameterBool("excludeNotForUI"); 484 boolean first = true; 485 for (ConceptSetComponent inc : compose.getInclude()) { 486 if (first == true) 487 first = false; 488 else 489 canBeHeirarchy = false; 490 includeCodes(inc, exp, expParams, canBeHeirarchy, compose.hasInactive() ? !compose.getInactive() : checkNoInActiveFromParam(expParams), extensions, valueSet); 491 } 492 } 493 494 /** 495 * returns true if activeOnly = true 496 * @param expParams 497 * @return 498 */ 499 private boolean checkNoInActiveFromParam(Parameters expParams) { 500 for (ParametersParameterComponent p : expParams.getParameter()) { 501 if (p.getName().equals("activeOnly")) { 502 return p.getValueBooleanType().getValue(); 503 } 504 } 505 return false; 506 } 507 508 private ValueSet importValueSet(String value, ValueSetExpansionComponent exp, Parameters expParams, boolean noInactive, ValueSet valueSet) throws ETooCostly, TerminologyServiceException, FileNotFoundException, IOException, FHIRFormatError { 509 if (value == null) 510 throw fail("unable to find value set with no identity"); 511 ValueSet vs = context.fetchResource(ValueSet.class, value, valueSet); 512 if (vs == null) { 513 if (context.fetchResource(CodeSystem.class, value, valueSet) != null) { 514 throw fail("Cannot include value set "+value+" because it's actually a code system"); 515 } else { 516 throw fail("Unable to find imported value set " + value); 517 } 518 } 519 if (noInactive) { 520 expParams = expParams.copy(); 521 expParams.addParameter("activeOnly", true); 522 } 523 ValueSetExpansionOutcome vso = new ValueSetExpanderSimple(context, allErrors).expand(vs, expParams); 524 if (vso.getError() != null) { 525 addErrors(vso.getAllErrors()); 526 throw fail("Unable to expand imported value set "+vs.getUrl()+": " + vso.getError()); 527 } 528 if (vs.hasVersion()) 529 if (!existsInParams(exp.getParameter(), "version", new UriType(vs.getUrl() + "|" + vs.getVersion()))) 530 exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("version").setValue(new UriType(vs.getUrl() + "|" + vs.getVersion()))); 531 for (Extension ex : vso.getValueset().getExpansion().getExtension()) { 532 if (ex.getUrl().equals(ToolingExtensions.EXT_EXP_TOOCOSTLY)) { 533 if (ex.getValue() instanceof BooleanType) { 534 exp.getExtension().add(new Extension(ToolingExtensions.EXT_EXP_TOOCOSTLY).setValue(new UriType(value))); 535 } else { 536 exp.getExtension().add(ex); 537 } 538 } 539 } 540 for (ValueSetExpansionParameterComponent p : vso.getValueset().getExpansion().getParameter()) { 541 if (!existsInParams(exp.getParameter(), p.getName(), p.getValue())) 542 exp.getParameter().add(p); 543 } 544 if (isValueSetUnionImports(valueSet)) { 545 copyExpansion(vso.getValueset().getExpansion().getContains()); 546 } 547 canBeHeirarchy = false; // if we're importing a value set, we have to be combining, so we won't try for a heirarchy 548 return vso.getValueset(); 549 } 550 551 552 protected boolean isValueSetUnionImports(ValueSet valueSet) { 553 PackageInformation p = valueSet.getSourcePackage(); 554 if (p != null) { 555 return p.getDate().before(new GregorianCalendar(2022, Calendar.MARCH, 31).getTime()); 556 } else { 557 return false; 558 } 559 } 560 561 public void copyExpansion(List<ValueSetExpansionContainsComponent> list) { 562 for (ValueSetExpansionContainsComponent cc : list) { 563 ValueSetExpansionContainsComponent n = new ValueSet.ValueSetExpansionContainsComponent(); 564 n.setSystem(cc.getSystem()); 565 n.setCode(cc.getCode()); 566 n.setAbstract(cc.getAbstract()); 567 n.setInactive(cc.getInactive()); 568 n.setDisplay(cc.getDisplay()); 569 n.getDesignation().addAll(cc.getDesignation()); 570 571 String s = key(n); 572 if (!map.containsKey(s) && !excludeKeys.contains(s)) { 573 codes.add(n); 574 map.put(s, n); 575 total++; 576 } 577 copyExpansion(cc.getContains()); 578 } 579 } 580 581 private void addErrors(List<String> errs) { 582 for (String s : errs) { 583 if (!allErrors.contains(s)) { 584 allErrors.add(s); 585 } 586 } 587 } 588 589 private void copyImportContains(List<ValueSetExpansionContainsComponent> list, ValueSetExpansionContainsComponent parent, Parameters expParams, List<ValueSet> filter, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps) throws FHIRException { 590 for (ValueSetExpansionContainsComponent c : list) { 591 c.checkNoModifiers("Imported Expansion in Code System", "expanding"); 592 ValueSetExpansionContainsComponent np = addCode(c.getSystem(), c.getCode(), c.getDisplay(), parent, null, expParams, c.getAbstract(), c.getInactive(), filter, noInactive, false, vsProps); 593 copyImportContains(c.getContains(), np, expParams, filter, noInactive, vsProps); 594 } 595 } 596 597 private void includeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, boolean heirarchical, boolean noInactive, List<Extension> extensions, ValueSet valueSet) throws ETooCostly, FileNotFoundException, IOException, FHIRException { 598 inc.checkNoModifiers("Compose.include", "expanding"); 599 List<ValueSet> imports = new ArrayList<ValueSet>(); 600 for (UriType imp : inc.getValueSet()) { 601 imports.add(importValueSet(imp.getValue(), exp, expParams, noInactive, valueSet)); 602 } 603 604 if (!inc.hasSystem()) { 605 if (imports.isEmpty()) // though this is not supposed to be the case 606 return; 607 ValueSet base = imports.get(0); 608 imports.remove(0); 609 base.checkNoModifiers("Imported ValueSet", "expanding"); 610 copyImportContains(base.getExpansion().getContains(), null, expParams, imports, noInactive, base.getExpansion().getProperty()); 611 } else { 612 CodeSystem cs = context.fetchCodeSystem(inc.getSystem()); 613 if (isServerSide(inc.getSystem()) || (cs == null || (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT))) { 614 doServerIncludeCodes(inc, heirarchical, exp, imports, expParams, extensions, noInactive, valueSet.getExpansion().getProperty()); 615 } else { 616 doInternalIncludeCodes(inc, exp, expParams, imports, cs, noInactive); 617 } 618 } 619 } 620 621 private void doServerIncludeCodes(ConceptSetComponent inc, boolean heirarchical, ValueSetExpansionComponent exp, List<ValueSet> imports, Parameters expParams, List<Extension> extensions, boolean noInactive, List<ValueSetExpansionPropertyComponent> vsProps) throws FHIRException { 622 ValueSetExpansionOutcome vso = context.expandVS(inc, heirarchical, noInactive); 623 if (vso.getError() != null) { 624 throw failTSE("Unable to expand imported value set: " + vso.getError()); 625 } 626 ValueSet vs = vso.getValueset(); 627 if (vs.hasVersion()) { 628 if (!existsInParams(exp.getParameter(), "version", new UriType(vs.getUrl() + "|" + vs.getVersion()))) { 629 exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("version").setValue(new UriType(vs.getUrl() + "|" + vs.getVersion()))); 630 } 631 } 632 for (ValueSetExpansionParameterComponent p : vso.getValueset().getExpansion().getParameter()) { 633 if (!existsInParams(exp.getParameter(), p.getName(), p.getValue())) { 634 exp.getParameter().add(p); 635 } 636 } 637 for (Extension ex : vs.getExpansion().getExtension()) { 638 if (Utilities.existsInList(ex.getUrl(), ToolingExtensions.EXT_EXP_TOOCOSTLY, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed")) { 639 if (!hasExtension(extensions, ex.getUrl())) { 640 extensions.add(ex); 641 } 642 } 643 } 644 for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) { 645 addCodeAndDescendents(cc, null, expParams, imports, noInactive, vsProps); 646 } 647 } 648 649 private boolean hasExtension(List<Extension> extensions, String url) { 650 for (Extension ex : extensions) { 651 if (ex.getUrl().equals(url)) { 652 return true; 653 } 654 } 655 return false; 656 } 657 658 public void doInternalIncludeCodes(ConceptSetComponent inc, ValueSetExpansionComponent exp, Parameters expParams, List<ValueSet> imports, CodeSystem cs, boolean noInactive) throws NoTerminologyServiceException, TerminologyServiceException, FHIRException { 659 if (cs == null) { 660 if (context.isNoTerminologyServer()) 661 throw failTSE("Unable to find code system " + inc.getSystem().toString()); 662 else 663 throw failTSE("Unable to find code system " + inc.getSystem().toString()); 664 } 665 cs.checkNoModifiers("Code System", "expanding"); 666 if (cs.getContent() != CodeSystemContentMode.COMPLETE && cs.getContent() != CodeSystemContentMode.FRAGMENT) 667 throw failTSE("Code system " + inc.getSystem().toString() + " is incomplete"); 668 if (cs.hasVersion()) 669 if (!existsInParams(exp.getParameter(), "version", new UriType(cs.getUrl() + "|" + cs.getVersion()))) 670 exp.getParameter().add(new ValueSetExpansionParameterComponent().setName("version").setValue(new UriType(cs.getUrl() + "|" + cs.getVersion()))); 671 672 if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) { 673 // special case - add all the code system 674 for (ConceptDefinitionComponent def : cs.getConcept()) { 675 addCodeAndDescendents(cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(), noInactive, exp.getProperty()); 676 } 677 if (cs.getContent() == CodeSystemContentMode.FRAGMENT) { 678 addFragmentWarning(exp, cs); 679 } 680 if (cs.getContent() == CodeSystemContentMode.EXAMPLE) { 681 addExampleWarning(exp, cs); 682 } 683 } 684 685 if (!inc.getConcept().isEmpty()) { 686 canBeHeirarchy = false; 687 for (ConceptReferenceComponent c : inc.getConcept()) { 688 c.checkNoModifiers("Code in Code System", "expanding"); 689 ConceptDefinitionComponent def = CodeSystemUtilities.findCode(cs.getConcept(), c.getCode()); 690 Boolean inactive = false; // default is true if we're a fragment and 691 if (def == null) { 692 if (cs.getContent() == CodeSystemContentMode.FRAGMENT) { 693 addFragmentWarning(exp, cs); 694 } else if (cs.getContent() == CodeSystemContentMode.EXAMPLE) { 695 addExampleWarning(exp, cs); 696 } else { 697 if (checkCodesWhenExpanding) { 698 throw failTSE("Unable to find code '" + c.getCode() + "' in code system " + cs.getUrl()); 699 } 700 } 701 } else { 702 inactive = CodeSystemUtilities.isInactive(cs, def); 703 } 704 addCode(inc.getSystem(), c.getCode(), !Utilities.noString(c.getDisplay()) ? c.getDisplay() : def == null ? null : def.getDisplay(), null, convertDesignations(c.getDesignation()), expParams, false, inactive, imports, noInactive, false, exp.getProperty()); 705 } 706 } 707 if (inc.getFilter().size() > 1) { 708 canBeHeirarchy = false; // which will bt the case if we get around to supporting this 709 throw failTSE("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 710 } 711 if (inc.getFilter().size() == 1) { 712 if (cs.getContent() == CodeSystemContentMode.FRAGMENT) { 713 addFragmentWarning(exp, cs); 714 } 715 ConceptSetFilterComponent fc = inc.getFilter().get(0); 716 if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISA) { 717 // special: all codes in the target code system under the value 718 ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue()); 719 if (def == null) 720 throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'"); 721 addCodeAndDescendents(cs, inc.getSystem(), def, null, expParams, imports, null, new AllConceptsFilter(), noInactive, exp.getProperty()); 722 } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.ISNOTA) { 723 // special: all codes in the target code system that are not under the value 724 ConceptDefinitionComponent defEx = getConceptForCode(cs.getConcept(), fc.getValue()); 725 if (defEx == null) 726 throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'"); 727 for (ConceptDefinitionComponent def : cs.getConcept()) { 728 addCodeAndDescendents(cs, inc.getSystem(), def, null, expParams, imports, defEx, new AllConceptsFilter(), noInactive, exp.getProperty()); 729 } 730 } else if ("concept".equals(fc.getProperty()) && fc.getOp() == FilterOperator.DESCENDENTOF) { 731 // special: all codes in the target code system under the value 732 ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue()); 733 if (def == null) 734 throw failTSE("Code '" + fc.getValue() + "' not found in system '" + inc.getSystem() + "'"); 735 for (ConceptDefinitionComponent c : def.getConcept()) 736 addCodeAndDescendents(cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(), noInactive, exp.getProperty()); 737 if (def.hasUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK)) { 738 List<ConceptDefinitionComponent> children = (List<ConceptDefinitionComponent>) def.getUserData(CodeSystemUtilities.USER_DATA_CROSS_LINK); 739 for (ConceptDefinitionComponent c : children) 740 addCodeAndDescendents(cs, inc.getSystem(), c, null, expParams, imports, null, new AllConceptsFilter(), noInactive, exp.getProperty()); 741 } 742 743 } else if ("display".equals(fc.getProperty()) && fc.getOp() == FilterOperator.EQUAL) { 744 // gg; note: wtf is this: if the filter is display=v, look up the code 'v', and see if it's diplsay is 'v'? 745 canBeHeirarchy = false; 746 ConceptDefinitionComponent def = getConceptForCode(cs.getConcept(), fc.getValue()); 747 if (def != null) { 748 if (isNotBlank(def.getDisplay()) && isNotBlank(fc.getValue())) { 749 if (def.getDisplay().contains(fc.getValue())) { 750 addCode(inc.getSystem(), def.getCode(), def.getDisplay(), null, def.getDesignation(), expParams, CodeSystemUtilities.isNotSelectable(cs, def), CodeSystemUtilities.isInactive(cs, def), 751 imports, noInactive, false, exp.getProperty()); 752 } 753 } 754 } 755 } else if (isDefinedProperty(cs, fc.getProperty())) { 756 for (ConceptDefinitionComponent def : cs.getConcept()) { 757 addCodeAndDescendents(cs, inc.getSystem(), def, null, expParams, imports, null, new PropertyFilter(fc, getPropertyDefinition(cs, fc.getProperty())), noInactive, exp.getProperty()); 758 } 759 } else { 760 throw fail("Search by property[" + fc.getProperty() + "] and op[" + fc.getOp() + "] is not supported yet"); 761 } 762 } 763 } 764 765 private PropertyComponent getPropertyDefinition(CodeSystem cs, String property) { 766 for (PropertyComponent cp : cs.getProperty()) { 767 if (cp.getCode().equals(property)) { 768 return cp; 769 } 770 } 771 return null; 772 } 773 774 private boolean isDefinedProperty(CodeSystem cs, String property) { 775 for (PropertyComponent cp : cs.getProperty()) { 776 if (cp.getCode().equals(property)) { 777 return true; 778 } 779 } 780 return false; 781 } 782 783 private void addFragmentWarning(ValueSetExpansionComponent exp, CodeSystem cs) { 784 String url = cs.getVersionedUrl(); 785 for (ValueSetExpansionParameterComponent p : exp.getParameter()) { 786 if ("fragment".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 787 return; 788 } 789 } 790 exp.addParameter().setName("fragment").setValue(new UriType(url)); 791 } 792 793 private void addExampleWarning(ValueSetExpansionComponent exp, CodeSystem cs) { 794 String url = cs.getVersionedUrl(); 795 for (ValueSetExpansionParameterComponent p : exp.getParameter()) { 796 if ("example".equals(p.getName()) && p.hasValueUriType() && url.equals(p.getValue().primitiveValue())) { 797 return; 798 } 799 } 800 exp.addParameter().setName("example").setValue(new UriType(url)); 801 } 802 803 private List<ConceptDefinitionDesignationComponent> convertDesignations(List<ConceptReferenceDesignationComponent> list) { 804 List<ConceptDefinitionDesignationComponent> res = new ArrayList<CodeSystem.ConceptDefinitionDesignationComponent>(); 805 for (ConceptReferenceDesignationComponent t : list) { 806 ConceptDefinitionDesignationComponent c = new ConceptDefinitionDesignationComponent(); 807 c.setLanguage(t.getLanguage()); 808 c.setUse(t.getUse()); 809 c.setValue(t.getValue()); 810 } 811 return res; 812 } 813 814 private String key(String uri, String code) { 815 return "{" + uri + "}" + code; 816 } 817 818 private String key(ValueSetExpansionContainsComponent c) { 819 return key(c.getSystem(), c.getCode()); 820 } 821 822 private FHIRException fail(String msg) { 823 allErrors.add(msg); 824 return new FHIRException(msg); 825 } 826 827 private ETooCostly failCostly(String msg) { 828 allErrors.add(msg); 829 return new ETooCostly(msg); 830 } 831 832 private TerminologyServiceException failTSE(String msg) { 833 allErrors.add(msg); 834 return new TerminologyServiceException(msg); 835 } 836 837 public Collection<? extends String> getAllErrors() { 838 return allErrors; 839 } 840 841 public boolean isCheckCodesWhenExpanding() { 842 return checkCodesWhenExpanding; 843 } 844 845 public void setCheckCodesWhenExpanding(boolean checkCodesWhenExpanding) { 846 this.checkCodesWhenExpanding = checkCodesWhenExpanding; 847 } 848 849}