001package org.hl7.fhir.r5.renderers; 002 003import java.io.IOException; 004import java.text.ParseException; 005import java.text.SimpleDateFormat; 006import java.util.ArrayList; 007import java.util.Collections; 008import java.util.Date; 009import java.util.HashMap; 010import java.util.List; 011import java.util.Map; 012 013import org.hl7.fhir.exceptions.DefinitionException; 014import org.hl7.fhir.exceptions.FHIRException; 015import org.hl7.fhir.exceptions.FHIRFormatError; 016import org.hl7.fhir.exceptions.TerminologyServiceException; 017import org.hl7.fhir.r5.context.IWorkerContext.CodingValidationRequest; 018import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; 019import org.hl7.fhir.r5.model.BooleanType; 020import org.hl7.fhir.r5.model.CanonicalResource; 021import org.hl7.fhir.r5.model.CodeSystem; 022import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; 023import org.hl7.fhir.r5.model.Coding; 024import org.hl7.fhir.r5.model.ConceptMap; 025import org.hl7.fhir.r5.model.DataType; 026import org.hl7.fhir.r5.model.Enumerations.FilterOperator; 027import org.hl7.fhir.r5.model.Extension; 028import org.hl7.fhir.r5.model.ExtensionHelper; 029import org.hl7.fhir.r5.model.PrimitiveType; 030import org.hl7.fhir.r5.model.Resource; 031import org.hl7.fhir.r5.model.UriType; 032import org.hl7.fhir.r5.model.ValueSet; 033import org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent; 034import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; 035import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent; 036import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; 037import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; 038import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent; 039import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; 040import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent; 041import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent; 042import org.hl7.fhir.r5.renderers.utils.RenderingContext; 043import org.hl7.fhir.r5.renderers.utils.RenderingContext.GenerationRules; 044import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext; 045import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; 046import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 047import org.hl7.fhir.r5.terminologies.ValueSetUtilities; 048import org.hl7.fhir.r5.utils.ToolingExtensions; 049import org.hl7.fhir.utilities.LoincLinker; 050import org.hl7.fhir.utilities.Utilities; 051import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; 052import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row; 053import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; 054import org.hl7.fhir.utilities.xhtml.XhtmlNode; 055 056import com.google.common.collect.HashMultimap; 057import com.google.common.collect.Multimap; 058 059public class ValueSetRenderer extends TerminologyRenderer { 060 061 public ValueSetRenderer(RenderingContext context) { 062 super(context); 063 } 064 065 public ValueSetRenderer(RenderingContext context, ResourceContext rcontext) { 066 super(context, rcontext); 067 } 068 069 private static final String ABSTRACT_CODE_HINT = "This code is not selectable ('Abstract')"; 070 071 private static final int MAX_DESIGNATIONS_IN_LINE = 5; 072 073 private List<ConceptMapRenderInstructions> renderingMaps = new ArrayList<ConceptMapRenderInstructions>(); 074 075 public boolean render(XhtmlNode x, Resource dr) throws FHIRFormatError, DefinitionException, IOException { 076 return render(x, (ValueSet) dr, false); 077 } 078 079 public boolean render(XhtmlNode x, ValueSet vs, boolean header) throws FHIRFormatError, DefinitionException, IOException { 080 List<UsedConceptMap> maps = findReleventMaps(vs); 081 082 boolean hasExtensions; 083 if (vs.hasExpansion()) { 084 // for now, we just accept an expansion if there is one 085 hasExtensions = generateExpansion(x, vs, header, maps); 086 } else { 087 hasExtensions = generateComposition(x, vs, header, maps); 088 } 089 return hasExtensions; 090 } 091 092 public void describe(XhtmlNode x, ValueSet vs) { 093 x.tx(display(vs)); 094 } 095 096 public String display(ValueSet vs) { 097 return vs.present(); 098 } 099 100 101 private List<UsedConceptMap> findReleventMaps(ValueSet vs) throws FHIRException { 102 List<UsedConceptMap> res = new ArrayList<UsedConceptMap>(); 103 for (ConceptMap cm : getContext().getWorker().fetchResourcesByType(ConceptMap.class)) { 104 if (isSource(vs, cm.getSourceScope())) { 105 ConceptMapRenderInstructions re = findByTarget(cm.getTargetScope()); 106 if (re == null) { 107 re = new ConceptMapRenderInstructions(cm.present(), cm.getUrl(), false); 108 } 109 if (re != null) { 110 ValueSet vst = cm.hasTargetScope() ? getContext().getWorker().fetchResource(ValueSet.class, cm.hasTargetScopeCanonicalType() ? cm.getTargetScopeCanonicalType().getValue() : cm.getTargetScopeUriType().asStringValue(), cm) : null; 111 res.add(new UsedConceptMap(re, vst == null ? cm.getUserString("path") : vst.getUserString("path"), cm)); 112 } 113 } 114 } 115 return res; 116 117// @Override 118// public List<ConceptMap> findMapsForSource(String url) throws FHIRException { 119// synchronized (lock) { 120// List<ConceptMap> res = new ArrayList<ConceptMap>(); 121// for (ConceptMap map : maps.getList()) { 122// if (((Reference) map.getSourceScope()).getReference().equals(url)) { 123// res.add(map); 124// } 125// } 126// return res; 127// } 128// } 129 130// Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>(); 131// for (ConceptMap a : context.getWorker().findMapsForSource(vs.getUrl())) { 132// String url = ""; 133// ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference()); 134// if (vsr != null) 135// url = (String) vsr.getUserData("filename"); 136// mymaps.put(a, url); 137// } 138// Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>(); 139// for (ConceptMap a : context.getWorker().findMapsForSource(cs.getValueSet())) { 140// String url = ""; 141// ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference()); 142// if (vsr != null) 143// url = (String) vsr.getUserData("filename"); 144// mymaps.put(a, url); 145// } 146 // also, look in the contained resources for a concept map 147// for (Resource r : cs.getContained()) { 148// if (r instanceof ConceptMap) { 149// ConceptMap cm = (ConceptMap) r; 150// if (((Reference) cm.getSource()).getReference().equals(cs.getValueSet())) { 151// String url = ""; 152// ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) cm.getTarget()).getReference()); 153// if (vsr != null) 154// url = (String) vsr.getUserData("filename"); 155// mymaps.put(cm, url); 156// } 157// } 158// } 159 } 160 161 private boolean isSource(ValueSet vs, DataType source) { 162 return vs.hasUrl() && source != null && vs.getUrl().equals(source.primitiveValue()); 163 } 164 165 private boolean generateExpansion(XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRFormatError, DefinitionException, IOException { 166 boolean hasExtensions = false; 167 List<String> langs = new ArrayList<String>(); 168 Map<String, String> designations = new HashMap<>(); // map of url = description, where url is the designation code. Designations that are for languages won't make it into this list 169 Map<String, String> properties = new HashMap<>(); // map of url = description, where url is the designation code. Designations that are for languages won't make it into this list 170 171 if (header) { 172 XhtmlNode h = x.addTag(getHeader()); 173 h.tx("Value Set Contents"); 174 if (IsNotFixedExpansion(vs)) 175 addMarkdown(x, vs.getDescription()); 176 if (vs.hasCopyright()) 177 generateCopyright(x, vs); 178 } 179 if (ToolingExtensions.hasExtension(vs.getExpansion(), ToolingExtensions.EXT_EXP_TOOCOSTLY)) { 180 List<Extension> exl = vs.getExpansion().getExtensionsByUrl(ToolingExtensions.EXT_EXP_TOOCOSTLY); 181 boolean other = false; 182 for (Extension ex : exl) { 183 if (ex.getValue() instanceof BooleanType) { 184 x.para().style("border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(vs.getExpansion().getContains().isEmpty() ? getContext().getTooCostlyNoteEmpty() : getContext().getTooCostlyNoteNotEmpty()); 185 } else if (!other) { 186 x.para().style("border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(vs.getExpansion().getContains().isEmpty() ? getContext().getTooCostlyNoteEmptyDependent() : getContext().getTooCostlyNoteNotEmptyDependent()); 187 other = true; 188 } 189 } 190 } else { 191 Integer count = countMembership(vs); 192 if (count == null) 193 x.para().tx("This value set does not contain a fixed number of concepts"); 194 else 195 x.para().tx("This value set contains "+count.toString()+" concepts"); 196 } 197 198 generateContentModeNotices(x, vs.getExpansion(), vs); 199 generateVersionNotice(x, vs.getExpansion(), vs); 200 201 CodeSystem allCS = null; 202 boolean doLevel = false; 203 for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) { 204 if (cc.hasContains()) { 205 doLevel = true; 206 break; 207 } 208 } 209 210 boolean doDefinition = checkDoDefinition(vs.getExpansion().getContains()); 211 XhtmlNode t = x.table( "codes"); 212 XhtmlNode tr = t.tr(); 213 if (doLevel) 214 tr.td().b().tx("Level"); 215 tr.td().attribute("style", "white-space:nowrap").b().tx("Code"); 216 tr.td().b().tx("System"); 217 XhtmlNode tdDisp = tr.td(); 218 tdDisp.b().tx("Display"); 219 boolean doDesignations = false; 220 for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { 221 scanForDesignations(c, langs, designations); 222 } 223 scanForProperties(vs.getExpansion(), langs, properties); 224 if (doDefinition) { 225 tr.td().b().tx("Definition"); 226 doDesignations = false; 227 for (String n : Utilities.sorted(properties.keySet())) { 228 tr.td().b().ah(properties.get(n)).addText(n); 229 } 230 } else { 231 for (String n : Utilities.sorted(properties.keySet())) { 232 tr.td().b().ah(properties.get(n)).addText(n); 233 } 234 // if we're not doing definitions and we don't have too many languages, we'll do them in line 235 doDesignations = langs.size() + properties.size() + designations.size() < MAX_DESIGNATIONS_IN_LINE; 236 237 if (doDesignations) { 238 if (vs.hasLanguage()) { 239 tdDisp.tx(" - "+describeLang(vs.getLanguage())); 240 } 241 for (String url : designations.keySet()) { 242 tr.td().b().addText(designations.get(url)); 243 } 244 for (String lang : langs) { 245 tr.td().b().addText(describeLang(lang)); 246 } 247 } 248 } 249 250 251 addMapHeaders(tr, maps); 252 for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { 253 addExpansionRowToTable(t, vs, c, 1, doLevel, true, doDefinition, maps, allCS, langs, designations, doDesignations, properties); 254 } 255 256 // now, build observed languages 257 258 if (!doDesignations && langs.size() + designations.size() > 0) { 259 Collections.sort(langs); 260 if (designations.size() == 0) { 261 x.para().b().tx("Additional Language Displays"); 262 } else if (langs.size() == 0) { 263 x.para().b().tx("Additional Designations"); 264 } else { 265 x.para().b().tx("Additional Designations and Language Displays"); 266 } 267 t = x.table("codes"); 268 tr = t.tr(); 269 tr.td().b().tx("Code"); 270 for (String url : designations.keySet()) { 271 tr.td().b().addText(designations.get(url)); 272 } 273 for (String lang : langs) { 274 tr.td().b().addText(describeLang(lang)); 275 } 276 for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { 277 addDesignationRow(c, t, langs, designations); 278 } 279 } 280 281 return hasExtensions; 282 } 283 284 285 private void scanForProperties(ValueSetExpansionComponent exp, List<String> langs, Map<String, String> properties) { 286 properties.clear(); 287 for (ValueSetExpansionPropertyComponent pp : exp.getProperty()) { 288 if (pp.hasCode() && pp.hasUri() && anyActualproperties(exp.getContains(), pp.getCode())) { 289 properties.put(pp.getCode(), pp.getUri()); 290 } 291 } 292 } 293 294 private boolean anyActualproperties(List<ValueSetExpansionContainsComponent> contains, String pp) { 295 for (ValueSetExpansionContainsComponent c : contains) { 296 for (ConceptPropertyComponent cp : c.getProperty()) { 297 if (pp.equals(cp.getCode())) { 298 return true; 299 } 300 } 301 if (anyActualproperties(c.getContains(), pp)) { 302 return true; 303 } 304 } 305 return false; 306 } 307 308 private void generateContentModeNotices(XhtmlNode x, ValueSetExpansionComponent expansion, Resource vs) { 309 generateContentModeNotice(x, expansion, "example", "Expansion based on example code system", vs); 310 generateContentModeNotice(x, expansion, "fragment", "Expansion based on code system fragment", vs); 311 } 312 313 private void generateContentModeNotice(XhtmlNode x, ValueSetExpansionComponent expansion, String mode, String text, Resource vs) { 314 Multimap<String, String> versions = HashMultimap.create(); 315 for (ValueSetExpansionParameterComponent p : expansion.getParameter()) { 316 if (p.getName().equals(mode)) { 317 String[] parts = ((PrimitiveType) p.getValue()).asStringValue().split("\\|"); 318 if (parts.length == 2) 319 versions.put(parts[0], parts[1]); 320 } 321 } 322 if (versions.size() > 0) { 323 XhtmlNode div = null; 324 XhtmlNode ul = null; 325 boolean first = true; 326 for (String s : versions.keySet()) { 327 if (versions.size() == 1 && versions.get(s).size() == 1) { 328 for (String v : versions.get(s)) { // though there'll only be one 329 XhtmlNode p = x.para().style("border: black 1px dotted; background-color: #ffcccc; padding: 8px; margin-bottom: 8px"); 330 p.tx(text+" "); 331 expRef(p, s, v, vs); 332 } 333 } else { 334 for (String v : versions.get(s)) { 335 if (first) { 336 div = x.div().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px"); 337 div.para().tx(text+"s: "); 338 ul = div.ul(); 339 first = false; 340 } 341 expRef(ul.li(), s, v, vs); 342 } 343 } 344 } 345 } 346 } 347 348 private boolean checkDoSystem(ValueSet vs, ValueSet src) { 349 if (src != null) 350 vs = src; 351 return vs.hasCompose(); 352 } 353 354 private boolean IsNotFixedExpansion(ValueSet vs) { 355 if (vs.hasCompose()) 356 return false; 357 358 359 // it's not fixed if it has any includes that are not version fixed 360 for (ConceptSetComponent cc : vs.getCompose().getInclude()) { 361 if (cc.hasValueSet()) 362 return true; 363 if (!cc.hasVersion()) 364 return true; 365 } 366 return false; 367 } 368 369 370 371 372 private ConceptMapRenderInstructions findByTarget(DataType source) { 373 if (source == null) { 374 return null; 375 } 376 String src = source.primitiveValue(); 377 if (src == null) { 378 return null; 379 } 380 for (ConceptMapRenderInstructions t : renderingMaps) { 381 if (src.equals(t.getUrl())) 382 return t; 383 } 384 return null; 385 } 386 387 388 private Integer countMembership(ValueSet vs) { 389 int count = 0; 390 if (vs.hasExpansion()) 391 count = count + conceptCount(vs.getExpansion().getContains()); 392 else { 393 if (vs.hasCompose()) { 394 if (vs.getCompose().hasExclude()) { 395 try { 396 ValueSetExpansionOutcome vse = getContext().getWorker().expandVS(vs, true, false); 397 count = 0; 398 count += conceptCount(vse.getValueset().getExpansion().getContains()); 399 return count; 400 } catch (Exception e) { 401 return null; 402 } 403 } 404 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 405 if (inc.hasFilter()) 406 return null; 407 if (!inc.hasConcept()) 408 return null; 409 count = count + inc.getConcept().size(); 410 } 411 } 412 } 413 return count; 414 } 415 416 private int conceptCount(List<ValueSetExpansionContainsComponent> list) { 417 int count = 0; 418 for (ValueSetExpansionContainsComponent c : list) { 419 if (!c.getAbstract()) 420 count++; 421 count = count + conceptCount(c.getContains()); 422 } 423 return count; 424 } 425 426 private void addCSRef(XhtmlNode x, String url) { 427 CodeSystem cs = getContext().getWorker().fetchCodeSystem(url); 428 if (cs == null) { 429 x.code(url); 430 } else if (cs.hasUserData("path")) { 431 x.ah(cs.getUserString("path")).tx(cs.present()); 432 } else { 433 x.code(url); 434 x.tx(" ("+cs.present()+")"); 435 } 436 } 437 438 @SuppressWarnings("rawtypes") 439 private void generateVersionNotice(XhtmlNode x, ValueSetExpansionComponent expansion, Resource vs) { 440 Multimap<String, String> versions = HashMultimap.create(); 441 for (ValueSetExpansionParameterComponent p : expansion.getParameter()) { 442 if (p.getName().equals("version")) { 443 String[] parts = ((PrimitiveType) p.getValue()).asStringValue().split("\\|"); 444 if (parts.length == 2) 445 versions.put(parts[0], parts[1]); 446 } 447 } 448 if (versions.size() > 0) { 449 XhtmlNode div = null; 450 XhtmlNode ul = null; 451 boolean first = true; 452 for (String s : versions.keySet()) { 453 if (versions.size() == 1 && versions.get(s).size() == 1) { 454 for (String v : versions.get(s)) { // though there'll only be one 455 XhtmlNode p = x.para().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px"); 456 p.tx("Expansion based on "); 457 expRef(p, s, v, vs); 458 } 459 } else { 460 for (String v : versions.get(s)) { 461 if (first) { 462 div = x.div().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px"); 463 div.para().tx("Expansion based on: "); 464 ul = div.ul(); 465 first = false; 466 } 467 expRef(ul.li(), s, v, vs); 468 } 469 } 470 } 471 } 472 } 473 474 private void expRef(XhtmlNode x, String u, String v, Resource source) { 475 // TODO Auto-generated method stub 476 if (u.equals("http://snomed.info/sct")) { 477 String[] parts = v.split("\\/"); 478 if (parts.length >= 5) { 479 String m = describeModule(parts[4]); 480 if (parts.length == 7) { 481 x.tx("SNOMED CT "+m+" edition "+formatSCTDate(parts[6])); 482 } else { 483 x.tx("SNOMED CT "+m+" edition"); 484 } 485 } else { 486 x.tx(describeSystem(u)+" version "+v); 487 } 488 } else if (u.equals("http://loinc.org")) { 489 String vd = describeLoincVer(v); 490 if (vd != null) { 491 x.tx("Loinc v"+v+" ("+vd+")"); 492 } else { 493 x.tx("Loinc v"+v); 494 } 495 } else { 496 CanonicalResource cr = (CanonicalResource) getContext().getWorker().fetchResource(Resource.class, u+"|"+v, source); 497 if (cr != null) { 498 if (cr.hasUserData("path")) { 499 x.ah(cr.getUserString("path")).tx(cr.present()+" v"+v+" ("+cr.fhirType()+")"); 500 } else { 501 x.tx(describeSystem(u)+" v"+v+" ("+cr.fhirType()+")"); 502 } 503 } else { 504 x.tx(describeSystem(u)+" version "+v); 505 } 506 } 507 } 508 509 private String describeLoincVer(String v) { 510 if ("2.67".equals(v)) return "Dec 2019"; 511 if ("2.66".equals(v)) return "Jun 2019"; 512 if ("2.65".equals(v)) return "Dec 2018"; 513 if ("2.64".equals(v)) return "Jun 2018"; 514 if ("2.63".equals(v)) return "Dec 2017"; 515 if ("2.61".equals(v)) return "Jun 2017"; 516 if ("2.59".equals(v)) return "Feb 2017"; 517 if ("2.58".equals(v)) return "Dec 2016"; 518 if ("2.56".equals(v)) return "Jun 2016"; 519 if ("2.54".equals(v)) return "Dec 2015"; 520 if ("2.52".equals(v)) return "Jun 2015"; 521 if ("2.50".equals(v)) return "Dec 2014"; 522 if ("2.48".equals(v)) return "Jun 2014"; 523 if ("2.46".equals(v)) return "Dec 2013"; 524 if ("2.44".equals(v)) return "Jun 2013"; 525 if ("2.42".equals(v)) return "Dec 2012"; 526 if ("2.40".equals(v)) return "Jun 2012"; 527 if ("2.38".equals(v)) return "Dec 2011"; 528 if ("2.36".equals(v)) return "Jun 2011"; 529 if ("2.34".equals(v)) return "Dec 2010"; 530 if ("2.32".equals(v)) return "Jun 2010"; 531 if ("2.30".equals(v)) return "Feb 2010"; 532 if ("2.29".equals(v)) return "Dec 2009"; 533 if ("2.27".equals(v)) return "Jul 2009"; 534 if ("2.26".equals(v)) return "Jan 2009"; 535 if ("2.24".equals(v)) return "Jul 2008"; 536 if ("2.22".equals(v)) return "Dec 2007"; 537 if ("2.21".equals(v)) return "Jun 2007"; 538 if ("2.19".equals(v)) return "Dec 2006"; 539 if ("2.17".equals(v)) return "Jun 2006"; 540 if ("2.16".equals(v)) return "Dec 2005"; 541 if ("2.15".equals(v)) return "Jun 2005"; 542 if ("2.14".equals(v)) return "Dec 2004"; 543 if ("2.13".equals(v)) return "Aug 2004"; 544 if ("2.12".equals(v)) return "Feb 2004"; 545 if ("2.10".equals(v)) return "Oct 2003"; 546 if ("2.09".equals(v)) return "May 2003"; 547 if ("2.08 ".equals(v)) return "Sep 2002"; 548 if ("2.07".equals(v)) return "Aug 2002"; 549 if ("2.05".equals(v)) return "Feb 2002"; 550 if ("2.04".equals(v)) return "Jan 2002"; 551 if ("2.03".equals(v)) return "Jul 2001"; 552 if ("2.02".equals(v)) return "May 2001"; 553 if ("2.01".equals(v)) return "Jan 2001"; 554 if ("2.00".equals(v)) return "Jan 2001"; 555 if ("1.0n".equals(v)) return "Feb 2000"; 556 if ("1.0ma".equals(v)) return "Aug 1999"; 557 if ("1.0m".equals(v)) return "Jul 1999"; 558 if ("1.0l".equals(v)) return "Jan 1998"; 559 if ("1.0ja".equals(v)) return "Oct 1997"; 560 return null; 561 } 562 563 private String formatSCTDate(String ds) { 564 SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd"); 565 Date date; 566 try { 567 date = format.parse(ds); 568 } catch (ParseException e) { 569 return ds; 570 } 571 return new SimpleDateFormat("dd-MMM yyyy").format(date); 572 } 573 574 private String describeModule(String module) { 575 if ("900000000000207008".equals(module)) 576 return "International"; 577 if ("731000124108".equals(module)) 578 return "United States"; 579 if ("32506021000036107".equals(module)) 580 return "Australian"; 581 if ("449081005".equals(module)) 582 return "Spanish"; 583 if ("554471000005108".equals(module)) 584 return "Danish"; 585 if ("11000146104".equals(module)) 586 return "Dutch"; 587 if ("45991000052106".equals(module)) 588 return "Swedish"; 589 if ("999000041000000102".equals(module)) 590 return "United Kingdon"; 591 return module; 592 } 593 594 private boolean hasVersionParameter(ValueSetExpansionComponent expansion) { 595 for (ValueSetExpansionParameterComponent p : expansion.getParameter()) { 596 if (p.getName().equals("version")) 597 return true; 598 } 599 return false; 600 } 601 602 private void addDesignationRow(ValueSetExpansionContainsComponent c, XhtmlNode t, List<String> langs, Map<String, String> designations) { 603 XhtmlNode tr = t.tr(); 604 tr.td().addText(c.getCode()); 605 addDesignationsToRow(c, designations, tr); 606 addLangaugesToRow(c, langs, tr); 607 for (ValueSetExpansionContainsComponent cc : c.getContains()) { 608 addDesignationRow(cc, t, langs, designations); 609 } 610 } 611 612 public void addDesignationsToRow(ValueSetExpansionContainsComponent c, Map<String, String> designations, XhtmlNode tr) { 613 for (String url : designations.keySet()) { 614 String d = null; 615 if (d == null) { 616 for (ConceptReferenceDesignationComponent dd : c.getDesignation()) { 617 if (url.equals(getUrlForDesignation(dd))) { 618 d = dd.getValue(); 619 } 620 } 621 } 622 tr.td().addText(d == null ? "" : d); 623 } 624 } 625 626 public void addLangaugesToRow(ValueSetExpansionContainsComponent c, List<String> langs, XhtmlNode tr) { 627 for (String lang : langs) { 628 String d = null; 629 for (Extension ext : c.getExtension()) { 630 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 631 String l = ToolingExtensions.readStringExtension(ext, "lang"); 632 if (lang.equals(l)) { 633 d = ToolingExtensions.readStringExtension(ext, "content"); 634 } 635 } 636 } 637 if (d == null) { 638 for (ConceptReferenceDesignationComponent dd : c.getDesignation()) { 639 String l = dd.getLanguage(); 640 if (lang.equals(l)) { 641 d = dd.getValue(); 642 } 643 } 644 } 645 tr.td().addText(d == null ? "" : d); 646 } 647 } 648 649 650 private boolean checkDoDefinition(List<ValueSetExpansionContainsComponent> contains) { 651 for (ValueSetExpansionContainsComponent c : contains) { 652 CodeSystem cs = getContext().getWorker().fetchCodeSystem(c.getSystem()); 653 if (cs != null) { 654 ConceptDefinitionComponent cd = CodeSystemUtilities.getCode(cs, c.getCode()); 655 if (cd != null && cd.hasDefinition()) { 656 return true; 657 } 658 } 659 if (checkDoDefinition(c.getContains())) 660 return true; 661 } 662 return false; 663 } 664 665 666 private boolean allFromOneSystem(ValueSet vs) { 667 if (vs.getExpansion().getContains().isEmpty()) 668 return false; 669 String system = vs.getExpansion().getContains().get(0).getSystem(); 670 for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) { 671 if (!checkSystemMatches(system, cc)) 672 return false; 673 } 674 return true; 675 } 676 677 private String getCsRef(String system) { 678 CodeSystem cs = getContext().getWorker().fetchCodeSystem(system); 679 return getCsRef(cs); 680 } 681 682 private <T extends Resource> String getCsRef(T cs) { 683 if (cs == null) { 684 return "?cs-n?"; 685 } 686 String ref = (String) cs.getUserData("filename"); 687 if (ref == null) 688 ref = (String) cs.getUserData("path"); 689 if (ref == null) 690 return "?ngen-14?.html"; 691 if (!ref.contains(".html")) 692 ref = ref + ".html"; 693 return ref.replace("\\", "/"); 694 } 695 696 private void scanForDesignations(ValueSetExpansionContainsComponent c, List<String> langs, Map<String, String> designations) { 697 for (Extension ext : c.getExtension()) { 698 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 699 String lang = ToolingExtensions.readStringExtension(ext, "lang"); 700 if (!Utilities.noString(lang) && !langs.contains(lang)) { 701 langs.add(lang); 702 } 703 } 704 } 705 for (ConceptReferenceDesignationComponent d : c.getDesignation()) { 706 String lang = d.getLanguage(); 707 if (!Utilities.noString(lang) && !langs.contains(lang)) { 708 langs.add(lang); 709 } else { 710 // can we present this as a designation that we know? 711 String disp = getDisplayForDesignation(d); 712 String url = getUrlForDesignation(d); 713 if (disp == null) { 714 disp = getDisplayForUrl(url); 715 } 716 if (disp != null && !designations.containsKey(url) && url != null) { 717 designations.put(url, disp); 718 } 719 } 720 } 721 for (ValueSetExpansionContainsComponent cc : c.getContains()) { 722 scanForDesignations(cc, langs, designations); 723 } 724 } 725 726 private void scanForLangs(ValueSetExpansionContainsComponent c, List<String> langs) { 727 for (Extension ext : c.getExtension()) { 728 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 729 String lang = ToolingExtensions.readStringExtension(ext, "lang"); 730 if (!Utilities.noString(lang) && !langs.contains(lang)) { 731 langs.add(lang); 732 } 733 } 734 } 735 for (ConceptReferenceDesignationComponent d : c.getDesignation()) { 736 String lang = d.getLanguage(); 737 if (!Utilities.noString(lang) && !langs.contains(lang)) { 738 langs.add(lang); 739 } 740 } 741 for (ValueSetExpansionContainsComponent cc : c.getContains()) { 742 scanForLangs(cc, langs); 743 } 744 } 745 746 private void addExpansionRowToTable(XhtmlNode t, ValueSet vs, ValueSetExpansionContainsComponent c, int i, boolean doLevel, boolean doSystem, boolean doDefinition, List<UsedConceptMap> maps, CodeSystem allCS, List<String> langs, Map<String, String> designations, boolean doDesignations, Map<String, String> properties) throws FHIRFormatError, DefinitionException, IOException { 747 XhtmlNode tr = t.tr(); 748 if (ValueSetUtilities.isDeprecated(vs, c)) { 749 tr.setAttribute("style", "background-color: #ffeeee"); 750 } 751 752 XhtmlNode td = tr.td(); 753 754 String tgt = makeAnchor(c.getSystem(), c.getCode()); 755 td.an(tgt); 756 757 if (doLevel) { 758 td.addText(Integer.toString(i)); 759 td = tr.td(); 760 } 761 String s = Utilities.padLeft("", '\u00A0', i*2); 762 td.attribute("style", "white-space:nowrap").addText(s); 763 addCodeToTable(c.getAbstract(), c.getSystem(), c.getCode(), c.getDisplay(), td); 764 if (doSystem) { 765 td = tr.td(); 766 td.addText(c.getSystem()); 767 } 768 td = tr.td(); 769 if (c.hasDisplayElement()) 770 td.addText(c.getDisplay()); 771 772 if (doDefinition) { 773 CodeSystem cs = allCS; 774 if (cs == null) 775 cs = getContext().getWorker().fetchCodeSystem(c.getSystem()); 776 td = tr.td(); 777 if (cs != null) { 778 String defn = CodeSystemUtilities.getCodeDefinition(cs, c.getCode()); 779 addMarkdown(td, defn, cs.getUserString("path")); 780 } 781 } 782 for (String n : Utilities.sorted(properties.keySet())) { 783 td = tr.td(); 784 String ps = getPropertyValue(c, n); 785 if (!Utilities.noString(ps)) { 786 td.addText(ps); 787 } 788 } 789 for (UsedConceptMap m : maps) { 790 td = tr.td(); 791 List<TargetElementComponentWrapper> mappings = findMappingsForCode(c.getCode(), m.getMap()); 792 boolean first = true; 793 for (TargetElementComponentWrapper mapping : mappings) { 794 if (!first) 795 td.br(); 796 first = false; 797 XhtmlNode span = td.span(null, mapping.comp.getRelationship().toString()); 798 span.addText(getCharForRelationship(mapping.comp)); 799 addRefToCode(td, mapping.group.getTarget(), m.getLink(), mapping.comp.getCode()); 800 if (!Utilities.noString(mapping.comp.getComment())) 801 td.i().tx("("+mapping.comp.getComment()+")"); 802 } 803 } 804 if (doDesignations) { 805 addDesignationsToRow(c, designations, tr); 806 addLangaugesToRow(c, langs, tr); 807 } 808 for (ValueSetExpansionContainsComponent cc : c.getContains()) { 809 addExpansionRowToTable(t, vs, cc, i+1, doLevel, doSystem, doDefinition, maps, allCS, langs, designations, doDesignations, properties); 810 } 811 } 812 813 814 815 816 817 private String getPropertyValue(ValueSetExpansionContainsComponent c, String n) { 818 for (ConceptPropertyComponent cp : c.getProperty()) { 819 if (n.equals(cp.getCode())) { 820 return cp.getValue().primitiveValue(); 821 } 822 } 823 return null; 824 } 825 826 private boolean checkSystemMatches(String system, ValueSetExpansionContainsComponent cc) { 827 if (!system.equals(cc.getSystem())) 828 return false; 829 for (ValueSetExpansionContainsComponent cc1 : cc.getContains()) { 830 if (!checkSystemMatches(system, cc1)) 831 return false; 832 } 833 return true; 834 } 835 836 private void addCodeToTable(boolean isAbstract, String system, String code, String display, XhtmlNode td) { 837 CodeSystem e = getContext().getWorker().fetchCodeSystem(system); 838 if (e == null || (e.getContent() != org.hl7.fhir.r5.model.CodeSystem.CodeSystemContentMode.COMPLETE && e.getContent() != org.hl7.fhir.r5.model.CodeSystem.CodeSystemContentMode.FRAGMENT)) { 839 if (isAbstract) 840 td.i().setAttribute("title", ABSTRACT_CODE_HINT).addText(code); 841 else if ("http://snomed.info/sct".equals(system)) { 842 td.ah(sctLink(code)).addText(code); 843 } else if ("http://loinc.org".equals(system)) { 844 td.ah(LoincLinker.getLinkForCode(code)).addText(code); 845 } else 846 td.addText(code); 847 } else { 848 String href = context.fixReference(getCsRef(e)); 849 if (href.contains("#")) 850 href = href + "-"+Utilities.nmtokenize(code); 851 else 852 href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(code); 853 if (isAbstract) 854 td.ah(href).setAttribute("title", ABSTRACT_CODE_HINT).i().addText(code); 855 else 856 td.ah(href).addText(code); 857 } 858 } 859 860 861 public String sctLink(String code) { 862// if (snomedEdition != null) 863// http://browser.ihtsdotools.org/?perspective=full&conceptId1=428041000124106&edition=us-edition&release=v20180301&server=https://prod-browser-exten.ihtsdotools.org/api/snomed&langRefset=900000000000509007 864 return "http://snomed.info/id/"+code; 865 } 866 867 private void addRefToCode(XhtmlNode td, String target, String vslink, String code) { 868 addCodeToTable(false, target, code, null, td); 869// CodeSystem cs = getContext().getWorker().fetchCodeSystem(target); 870// String cslink = getCsRef(cs); 871// String link = cslink != null ? cslink+"#"+cs.getId()+"-"+code : vslink+"#"+code; 872// if (!Utilities.isAbsoluteUrl(link)) { 873// link = getContext().getSpecificationLink()+link; 874// } 875// XhtmlNode a = td.ah(link); 876// a.addText(code); 877 } 878 879 private boolean generateComposition(XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRException, IOException { 880 boolean hasExtensions = false; 881 List<String> langs = new ArrayList<String>(); 882 Map<String, String> designations = new HashMap<>(); // map of url = description, where url is the designation code. Designations that are for languages won't make it into this list 883 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 884 scanDesignations(inc, langs, designations); 885 } 886 for (ConceptSetComponent inc : vs.getCompose().getExclude()) { 887 scanDesignations(inc, langs, designations); 888 } 889 boolean doDesignations = langs.size() + designations.size() < MAX_DESIGNATIONS_IN_LINE; 890 891 if (header) { 892 XhtmlNode h = x.h2(); 893 h.addText(vs.present()); 894 addMarkdown(x, vs.getDescription()); 895 if (vs.hasCopyrightElement()) 896 generateCopyright(x, vs); 897 } 898 int index = 0; 899 if (vs.getCompose().getInclude().size() == 1 && vs.getCompose().getExclude().size() == 0) { 900 hasExtensions = genInclude(x.ul(), vs.getCompose().getInclude().get(0), "Include", langs, doDesignations, maps, designations, index, vs) || hasExtensions; 901 } else { 902 XhtmlNode p = x.para(); 903 p.tx("This value set includes codes based on the following rules:"); 904 XhtmlNode ul = x.ul(); 905 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 906 hasExtensions = genInclude(ul, inc, "Include", langs, doDesignations, maps, designations, index, vs) || hasExtensions; 907 index++; 908 } 909 if (vs.getCompose().hasExclude()) { 910 p = x.para(); 911 p.tx("This value set excludes codes based on the following rules:"); 912 ul = x.ul(); 913 for (ConceptSetComponent exc : vs.getCompose().getExclude()) { 914 hasExtensions = genInclude(ul, exc, "Exclude", langs, doDesignations, maps, designations, index, vs) || hasExtensions; 915 index++; 916 } 917 } 918 } 919 920 // now, build observed languages 921 922 if (!doDesignations && langs.size() + designations.size() > 0) { 923 Collections.sort(langs); 924 if (designations.size() == 0) { 925 x.para().b().tx("Additional Language Displays"); 926 } else if (langs.size() == 0) { 927 x.para().b().tx("Additional Designations"); 928 } else { 929 x.para().b().tx("Additional Designations and Language Displays"); 930 } 931 XhtmlNode t = x.table("codes"); 932 XhtmlNode tr = t.tr(); 933 tr.td().b().tx("Code"); 934 for (String url : designations.keySet()) { 935 tr.td().b().addText(designations.get(url)); 936 } 937 for (String lang : langs) { 938 tr.td().b().addText(describeLang(lang)); 939 } 940 for (ConceptSetComponent c : vs.getCompose().getInclude()) { 941 for (ConceptReferenceComponent cc : c.getConcept()) { 942 addDesignationRow(cc, t, langs, designations); 943 } 944 } 945 } 946 947 948 return hasExtensions; 949 } 950 951 private void renderExpansionRules(XhtmlNode x, ConceptSetComponent inc, int index, Map<String, ConceptDefinitionComponent> definitions) throws FHIRException, IOException { 952 String s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface, but the rules are not properly defined"; 953 if (inc.hasExtension(ToolingExtensions.EXT_EXPAND_RULES)) { 954 String rule = inc.getExtensionString(ToolingExtensions.EXT_EXPAND_RULES); 955 if (rule != null) { 956 switch (rule) { 957 case "all-codes": s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface. The expansion contains all the codes, and also this structure:"; 958 case "ungrouped": s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface. The expansion contains this structure, and any codes not found in the structure:"; 959 case "groups-only": s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface. The expansion contains this structure:"; 960 } 961 } 962 } 963 x.br(); 964 x.tx(s); 965 HierarchicalTableGenerator gen = new HierarchicalTableGenerator(context.getDestDir(), context.isInlineGraphics(), true); 966 TableModel model = gen.new TableModel("exp.h="+index, context.getRules() == GenerationRules.IG_PUBLISHER); 967 model.setAlternating(true); 968 model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("vs.exp.header", "Code"), translate("vs.exp.hint", "The code for the item"), null, 0)); 969 model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("vs.exp.header", "Display"), translate("vs.exp.hint", "The display for the item"), null, 0)); 970 971 for (Extension ext : inc.getExtensionsByUrl(ToolingExtensions.EXT_EXPAND_GROUP)) { 972 renderExpandGroup(gen, model, ext, inc, definitions); 973 } 974 x.br(); 975 x.tx("table"); 976 XhtmlNode xn = gen.generate(model, context.getLocalPrefix(), 1, null); 977 x.getChildNodes().add(xn); 978 } 979 980 private void renderExpandGroup(HierarchicalTableGenerator gen, TableModel model, Extension ext, ConceptSetComponent inc, Map<String, ConceptDefinitionComponent> definitions) { 981 Row row = gen.new Row(); 982 model.getRows().add(row); 983 row.setIcon("icon_entry_blue.png", "entry"); 984 String code = ext.getExtensionString("code"); 985 if (code != null) { 986 row.getCells().add(gen.new Cell(null, null, code, null, null)); 987 row.getCells().add(gen.new Cell(null, null, getDisplayForCode(inc, code, definitions), null, null)); 988 } else if (ext.hasId()) { 989 row.getCells().add(gen.new Cell(null, null, "(#"+ext.getId()+")", null, null)); 990 row.getCells().add(gen.new Cell(null, null, ext.getExtensionString("display"), null, null)); 991 } else { 992 row.getCells().add(gen.new Cell(null, null, null, null, null)); 993 row.getCells().add(gen.new Cell(null, null, ext.getExtensionString("display"), null, null)); 994 } 995 for (Extension member : ext.getExtensionsByUrl("member")) { 996 Row subRow = gen.new Row(); 997 row.getSubRows().add(subRow); 998 subRow.setIcon("icon_entry_blue.png", "entry"); 999 String mc = member.getValue().primitiveValue(); 1000 // mc might be a reference to another expansion group - we check that first, or to a code in the compose 1001 if (mc.startsWith("#")) { 1002 // it's a reference by id 1003 subRow.getCells().add(gen.new Cell(null, null, "("+mc+")", null, null)); 1004 subRow.getCells().add(gen.new Cell(null, null, "group reference by id", null, null)); 1005 } else { 1006 Extension tgt = findTargetByCode(inc, mc); 1007 if (tgt != null) { 1008 subRow.getCells().add(gen.new Cell(null, null, mc, null, null)); 1009 subRow.getCells().add(gen.new Cell(null, null, "group reference by code", null, null)); 1010 } else { 1011 subRow.getCells().add(gen.new Cell(null, null, mc, null, null)); 1012 subRow.getCells().add(gen.new Cell(null, null, getDisplayForCode(inc, mc, definitions), null, null)); 1013 } 1014 } 1015 } 1016 } 1017 1018 private Extension findTargetByCode(ConceptSetComponent inc, String mc) { 1019 for (Extension ext : inc.getExtensionsByUrl(ToolingExtensions.EXT_EXPAND_GROUP)) { 1020 String code = ext.getExtensionString("code"); 1021 if (mc.equals(code)) { 1022 return ext; 1023 } 1024 } 1025 return null; 1026 } 1027 1028 private String getDisplayForCode(ConceptSetComponent inc, String code, Map<String, ConceptDefinitionComponent> definitions) { 1029 for (ConceptReferenceComponent cc : inc.getConcept()) { 1030 if (code.equals(cc.getCode())) { 1031 if (cc.hasDisplay()) { 1032 return cc.getDisplay(); 1033 } 1034 } 1035 } 1036 if (definitions.containsKey(code)) { 1037 return definitions.get(code).getDisplay(); 1038 } 1039 return null; 1040 } 1041 1042 private void scanDesignations(ConceptSetComponent inc, List<String> langs, Map<String, String> designations) { 1043 for (ConceptReferenceComponent cc : inc.getConcept()) { 1044 for (Extension ext : cc.getExtension()) { 1045 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 1046 String lang = ToolingExtensions.readStringExtension(ext, "lang"); 1047 if (!Utilities.noString(lang) && !langs.contains(lang)) { 1048 langs.add(lang); 1049 } 1050 } 1051 } 1052 for (ConceptReferenceDesignationComponent d : cc.getDesignation()) { 1053 String lang = d.getLanguage(); 1054 if (!Utilities.noString(lang) && !langs.contains(lang)) { 1055 langs.add(lang); 1056 } else { 1057 // can we present this as a designation that we know? 1058 String disp = getDisplayForDesignation(d); 1059 String url = getUrlForDesignation(d); 1060 if (disp == null) { 1061 disp = getDisplayForUrl(url); 1062 } 1063 if (disp != null && !designations.containsKey(url)) { 1064 designations.put(url, disp); 1065 } 1066 } 1067 } 1068 } 1069 } 1070 1071 private String getDisplayForUrl(String url) { 1072 if (url == null) { 1073 return null; 1074 } 1075 switch (url) { 1076 case "http://snomed.info/sct#900000000000003001": 1077 return "Fully specified name"; 1078 case "http://snomed.info/sct#900000000000013009": 1079 return "Synonym"; 1080 default: 1081 // As specified in http://www.hl7.org/fhir/valueset-definitions.html#ValueSet.compose.include.concept.designation.use and in http://www.hl7.org/fhir/codesystem-definitions.html#CodeSystem.concept.designation.use the terminology binding is extensible. 1082 return url; 1083 } 1084 } 1085 1086 private String getUrlForDesignation(ConceptReferenceDesignationComponent d) { 1087 if (d.hasUse() && d.getUse().hasSystem() && d.getUse().hasCode()) { 1088 return d.getUse().getSystem()+"#"+d.getUse().getCode(); 1089 } else { 1090 return null; 1091 } 1092 } 1093 1094 private String getDisplayForDesignation(ConceptReferenceDesignationComponent d) { 1095 if (d.hasUse() && d.getUse().hasDisplay()) { 1096 return d.getUse().getDisplay(); 1097 } else { 1098 return null; 1099 } 1100 } 1101 1102 private boolean genInclude(XhtmlNode ul, ConceptSetComponent inc, String type, List<String> langs, boolean doDesignations, List<UsedConceptMap> maps, Map<String, String> designations, int index, Resource vsRes) throws FHIRException, IOException { 1103 boolean hasExtensions = false; 1104 XhtmlNode li; 1105 li = ul.li(); 1106 CodeSystem e = getContext().getWorker().fetchCodeSystem(inc.getSystem()); 1107 Map<String, ConceptDefinitionComponent> definitions = new HashMap<>(); 1108 1109 if (inc.hasSystem()) { 1110 if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) { 1111 li.addText(type+" all codes defined in "); 1112 addCsRef(inc, li, e); 1113 } else { 1114 if (inc.getConcept().size() > 0) { 1115 li.addText(type+" these codes as defined in "); 1116 addCsRef(inc, li, e); 1117 if (inc.hasVersion()) { 1118 li.addText(" version "); 1119 li.code(inc.getVersion()); 1120 } 1121 1122 // for performance reasons, we do all the fetching in one batch 1123 definitions = getConceptsForCodes(e, inc); 1124 1125 1126 XhtmlNode t = li.table("none"); 1127 boolean hasComments = false; 1128 boolean hasDefinition = false; 1129 for (ConceptReferenceComponent c : inc.getConcept()) { 1130 hasComments = hasComments || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT); 1131 ConceptDefinitionComponent cc = definitions == null ? null : definitions.get(c.getCode()); 1132 hasDefinition = hasDefinition || ((cc != null && cc.hasDefinition()) || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION)); 1133 } 1134 if (hasComments || hasDefinition) 1135 hasExtensions = true; 1136 addMapHeaders(addTableHeaderRowStandard(t, false, true, hasDefinition, hasComments, false, false, null, langs, designations, doDesignations), maps); 1137 for (ConceptReferenceComponent c : inc.getConcept()) { 1138 XhtmlNode tr = t.tr(); 1139 XhtmlNode td = tr.td(); 1140 ConceptDefinitionComponent cc = definitions == null ? null : definitions.get(c.getCode()); 1141 addCodeToTable(false, inc.getSystem(), c.getCode(), c.hasDisplay()? c.getDisplay() : cc != null ? cc.getDisplay() : "", td); 1142 1143 td = tr.td(); 1144 if (!Utilities.noString(c.getDisplay())) 1145 td.addText(c.getDisplay()); 1146 else if (cc != null && !Utilities.noString(cc.getDisplay())) 1147 td.addText(cc.getDisplay()); 1148 1149 if (hasDefinition) { 1150 td = tr.td(); 1151 if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION)) { 1152 smartAddText(td, ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_DEFINITION)); 1153 } else if (cc != null && !Utilities.noString(cc.getDefinition())) { 1154 smartAddText(td, cc.getDefinition()); 1155 } 1156 } 1157 if (hasComments) { 1158 td = tr.td(); 1159 if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT)) { 1160 smartAddText(td, "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_VS_COMMENT)); 1161 } 1162 } 1163 if (doDesignations) { 1164 addDesignationsToRow(c, designations, tr); 1165 addLangaugesToRow(c, langs, tr); 1166 } 1167 for (UsedConceptMap m : maps) { 1168 td = tr.td(); 1169 List<TargetElementComponentWrapper> mappings = findMappingsForCode(c.getCode(), m.getMap()); 1170 boolean first = true; 1171 for (TargetElementComponentWrapper mapping : mappings) { 1172 if (!first) 1173 td.br(); 1174 first = false; 1175 XhtmlNode span = td.span(null, mapping.comp.getRelationship().toString()); 1176 span.addText(getCharForRelationship(mapping.comp)); 1177 addRefToCode(td, mapping.group.getTarget(), m.getLink(), mapping.comp.getCode()); 1178 if (!Utilities.noString(mapping.comp.getComment())) 1179 td.i().tx("("+mapping.comp.getComment()+")"); 1180 } 1181 } 1182 } 1183 } 1184 if (inc.getFilter().size() > 0) { 1185 li.addText(type+" codes from "); 1186 addCsRef(inc, li, e); 1187 li.tx(" where "); 1188 for (int i = 0; i < inc.getFilter().size(); i++) { 1189 ConceptSetFilterComponent f = inc.getFilter().get(i); 1190 if (i > 0) { 1191 if (i == inc.getFilter().size()-1) { 1192 li.tx(" and "); 1193 } else { 1194 li.tx(", "); 1195 } 1196 } 1197 if (f.getOp() == FilterOperator.EXISTS) { 1198 if (f.getValue().equals("true")) { 1199 li.tx(f.getProperty()+" exists"); 1200 } else { 1201 li.tx(f.getProperty()+" doesn't exist"); 1202 } 1203 } else { 1204 li.tx(f.getProperty()+" "+describe(f.getOp())+" "); 1205 if (e != null && codeExistsInValueSet(e, f.getValue())) { 1206 String href = getContext().fixReference(getCsRef(e)); 1207 if (href.contains("#")) 1208 href = href + "-"+Utilities.nmtokenize(f.getValue()); 1209 else 1210 href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(f.getValue()); 1211 li.ah(href).addText(f.getValue()); 1212 } else if ("concept".equals(f.getProperty()) && inc.hasSystem()) { 1213 li.addText(f.getValue()); 1214 ValidationResult vr = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions(), inc.getSystem(), inc.getVersion(), f.getValue(), null); 1215 if (vr.isOk()) { 1216 li.tx(" ("+vr.getDisplay()+")"); 1217 } 1218 } 1219 else 1220 li.addText(f.getValue()); 1221 String disp = ToolingExtensions.getDisplayHint(f); 1222 if (disp != null) 1223 li.tx(" ("+disp+")"); 1224 } 1225 } 1226 } 1227 } 1228 if (inc.hasValueSet()) { 1229 li.tx(", where the codes are contained in "); 1230 boolean first = true; 1231 for (UriType vs : inc.getValueSet()) { 1232 if (first) 1233 first = false; 1234 else 1235 li.tx(", "); 1236 AddVsRef(vs.asStringValue(), li, vsRes); 1237 } 1238 } 1239 if (inc.hasExtension(ToolingExtensions.EXT_EXPAND_RULES) || inc.hasExtension(ToolingExtensions.EXT_EXPAND_GROUP)) { 1240 hasExtensions = true; 1241 renderExpansionRules(li, inc, index, definitions); 1242 } 1243 } else { 1244 li.tx("Import all the codes that are contained in "); 1245 if (inc.getValueSet().size() < 4) { 1246 boolean first = true; 1247 for (UriType vs : inc.getValueSet()) { 1248 if (first) 1249 first = false; 1250 else 1251 li.tx(", "); 1252 AddVsRef(vs.asStringValue(), li, vsRes); 1253 } 1254 } else { 1255 XhtmlNode xul = li.ul(); 1256 for (UriType vs : inc.getValueSet()) { 1257 AddVsRef(vs.asStringValue(), xul.li(), vsRes); 1258 } 1259 1260 } 1261 } 1262 return hasExtensions; 1263 } 1264 1265 public void addDesignationsToRow(ConceptReferenceComponent c, Map<String, String> designations, XhtmlNode tr) { 1266 for (String url : designations.keySet()) { 1267 String d = null; 1268 if (d == null) { 1269 for (ConceptReferenceDesignationComponent dd : c.getDesignation()) { 1270 if (url.equals(getUrlForDesignation(dd))) { 1271 d = dd.getValue(); 1272 } 1273 } 1274 } 1275 tr.td().addText(d == null ? "" : d); 1276 } 1277 } 1278 1279 public void addLangaugesToRow(ConceptReferenceComponent c, List<String> langs, XhtmlNode tr) { 1280 for (String lang : langs) { 1281 String d = null; 1282 for (Extension ext : c.getExtension()) { 1283 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 1284 String l = ToolingExtensions.readStringExtension(ext, "lang"); 1285 if (lang.equals(l)) { 1286 d = ToolingExtensions.readStringExtension(ext, "content"); 1287 } 1288 } 1289 } 1290 if (d == null) { 1291 for (ConceptReferenceDesignationComponent dd : c.getDesignation()) { 1292 String l = dd.getLanguage(); 1293 if (lang.equals(l)) { 1294 d = dd.getValue(); 1295 } 1296 } 1297 } 1298 tr.td().addText(d == null ? "" : d); 1299 } 1300 } 1301 1302 1303 private Map<String, ConceptDefinitionComponent> getConceptsForCodes(CodeSystem e, ConceptSetComponent inc) { 1304 if (e == null) { 1305 e = getContext().getWorker().fetchCodeSystem(inc.getSystem()); 1306 } 1307 1308 ValueSetExpansionComponent vse = null; 1309 if (!context.isNoSlowLookup()) { // && !getContext().getWorker().hasCache()) { removed GG 20220107 like what is this trying to do? 1310 try { 1311 ValueSetExpansionOutcome vso = getContext().getWorker().expandVS(inc, false, false); 1312 ValueSet valueset = vso.getValueset(); 1313 if (valueset == null) 1314 throw new TerminologyServiceException("Error Expanding ValueSet: "+vso.getError()); 1315 vse = valueset.getExpansion(); 1316 1317 } catch (TerminologyServiceException e1) { 1318 return null; 1319 } 1320 } 1321 1322 Map<String, ConceptDefinitionComponent> results = new HashMap<>(); 1323 List<CodingValidationRequest> serverList = new ArrayList<>(); 1324 1325 // 1st pass, anything we can resolve internally 1326 for (ConceptReferenceComponent cc : inc.getConcept()) { 1327 String code = cc.getCode(); 1328 ConceptDefinitionComponent v = null; 1329 if (e != null && code != null) { 1330 v = getConceptForCode(e.getConcept(), code); 1331 } 1332 if (v == null && vse != null) { 1333 v = getConceptForCodeFromExpansion(vse.getContains(), code); 1334 } 1335 if (v != null) { 1336 results.put(code, v); 1337 } else { 1338 serverList.add(new CodingValidationRequest(new Coding(inc.getSystem(), code, null))); 1339 } 1340 } 1341 if (!context.isNoSlowLookup() && !serverList.isEmpty()) { 1342 getContext().getWorker().validateCodeBatch(getContext().getTerminologyServiceOptions(), serverList, null); 1343 for (CodingValidationRequest vr : serverList) { 1344 ConceptDefinitionComponent v = vr.getResult().asConceptDefinition(); 1345 if (v != null) { 1346 results.put(vr.getCoding().getCode(), v); 1347 } 1348 } 1349 } 1350 return results; 1351 } 1352 1353 private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> list, String code) { 1354 for (ConceptDefinitionComponent c : list) { 1355 if (code.equals(c.getCode())) 1356 return c; 1357 ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code); 1358 if (v != null) 1359 return v; 1360 } 1361 return null; 1362 } 1363 1364 private ConceptDefinitionComponent getConceptForCodeFromExpansion(List<ValueSetExpansionContainsComponent> list, String code) { 1365 for (ValueSetExpansionContainsComponent c : list) { 1366 if (code.equals(c.getCode())) { 1367 ConceptDefinitionComponent res = new ConceptDefinitionComponent(); 1368 res.setCode(c.getCode()); 1369 res.setDisplay(c.getDisplay()); 1370 return res; 1371 } 1372 ConceptDefinitionComponent v = getConceptForCodeFromExpansion(c.getContains(), code); 1373 if (v != null) 1374 return v; 1375 } 1376 return null; 1377 } 1378 1379 1380 private boolean codeExistsInValueSet(CodeSystem cs, String code) { 1381 for (ConceptDefinitionComponent c : cs.getConcept()) { 1382 if (inConcept(code, c)) 1383 return true; 1384 } 1385 return false; 1386 } 1387 1388 1389 1390 private void addDesignationRow(ConceptReferenceComponent c, XhtmlNode t, List<String> langs, Map<String, String> designations) { 1391 XhtmlNode tr = t.tr(); 1392 tr.td().addText(c.getCode()); 1393 addDesignationsToRow(c, designations, tr); 1394 addLangaugesToRow(c, langs, tr); 1395 } 1396 1397 1398 private String describe(FilterOperator op) { 1399 if (op == null) 1400 return " null "; 1401 switch (op) { 1402 case EQUAL: return " = "; 1403 case ISA: return " is-a "; 1404 case ISNOTA: return " is-not-a "; 1405 case REGEX: return " matches (by regex) "; 1406 case NULL: return " ?ngen-13? "; 1407 case IN: return " in "; 1408 case NOTIN: return " not in "; 1409 case DESCENDENTOF: return " descends from "; 1410 case EXISTS: return " exists "; 1411 case GENERALIZES: return " generalizes "; 1412 } 1413 return null; 1414 } 1415 1416 1417 1418 1419 1420 private boolean inConcept(String code, ConceptDefinitionComponent c) { 1421 if (c.hasCodeElement() && c.getCode().equals(code)) 1422 return true; 1423 for (ConceptDefinitionComponent g : c.getConcept()) { 1424 if (inConcept(code, g)) 1425 return true; 1426 } 1427 return false; 1428 } 1429 1430 1431}