001package org.hl7.fhir.r4b.renderers; 002 003import java.io.BufferedWriter; 004import java.io.FileWriter; 005import java.io.IOException; 006import java.text.ParseException; 007import java.text.SimpleDateFormat; 008import java.util.ArrayList; 009import java.util.Collections; 010import java.util.Date; 011import java.util.HashMap; 012import java.util.HashSet; 013import java.util.List; 014import java.util.Map; 015import java.util.Set; 016 017import org.hl7.fhir.exceptions.DefinitionException; 018import org.hl7.fhir.exceptions.FHIRException; 019import org.hl7.fhir.exceptions.FHIRFormatError; 020import org.hl7.fhir.exceptions.TerminologyServiceException; 021import org.hl7.fhir.r4b.context.IWorkerContext.CodingValidationRequest; 022import org.hl7.fhir.r4b.context.IWorkerContext.ValidationResult; 023import org.hl7.fhir.r4b.model.BooleanType; 024import org.hl7.fhir.r4b.model.CanonicalResource; 025import org.hl7.fhir.r4b.model.CodeSystem; 026import org.hl7.fhir.r4b.model.CodeSystem.ConceptDefinitionComponent; 027import org.hl7.fhir.r4b.model.Coding; 028import org.hl7.fhir.r4b.model.ConceptMap; 029import org.hl7.fhir.r4b.model.DataType; 030import org.hl7.fhir.r4b.model.DomainResource; 031import org.hl7.fhir.r4b.model.Enumerations.FilterOperator; 032import org.hl7.fhir.r4b.model.Extension; 033import org.hl7.fhir.r4b.model.ExtensionHelper; 034import org.hl7.fhir.r4b.model.PrimitiveType; 035import org.hl7.fhir.r4b.model.Resource; 036import org.hl7.fhir.r4b.model.UriType; 037import org.hl7.fhir.r4b.model.ValueSet; 038import org.hl7.fhir.r4b.model.ValueSet.ConceptReferenceComponent; 039import org.hl7.fhir.r4b.model.ValueSet.ConceptReferenceDesignationComponent; 040import org.hl7.fhir.r4b.model.ValueSet.ConceptSetComponent; 041import org.hl7.fhir.r4b.model.ValueSet.ConceptSetFilterComponent; 042import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionComponent; 043import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent; 044import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionParameterComponent; 045import org.hl7.fhir.r4b.renderers.utils.RenderingContext; 046import org.hl7.fhir.r4b.renderers.utils.Resolver.ResourceContext; 047import org.hl7.fhir.r4b.terminologies.CodeSystemUtilities; 048import org.hl7.fhir.r4b.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 049import org.hl7.fhir.r4b.utils.ToolingExtensions; 050import org.hl7.fhir.utilities.Utilities; 051import org.hl7.fhir.utilities.xhtml.XhtmlNode; 052 053import com.google.common.collect.HashMultimap; 054import com.google.common.collect.Multimap; 055 056public class ValueSetRenderer extends TerminologyRenderer { 057 058 public ValueSetRenderer(RenderingContext context) { 059 super(context); 060 } 061 062 public ValueSetRenderer(RenderingContext context, ResourceContext rcontext) { 063 super(context, rcontext); 064 } 065 066 private static final String ABSTRACT_CODE_HINT = "This code is not selectable ('Abstract')"; 067 068 private static final int MAX_LANGS_IN_LINE = 5; 069 070 private List<ConceptMapRenderInstructions> renderingMaps = new ArrayList<ConceptMapRenderInstructions>(); 071 072 public boolean render(XhtmlNode x, Resource dr) throws FHIRFormatError, DefinitionException, IOException { 073 return render(x, (ValueSet) dr, false); 074 } 075 076 public boolean render(XhtmlNode x, ValueSet vs, boolean header) throws FHIRFormatError, DefinitionException, IOException { 077 List<UsedConceptMap> maps = findReleventMaps(vs); 078 079 boolean hasExtensions; 080 if (vs.hasExpansion()) { 081 // for now, we just accept an expansion if there is one 082 hasExtensions = generateExpansion(x, vs, header, maps); 083 } else { 084 hasExtensions = generateComposition(x, vs, header, maps); 085 } 086 return hasExtensions; 087 } 088 089 public void describe(XhtmlNode x, ValueSet vs) { 090 x.tx(display(vs)); 091 } 092 093 public String display(ValueSet vs) { 094 return vs.present(); 095 } 096 097 098 private List<UsedConceptMap> findReleventMaps(ValueSet vs) throws FHIRException { 099 List<UsedConceptMap> res = new ArrayList<UsedConceptMap>(); 100 for (CanonicalResource md : getContext().getWorker().allConformanceResources()) { 101 if (md instanceof ConceptMap) { 102 ConceptMap cm = (ConceptMap) md; 103 if (isSource(vs, cm.getSource())) { 104 ConceptMapRenderInstructions re = findByTarget(cm.getTarget()); 105 if (re != null) { 106 ValueSet vst = cm.hasTarget() ? getContext().getWorker().fetchResource(ValueSet.class, cm.hasTargetCanonicalType() ? cm.getTargetCanonicalType().getValue() : cm.getTargetUriType().asStringValue()) : null; 107 res.add(new UsedConceptMap(re, vst == null ? cm.getUserString("path") : vst.getUserString("path"), cm)); 108 } 109 } 110 } 111 } 112 return res; 113// Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>(); 114// for (ConceptMap a : context.getWorker().findMapsForSource(vs.getUrl())) { 115// String url = ""; 116// ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference()); 117// if (vsr != null) 118// url = (String) vsr.getUserData("filename"); 119// mymaps.put(a, url); 120// } 121// Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>(); 122// for (ConceptMap a : context.getWorker().findMapsForSource(cs.getValueSet())) { 123// String url = ""; 124// ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference()); 125// if (vsr != null) 126// url = (String) vsr.getUserData("filename"); 127// mymaps.put(a, url); 128// } 129 // also, look in the contained resources for a concept map 130// for (Resource r : cs.getContained()) { 131// if (r instanceof ConceptMap) { 132// ConceptMap cm = (ConceptMap) r; 133// if (((Reference) cm.getSource()).getReference().equals(cs.getValueSet())) { 134// String url = ""; 135// ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) cm.getTarget()).getReference()); 136// if (vsr != null) 137// url = (String) vsr.getUserData("filename"); 138// mymaps.put(cm, url); 139// } 140// } 141// } 142 } 143 144 private boolean isSource(ValueSet vs, DataType source) { 145 return vs.hasUrl() && source != null && vs.getUrl().equals(source.primitiveValue()); 146 } 147 148 private boolean generateExpansion(XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRFormatError, DefinitionException, IOException { 149 boolean hasExtensions = false; 150 List<String> langs = new ArrayList<String>(); 151 152 153 if (header) { 154 XhtmlNode h = x.addTag(getHeader()); 155 h.tx("Value Set Contents"); 156 if (IsNotFixedExpansion(vs)) 157 addMarkdown(x, vs.getDescription()); 158 if (vs.hasCopyright()) 159 generateCopyright(x, vs); 160 } 161 if (ToolingExtensions.hasExtension(vs.getExpansion(), ToolingExtensions.EXT_EXP_TOOCOSTLY)) { 162 List<Extension> exl = vs.getExpansion().getExtensionsByUrl(ToolingExtensions.EXT_EXP_TOOCOSTLY); 163 boolean other = false; 164 for (Extension ex : exl) { 165 if (ex.getValue() instanceof BooleanType) { 166 x.para().style("border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(vs.getExpansion().getContains().isEmpty() ? getContext().getTooCostlyNoteEmpty() : getContext().getTooCostlyNoteNotEmpty()); 167 } else if (!other) { 168 x.para().style("border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(vs.getExpansion().getContains().isEmpty() ? getContext().getTooCostlyNoteEmptyDependent() : getContext().getTooCostlyNoteNotEmptyDependent()); 169 other = true; 170 } 171 } 172 } else { 173 Integer count = countMembership(vs); 174 if (count == null) 175 x.para().tx("This value set does not contain a fixed number of concepts"); 176 else 177 x.para().tx("This value set contains "+count.toString()+" concepts"); 178 } 179 180 generateContentModeNotices(x, vs.getExpansion()); 181 generateVersionNotice(x, vs.getExpansion()); 182 183 CodeSystem allCS = null; 184 boolean doLevel = false; 185 for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) { 186 if (cc.hasContains()) { 187 doLevel = true; 188 break; 189 } 190 } 191 192 boolean doSystem = true; // checkDoSystem(vs, src); 193 boolean doDefinition = checkDoDefinition(vs.getExpansion().getContains()); 194 if (doSystem && allFromOneSystem(vs)) { 195 doSystem = false; 196 XhtmlNode p = x.para(); 197 p.tx("All codes in this table are from the system "); 198 allCS = getContext().getWorker().fetchCodeSystem(vs.getExpansion().getContains().get(0).getSystem()); 199 String ref = null; 200 if (allCS != null) 201 ref = getCsRef(allCS); 202 if (ref == null) 203 p.code(vs.getExpansion().getContains().get(0).getSystem()); 204 else 205 p.ah(context.fixReference(ref)).code(vs.getExpansion().getContains().get(0).getSystem()); 206 } 207 XhtmlNode t = x.table( "codes"); 208 XhtmlNode tr = t.tr(); 209 if (doLevel) 210 tr.td().b().tx("Lvl"); 211 tr.td().attribute("style", "white-space:nowrap").b().tx("Code"); 212 if (doSystem) 213 tr.td().b().tx("System"); 214 XhtmlNode tdDisp = tr.td(); 215 tdDisp.b().tx("Display"); 216 boolean doLangs = false; 217 for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { 218 scanForLangs(c, langs); 219 } 220 if (doDefinition) { 221 tr.td().b().tx("Definition"); 222 doLangs = false; 223 } else { 224 // if we're not doing definitions and we don't have too many languages, we'll do them in line 225 if (langs.size() < MAX_LANGS_IN_LINE) { 226 doLangs = true; 227 if (vs.hasLanguage()) { 228 tdDisp.tx(" - "+describeLang(vs.getLanguage())); 229 } 230 for (String lang : langs) { 231 tr.td().b().addText(describeLang(lang)); 232 } 233 } 234 } 235 236 237 addMapHeaders(tr, maps); 238 for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { 239 addExpansionRowToTable(t, c, 0, doLevel, doSystem, doDefinition, maps, allCS, langs, doLangs); 240 } 241 242 // now, build observed languages 243 244 if (!doLangs && langs.size() > 0) { 245 Collections.sort(langs); 246 x.para().b().tx("Additional Language Displays"); 247 t = x.table( "codes"); 248 tr = t.tr(); 249 tr.td().b().tx("Code"); 250 for (String lang : langs) { 251 tr.td().b().addText(describeLang(lang)); 252 } 253 for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { 254 addLanguageRow(c, t, langs); 255 } 256 } 257 258 return hasExtensions; 259 } 260 261 private void generateContentModeNotices(XhtmlNode x, ValueSetExpansionComponent expansion) { 262 generateContentModeNotice(x, expansion, "example", "Expansion based on example code system"); 263 generateContentModeNotice(x, expansion, "fragment", "Expansion based on code system fragment"); 264 } 265 266 private void generateContentModeNotice(XhtmlNode x, ValueSetExpansionComponent expansion, String mode, String text) { 267 Multimap<String, String> versions = HashMultimap.create(); 268 for (ValueSetExpansionParameterComponent p : expansion.getParameter()) { 269 if (p.getName().equals(mode)) { 270 String[] parts = ((PrimitiveType) p.getValue()).asStringValue().split("\\|"); 271 if (parts.length == 2) 272 versions.put(parts[0], parts[1]); 273 } 274 } 275 if (versions.size() > 0) { 276 XhtmlNode div = null; 277 XhtmlNode ul = null; 278 boolean first = true; 279 for (String s : versions.keySet()) { 280 if (versions.size() == 1 && versions.get(s).size() == 1) { 281 for (String v : versions.get(s)) { // though there'll only be one 282 XhtmlNode p = x.para().style("border: black 1px dotted; background-color: #ffcccc; padding: 8px; margin-bottom: 8px"); 283 p.tx(text+" "); 284 expRef(p, s, v); 285 } 286 } else { 287 for (String v : versions.get(s)) { 288 if (first) { 289 div = x.div().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px"); 290 div.para().tx(text+"s: "); 291 ul = div.ul(); 292 first = false; 293 } 294 expRef(ul.li(), s, v); 295 } 296 } 297 } 298 } 299 } 300 301 private boolean checkDoSystem(ValueSet vs, ValueSet src) { 302 if (src != null) 303 vs = src; 304 return vs.hasCompose(); 305 } 306 307 private boolean IsNotFixedExpansion(ValueSet vs) { 308 if (vs.hasCompose()) 309 return false; 310 311 312 // it's not fixed if it has any includes that are not version fixed 313 for (ConceptSetComponent cc : vs.getCompose().getInclude()) { 314 if (cc.hasValueSet()) 315 return true; 316 if (!cc.hasVersion()) 317 return true; 318 } 319 return false; 320 } 321 322 323 324 325 private ConceptMapRenderInstructions findByTarget(DataType source) { 326 if (source == null) { 327 return null; 328 } 329 String src = source.primitiveValue(); 330 if (src != null) 331 for (ConceptMapRenderInstructions t : renderingMaps) { 332 if (src.equals(t.getUrl())) 333 return t; 334 } 335 return null; 336 } 337 338 339 private Integer countMembership(ValueSet vs) { 340 int count = 0; 341 if (vs.hasExpansion()) 342 count = count + conceptCount(vs.getExpansion().getContains()); 343 else { 344 if (vs.hasCompose()) { 345 if (vs.getCompose().hasExclude()) { 346 try { 347 ValueSetExpansionOutcome vse = getContext().getWorker().expandVS(vs, true, false); 348 count = 0; 349 count += conceptCount(vse.getValueset().getExpansion().getContains()); 350 return count; 351 } catch (Exception e) { 352 return null; 353 } 354 } 355 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 356 if (inc.hasFilter()) 357 return null; 358 if (!inc.hasConcept()) 359 return null; 360 count = count + inc.getConcept().size(); 361 } 362 } 363 } 364 return count; 365 } 366 367 private int conceptCount(List<ValueSetExpansionContainsComponent> list) { 368 int count = 0; 369 for (ValueSetExpansionContainsComponent c : list) { 370 if (!c.getAbstract()) 371 count++; 372 count = count + conceptCount(c.getContains()); 373 } 374 return count; 375 } 376 377 private void addCSRef(XhtmlNode x, String url) { 378 CodeSystem cs = getContext().getWorker().fetchCodeSystem(url); 379 if (cs == null) { 380 x.code(url); 381 } else if (cs.hasUserData("path")) { 382 x.ah(cs.getUserString("path")).tx(cs.present()); 383 } else { 384 x.code(url); 385 x.tx(" ("+cs.present()+")"); 386 } 387 } 388 389 @SuppressWarnings("rawtypes") 390 private void generateVersionNotice(XhtmlNode x, ValueSetExpansionComponent expansion) { 391 Multimap<String, String> versions = HashMultimap.create(); 392 for (ValueSetExpansionParameterComponent p : expansion.getParameter()) { 393 if (p.getName().equals("version")) { 394 String[] parts = ((PrimitiveType) p.getValue()).asStringValue().split("\\|"); 395 if (parts.length == 2) 396 versions.put(parts[0], parts[1]); 397 } 398 } 399 if (versions.size() > 0) { 400 XhtmlNode div = null; 401 XhtmlNode ul = null; 402 boolean first = true; 403 for (String s : versions.keySet()) { 404 if (versions.size() == 1 && versions.get(s).size() == 1) { 405 for (String v : versions.get(s)) { // though there'll only be one 406 XhtmlNode p = x.para().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px"); 407 p.tx("Expansion based on "); 408 expRef(p, s, v); 409 } 410 } else { 411 for (String v : versions.get(s)) { 412 if (first) { 413 div = x.div().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px"); 414 div.para().tx("Expansion based on: "); 415 ul = div.ul(); 416 first = false; 417 } 418 expRef(ul.li(), s, v); 419 } 420 } 421 } 422 } 423 } 424 425 private void expRef(XhtmlNode x, String u, String v) { 426 // TODO Auto-generated method stub 427 if (u.equals("http://snomed.info/sct")) { 428 String[] parts = v.split("\\/"); 429 if (parts.length >= 5) { 430 String m = describeModule(parts[4]); 431 if (parts.length == 7) { 432 x.tx("SNOMED CT "+m+" edition "+formatSCTDate(parts[6])); 433 } else { 434 x.tx("SNOMED CT "+m+" edition"); 435 } 436 } else { 437 x.tx(describeSystem(u)+" version "+v); 438 } 439 } else if (u.equals("http://loinc.org")) { 440 String vd = describeLoincVer(v); 441 if (vd != null) { 442 x.tx("Loinc v"+v+" ("+vd+")"); 443 } else { 444 x.tx("Loinc v"+v); 445 } 446 } else { 447 CanonicalResource cr = (CanonicalResource) getContext().getWorker().fetchResource(Resource.class, u+"|"+v); 448 if (cr != null) { 449 if (cr.hasUserData("path")) { 450 x.ah(cr.getUserString("path")).tx(cr.present()+" v"+v+" ("+cr.fhirType()+")"); 451 } else { 452 x.tx(describeSystem(u)+" v"+v+" ("+cr.fhirType()+")"); 453 } 454 } else { 455 x.tx(describeSystem(u)+" version "+v); 456 } 457 } 458 } 459 460 private String describeLoincVer(String v) { 461 if ("2.67".equals(v)) return "Dec 2019"; 462 if ("2.66".equals(v)) return "Jun 2019"; 463 if ("2.65".equals(v)) return "Dec 2018"; 464 if ("2.64".equals(v)) return "Jun 2018"; 465 if ("2.63".equals(v)) return "Dec 2017"; 466 if ("2.61".equals(v)) return "Jun 2017"; 467 if ("2.59".equals(v)) return "Feb 2017"; 468 if ("2.58".equals(v)) return "Dec 2016"; 469 if ("2.56".equals(v)) return "Jun 2016"; 470 if ("2.54".equals(v)) return "Dec 2015"; 471 if ("2.52".equals(v)) return "Jun 2015"; 472 if ("2.50".equals(v)) return "Dec 2014"; 473 if ("2.48".equals(v)) return "Jun 2014"; 474 if ("2.46".equals(v)) return "Dec 2013"; 475 if ("2.44".equals(v)) return "Jun 2013"; 476 if ("2.42".equals(v)) return "Dec 2012"; 477 if ("2.40".equals(v)) return "Jun 2012"; 478 if ("2.38".equals(v)) return "Dec 2011"; 479 if ("2.36".equals(v)) return "Jun 2011"; 480 if ("2.34".equals(v)) return "Dec 2010"; 481 if ("2.32".equals(v)) return "Jun 2010"; 482 if ("2.30".equals(v)) return "Feb 2010"; 483 if ("2.29".equals(v)) return "Dec 2009"; 484 if ("2.27".equals(v)) return "Jul 2009"; 485 if ("2.26".equals(v)) return "Jan 2009"; 486 if ("2.24".equals(v)) return "Jul 2008"; 487 if ("2.22".equals(v)) return "Dec 2007"; 488 if ("2.21".equals(v)) return "Jun 2007"; 489 if ("2.19".equals(v)) return "Dec 2006"; 490 if ("2.17".equals(v)) return "Jun 2006"; 491 if ("2.16".equals(v)) return "Dec 2005"; 492 if ("2.15".equals(v)) return "Jun 2005"; 493 if ("2.14".equals(v)) return "Dec 2004"; 494 if ("2.13".equals(v)) return "Aug 2004"; 495 if ("2.12".equals(v)) return "Feb 2004"; 496 if ("2.10".equals(v)) return "Oct 2003"; 497 if ("2.09".equals(v)) return "May 2003"; 498 if ("2.08 ".equals(v)) return "Sep 2002"; 499 if ("2.07".equals(v)) return "Aug 2002"; 500 if ("2.05".equals(v)) return "Feb 2002"; 501 if ("2.04".equals(v)) return "Jan 2002"; 502 if ("2.03".equals(v)) return "Jul 2001"; 503 if ("2.02".equals(v)) return "May 2001"; 504 if ("2.01".equals(v)) return "Jan 2001"; 505 if ("2.00".equals(v)) return "Jan 2001"; 506 if ("1.0n".equals(v)) return "Feb 2000"; 507 if ("1.0ma".equals(v)) return "Aug 1999"; 508 if ("1.0m".equals(v)) return "Jul 1999"; 509 if ("1.0l".equals(v)) return "Jan 1998"; 510 if ("1.0ja".equals(v)) return "Oct 1997"; 511 return null; 512 } 513 514 private String formatSCTDate(String ds) { 515 SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd"); 516 Date date; 517 try { 518 date = format.parse(ds); 519 } catch (ParseException e) { 520 return ds; 521 } 522 return new SimpleDateFormat("dd-MMM yyyy").format(date); 523 } 524 525 private String describeModule(String module) { 526 if ("900000000000207008".equals(module)) 527 return "International"; 528 if ("731000124108".equals(module)) 529 return "United States"; 530 if ("32506021000036107".equals(module)) 531 return "Australian"; 532 if ("449081005".equals(module)) 533 return "Spanish"; 534 if ("554471000005108".equals(module)) 535 return "Danish"; 536 if ("11000146104".equals(module)) 537 return "Dutch"; 538 if ("45991000052106".equals(module)) 539 return "Swedish"; 540 if ("999000041000000102".equals(module)) 541 return "United Kingdon"; 542 return module; 543 } 544 545 private boolean hasVersionParameter(ValueSetExpansionComponent expansion) { 546 for (ValueSetExpansionParameterComponent p : expansion.getParameter()) { 547 if (p.getName().equals("version")) 548 return true; 549 } 550 return false; 551 } 552 553 private void addLanguageRow(ValueSetExpansionContainsComponent c, XhtmlNode t, List<String> langs) { 554 XhtmlNode tr = t.tr(); 555 tr.td().addText(c.getCode()); 556 addLangaugesToRow(c, langs, tr); 557 for (ValueSetExpansionContainsComponent cc : c.getContains()) { 558 addLanguageRow(cc, t, langs); 559 } 560 } 561 562 public void addLangaugesToRow(ValueSetExpansionContainsComponent c, List<String> langs, XhtmlNode tr) { 563 for (String lang : langs) { 564 String d = null; 565 for (Extension ext : c.getExtension()) { 566 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 567 String l = ToolingExtensions.readStringExtension(ext, "lang"); 568 if (lang.equals(l)) { 569 d = ToolingExtensions.readStringExtension(ext, "content"); 570 } 571 } 572 } 573 if (d == null) { 574 for (ConceptReferenceDesignationComponent dd : c.getDesignation()) { 575 String l = dd.getLanguage(); 576 if (lang.equals(l)) { 577 d = dd.getValue(); 578 } 579 } 580 } 581 tr.td().addText(d == null ? "" : d); 582 } 583 } 584 585 586 private boolean checkDoDefinition(List<ValueSetExpansionContainsComponent> contains) { 587 for (ValueSetExpansionContainsComponent c : contains) { 588 CodeSystem cs = getContext().getWorker().fetchCodeSystem(c.getSystem()); 589 if (cs != null) 590 return true; 591 if (checkDoDefinition(c.getContains())) 592 return true; 593 } 594 return false; 595 } 596 597 598 private boolean allFromOneSystem(ValueSet vs) { 599 if (vs.getExpansion().getContains().isEmpty()) 600 return false; 601 String system = vs.getExpansion().getContains().get(0).getSystem(); 602 for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) { 603 if (!checkSystemMatches(system, cc)) 604 return false; 605 } 606 return true; 607 } 608 609 private String getCsRef(String system) { 610 CodeSystem cs = getContext().getWorker().fetchCodeSystem(system); 611 return getCsRef(cs); 612 } 613 614 private <T extends Resource> String getCsRef(T cs) { 615 String ref = (String) cs.getUserData("filename"); 616 if (ref == null) 617 ref = (String) cs.getUserData("path"); 618 if (ref == null) 619 return "?ngen-14?.html"; 620 if (!ref.contains(".html")) 621 ref = ref + ".html"; 622 return ref.replace("\\", "/"); 623 } 624 625 private void scanForLangs(ValueSetExpansionContainsComponent c, List<String> langs) { 626 for (Extension ext : c.getExtension()) { 627 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 628 String lang = ToolingExtensions.readStringExtension(ext, "lang"); 629 if (!Utilities.noString(lang) && !langs.contains(lang)) { 630 langs.add(lang); 631 } 632 } 633 } 634 for (ConceptReferenceDesignationComponent d : c.getDesignation()) { 635 String lang = d.getLanguage(); 636 if (!Utilities.noString(lang) && !langs.contains(lang)) { 637 langs.add(lang); 638 } 639 } 640 for (ValueSetExpansionContainsComponent cc : c.getContains()) { 641 scanForLangs(cc, langs); 642 } 643 } 644 645 private void addExpansionRowToTable(XhtmlNode t, ValueSetExpansionContainsComponent c, int i, boolean doLevel, boolean doSystem, boolean doDefinition, List<UsedConceptMap> maps, CodeSystem allCS, List<String> langs, boolean doLangs) { 646 XhtmlNode tr = t.tr(); 647 XhtmlNode td = tr.td(); 648 649 String tgt = makeAnchor(c.getSystem(), c.getCode()); 650 td.an(tgt); 651 652 if (doLevel) { 653 td.addText(Integer.toString(i)); 654 td = tr.td(); 655 } 656 String s = Utilities.padLeft("", '\u00A0', i*2); 657 td.attribute("style", "white-space:nowrap").addText(s); 658 addCodeToTable(c.getAbstract(), c.getSystem(), c.getCode(), c.getDisplay(), td); 659 if (doSystem) { 660 td = tr.td(); 661 td.addText(c.getSystem()); 662 } 663 td = tr.td(); 664 if (c.hasDisplayElement()) 665 td.addText(c.getDisplay()); 666 667 if (doDefinition) { 668 CodeSystem cs = allCS; 669 if (cs == null) 670 cs = getContext().getWorker().fetchCodeSystem(c.getSystem()); 671 td = tr.td(); 672 if (cs != null) 673 td.addText(CodeSystemUtilities.getCodeDefinition(cs, c.getCode())); 674 } 675 for (UsedConceptMap m : maps) { 676 td = tr.td(); 677 List<TargetElementComponentWrapper> mappings = findMappingsForCode(c.getCode(), m.getMap()); 678 boolean first = true; 679 for (TargetElementComponentWrapper mapping : mappings) { 680 if (!first) 681 td.br(); 682 first = false; 683 XhtmlNode span = td.span(null, mapping.comp.getEquivalence().toString()); 684 span.addText(getCharForRelationship(mapping.comp)); 685 addRefToCode(td, mapping.group.getTarget(), m.getLink(), mapping.comp.getCode()); 686 if (!Utilities.noString(mapping.comp.getComment())) 687 td.i().tx("("+mapping.comp.getComment()+")"); 688 } 689 } 690 if (doLangs) { 691 addLangaugesToRow(c, langs, tr); 692 } 693 for (ValueSetExpansionContainsComponent cc : c.getContains()) { 694 addExpansionRowToTable(t, cc, i+1, doLevel, doSystem, doDefinition, maps, allCS, langs, doLangs); 695 } 696 } 697 698 699 700 701 702 private boolean checkSystemMatches(String system, ValueSetExpansionContainsComponent cc) { 703 if (!system.equals(cc.getSystem())) 704 return false; 705 for (ValueSetExpansionContainsComponent cc1 : cc.getContains()) { 706 if (!checkSystemMatches(system, cc1)) 707 return false; 708 } 709 return true; 710 } 711 712 private void addCodeToTable(boolean isAbstract, String system, String code, String display, XhtmlNode td) { 713 CodeSystem e = getContext().getWorker().fetchCodeSystem(system); 714 if (e == null || e.getContent() != org.hl7.fhir.r4b.model.CodeSystem.CodeSystemContentMode.COMPLETE) { 715 if (isAbstract) 716 td.i().setAttribute("title", ABSTRACT_CODE_HINT).addText(code); 717 else if ("http://snomed.info/sct".equals(system)) { 718 td.ah(sctLink(code)).addText(code); 719 } else if ("http://loinc.org".equals(system)) { 720 td.ah("http://details.loinc.org/LOINC/"+code+".html").addText(code); 721 } else 722 td.addText(code); 723 } else { 724 String href = context.fixReference(getCsRef(e)); 725 if (href.contains("#")) 726 href = href + "-"+Utilities.nmtokenize(code); 727 else 728 href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(code); 729 if (isAbstract) 730 td.ah(href).setAttribute("title", ABSTRACT_CODE_HINT).i().addText(code); 731 else 732 td.ah(href).addText(code); 733 } 734 } 735 736 737 public String sctLink(String code) { 738// if (snomedEdition != null) 739// http://browser.ihtsdotools.org/?perspective=full&conceptId1=428041000124106&edition=us-edition&release=v20180301&server=https://prod-browser-exten.ihtsdotools.org/api/snomed&langRefset=900000000000509007 740 return "http://snomed.info/id/"+code; 741 } 742 743 private void addRefToCode(XhtmlNode td, String target, String vslink, String code) { 744 CodeSystem cs = getContext().getWorker().fetchCodeSystem(target); 745 String cslink = getCsRef(cs); 746 XhtmlNode a = null; 747 if (cslink != null) 748 a = td.ah(getContext().getSpecificationLink()+cslink+"#"+cs.getId()+"-"+code); 749 else 750 a = td.ah(getContext().getSpecificationLink()+vslink+"#"+code); 751 a.addText(code); 752 } 753 754 private boolean generateComposition(XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRException, IOException { 755 boolean hasExtensions = false; 756 List<String> langs = new ArrayList<String>(); 757 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 758 scanForLangs(inc, langs); 759 } 760 for (ConceptSetComponent inc : vs.getCompose().getExclude()) { 761 scanForLangs(inc, langs); 762 } 763 boolean doLangs = langs.size() < MAX_LANGS_IN_LINE; 764 765 if (header) { 766 XhtmlNode h = x.h2(); 767 h.addText(vs.present()); 768 addMarkdown(x, vs.getDescription()); 769 if (vs.hasCopyrightElement()) 770 generateCopyright(x, vs); 771 } 772 if (vs.getCompose().getInclude().size() == 1 && vs.getCompose().getExclude().size() == 0) { 773 hasExtensions = genInclude(x.ul(), vs.getCompose().getInclude().get(0), "Include", langs, doLangs, maps) || hasExtensions; 774 } else { 775 XhtmlNode p = x.para(); 776 p.tx("This value set includes codes based on the following rules:"); 777 XhtmlNode ul = x.ul(); 778 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 779 hasExtensions = genInclude(ul, inc, "Include", langs, doLangs, maps) || hasExtensions; 780 } 781 if (vs.getCompose().hasExclude()) { 782 p = x.para(); 783 p.tx("This value set excludes codes based on the following rules:"); 784 ul = x.ul(); 785 for (ConceptSetComponent exc : vs.getCompose().getExclude()) { 786 hasExtensions = genInclude(ul, exc, "Exclude", langs, doLangs, maps) || hasExtensions; 787 } 788 } 789 } 790 791 // now, build observed languages 792 793 if (!doLangs && langs.size() > 0) { 794 Collections.sort(langs); 795 x.para().b().tx("Additional Language Displays"); 796 XhtmlNode t = x.table( "codes"); 797 XhtmlNode tr = t.tr(); 798 tr.td().b().tx("Code"); 799 for (String lang : langs) 800 tr.td().b().addText(describeLang(lang)); 801 for (ConceptSetComponent c : vs.getCompose().getInclude()) { 802 for (ConceptReferenceComponent cc : c.getConcept()) { 803 addLanguageRow(cc, t, langs); 804 } 805 } 806 } 807 808 return hasExtensions; 809 } 810 811 private void scanForLangs(ConceptSetComponent inc, List<String> langs) { 812 for (ConceptReferenceComponent cc : inc.getConcept()) { 813 for (Extension ext : cc.getExtension()) { 814 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 815 String lang = ToolingExtensions.readStringExtension(ext, "lang"); 816 if (!Utilities.noString(lang) && !langs.contains(lang)) { 817 langs.add(lang); 818 } 819 } 820 } 821 for (ConceptReferenceDesignationComponent d : cc.getDesignation()) { 822 String lang = d.getLanguage(); 823 if (!Utilities.noString(lang) && !langs.contains(lang)) { 824 langs.add(lang); 825 } 826 } 827 } 828 } 829 830 private boolean genInclude(XhtmlNode ul, ConceptSetComponent inc, String type, List<String> langs, boolean doLangs, List<UsedConceptMap> maps) throws FHIRException, IOException { 831 boolean hasExtensions = false; 832 XhtmlNode li; 833 li = ul.li(); 834 CodeSystem e = getContext().getWorker().fetchCodeSystem(inc.getSystem()); 835 836 if (inc.hasSystem()) { 837 if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) { 838 li.addText(type+" all codes defined in "); 839 addCsRef(inc, li, e); 840 } else { 841 if (inc.getConcept().size() > 0) { 842 li.addText(type+" these codes as defined in "); 843 addCsRef(inc, li, e); 844 if (inc.hasVersion()) { 845 li.addText(" version "); 846 li.code(inc.getVersion()); 847 } 848 849 // for performance reasons, we do all the fetching in one batch 850 Map<String, ConceptDefinitionComponent> definitions = getConceptsForCodes(e, inc); 851 852 XhtmlNode t = li.table("none"); 853 boolean hasComments = false; 854 boolean hasDefinition = false; 855 for (ConceptReferenceComponent c : inc.getConcept()) { 856 hasComments = hasComments || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT); 857 ConceptDefinitionComponent cc = definitions.get(c.getCode()); 858 hasDefinition = hasDefinition || ((cc != null && cc.hasDefinition()) || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION)); 859 } 860 if (hasComments || hasDefinition) 861 hasExtensions = true; 862 addMapHeaders(addTableHeaderRowStandard(t, false, true, hasDefinition, hasComments, false, false, null, langs, doLangs), maps); 863 for (ConceptReferenceComponent c : inc.getConcept()) { 864 XhtmlNode tr = t.tr(); 865 XhtmlNode td = tr.td(); 866 ConceptDefinitionComponent cc = definitions.get(c.getCode()); 867 addCodeToTable(false, inc.getSystem(), c.getCode(), c.hasDisplay()? c.getDisplay() : cc != null ? cc.getDisplay() : "", td); 868 869 td = tr.td(); 870 if (!Utilities.noString(c.getDisplay())) 871 td.addText(c.getDisplay()); 872 else if (cc != null && !Utilities.noString(cc.getDisplay())) 873 td.addText(cc.getDisplay()); 874 875 if (hasDefinition) { 876 td = tr.td(); 877 if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION)) { 878 smartAddText(td, ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_DEFINITION)); 879 } else if (cc != null && !Utilities.noString(cc.getDefinition())) { 880 smartAddText(td, cc.getDefinition()); 881 } 882 } 883 if (hasComments) { 884 td = tr.td(); 885 if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT)) { 886 smartAddText(td, "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_VS_COMMENT)); 887 } 888 } 889 if (doLangs) { 890 addLangaugesToRow(c, langs, tr); 891 } 892 } 893 } 894 if (inc.getFilter().size() > 0) { 895 li.addText(type+" codes from "); 896 addCsRef(inc, li, e); 897 li.tx(" where "); 898 for (int i = 0; i < inc.getFilter().size(); i++) { 899 ConceptSetFilterComponent f = inc.getFilter().get(i); 900 if (i > 0) { 901 if (i == inc.getFilter().size()-1) { 902 li.tx(" and "); 903 } else { 904 li.tx(", "); 905 } 906 } 907 if (f.getOp() == FilterOperator.EXISTS) { 908 if (f.getValue().equals("true")) { 909 li.tx(f.getProperty()+" exists"); 910 } else { 911 li.tx(f.getProperty()+" doesn't exist"); 912 } 913 } else { 914 li.tx(f.getProperty()+" "+describe(f.getOp())+" "); 915 if (e != null && codeExistsInValueSet(e, f.getValue())) { 916 String href = getContext().fixReference(getCsRef(e)); 917 if (href.contains("#")) 918 href = href + "-"+Utilities.nmtokenize(f.getValue()); 919 else 920 href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(f.getValue()); 921 li.ah(href).addText(f.getValue()); 922 } else if ("concept".equals(f.getProperty()) && inc.hasSystem()) { 923 li.addText(f.getValue()); 924 ValidationResult vr = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions(), inc.getSystem(), inc.getVersion(), f.getValue(), null); 925 if (vr.isOk()) { 926 li.tx(" ("+vr.getDisplay()+")"); 927 } 928 } 929 else 930 li.addText(f.getValue()); 931 String disp = ToolingExtensions.getDisplayHint(f); 932 if (disp != null) 933 li.tx(" ("+disp+")"); 934 } 935 } 936 } 937 } 938 if (inc.hasValueSet()) { 939 li.tx(", where the codes are contained in "); 940 boolean first = true; 941 for (UriType vs : inc.getValueSet()) { 942 if (first) 943 first = false; 944 else 945 li.tx(", "); 946 AddVsRef(vs.asStringValue(), li); 947 } 948 } 949 } else { 950 li.tx("Import all the codes that are contained in "); 951 if (inc.getValueSet().size() < 4) { 952 boolean first = true; 953 for (UriType vs : inc.getValueSet()) { 954 if (first) 955 first = false; 956 else 957 li.tx(", "); 958 AddVsRef(vs.asStringValue(), li); 959 } 960 } else { 961 XhtmlNode xul = li.ul(); 962 for (UriType vs : inc.getValueSet()) { 963 AddVsRef(vs.asStringValue(), xul.li()); 964 } 965 966 } 967 } 968 return hasExtensions; 969 } 970 971 public void addLangaugesToRow(ConceptReferenceComponent c, List<String> langs, XhtmlNode tr) { 972 for (String lang : langs) { 973 String d = null; 974 for (Extension ext : c.getExtension()) { 975 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 976 String l = ToolingExtensions.readStringExtension(ext, "lang"); 977 if (lang.equals(l)) { 978 d = ToolingExtensions.readStringExtension(ext, "content"); 979 } 980 } 981 } 982 if (d == null) { 983 for (ConceptReferenceDesignationComponent dd : c.getDesignation()) { 984 String l = dd.getLanguage(); 985 if (lang.equals(l)) { 986 d = dd.getValue(); 987 } 988 } 989 } 990 tr.td().addText(d == null ? "" : d); 991 } 992 } 993 994 995 private Map<String, ConceptDefinitionComponent> getConceptsForCodes(CodeSystem e, ConceptSetComponent inc) { 996 if (e == null) { 997 e = getContext().getWorker().fetchCodeSystem(inc.getSystem()); 998 } 999 1000 ValueSetExpansionComponent vse = null; 1001 if (!context.isNoSlowLookup() && !getContext().getWorker().hasCache()) { 1002 try { 1003 ValueSetExpansionOutcome vso = getContext().getWorker().expandVS(inc, false); 1004 ValueSet valueset = vso.getValueset(); 1005 if (valueset == null) 1006 throw new TerminologyServiceException("Error Expanding ValueSet: "+vso.getError()); 1007 vse = valueset.getExpansion(); 1008 1009 } catch (TerminologyServiceException e1) { 1010 return null; 1011 } 1012 } 1013 1014 Map<String, ConceptDefinitionComponent> results = new HashMap<>(); 1015 List<CodingValidationRequest> serverList = new ArrayList<>(); 1016 1017 // 1st pass, anything we can resolve internally 1018 for (ConceptReferenceComponent cc : inc.getConcept()) { 1019 String code = cc.getCode(); 1020 ConceptDefinitionComponent v = null; 1021 if (e != null) { 1022 v = getConceptForCode(e.getConcept(), code); 1023 } 1024 if (v == null && vse != null) { 1025 v = getConceptForCodeFromExpansion(vse.getContains(), code); 1026 } 1027 if (v != null) { 1028 results.put(code, v); 1029 } else { 1030 serverList.add(new CodingValidationRequest(new Coding(inc.getSystem(), code, null))); 1031 } 1032 } 1033 if (!context.isNoSlowLookup() && !serverList.isEmpty()) { 1034 getContext().getWorker().validateCodeBatch(getContext().getTerminologyServiceOptions(), serverList, null); 1035 for (CodingValidationRequest vr : serverList) { 1036 ConceptDefinitionComponent v = vr.getResult().asConceptDefinition(); 1037 if (v != null) { 1038 results.put(vr.getCoding().getCode(), v); 1039 } 1040 } 1041 } 1042 return results; 1043 } 1044 1045 private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> list, String code) { 1046 for (ConceptDefinitionComponent c : list) { 1047 if (code.equals(c.getCode())) 1048 return c; 1049 ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code); 1050 if (v != null) 1051 return v; 1052 } 1053 return null; 1054 } 1055 1056 private ConceptDefinitionComponent getConceptForCodeFromExpansion(List<ValueSetExpansionContainsComponent> list, String code) { 1057 for (ValueSetExpansionContainsComponent c : list) { 1058 if (code.equals(c.getCode())) { 1059 ConceptDefinitionComponent res = new ConceptDefinitionComponent(); 1060 res.setCode(c.getCode()); 1061 res.setDisplay(c.getDisplay()); 1062 return res; 1063 } 1064 ConceptDefinitionComponent v = getConceptForCodeFromExpansion(c.getContains(), code); 1065 if (v != null) 1066 return v; 1067 } 1068 return null; 1069 } 1070 1071 1072 private boolean codeExistsInValueSet(CodeSystem cs, String code) { 1073 for (ConceptDefinitionComponent c : cs.getConcept()) { 1074 if (inConcept(code, c)) 1075 return true; 1076 } 1077 return false; 1078 } 1079 1080 1081 private void addLanguageRow(ConceptReferenceComponent c, XhtmlNode t, List<String> langs) { 1082 XhtmlNode tr = t.tr(); 1083 tr.td().addText(c.getCode()); 1084 for (String lang : langs) { 1085 String d = null; 1086 for (ConceptReferenceDesignationComponent cd : c.getDesignation()) { 1087 String l = cd.getLanguage(); 1088 if (lang.equals(l)) 1089 d = cd.getValue(); 1090 } 1091 tr.td().addText(d == null ? "" : d); 1092 } 1093 } 1094 1095 1096 private String describe(FilterOperator op) { 1097 if (op == null) 1098 return " null "; 1099 switch (op) { 1100 case EQUAL: return " = "; 1101 case ISA: return " is-a "; 1102 case ISNOTA: return " is-not-a "; 1103 case REGEX: return " matches (by regex) "; 1104 case NULL: return " ?ngen-13? "; 1105 case IN: return " in "; 1106 case NOTIN: return " not in "; 1107 case DESCENDENTOF: return " descends from "; 1108 case EXISTS: return " exists "; 1109 case GENERALIZES: return " generalizes "; 1110 } 1111 return null; 1112 } 1113 1114 1115 1116 1117 1118 private boolean inConcept(String code, ConceptDefinitionComponent c) { 1119 if (c.hasCodeElement() && c.getCode().equals(code)) 1120 return true; 1121 for (ConceptDefinitionComponent g : c.getConcept()) { 1122 if (inConcept(code, g)) 1123 return true; 1124 } 1125 return false; 1126 } 1127 1128 1129}