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