001package org.hl7.fhir.r4.utils; 002 003/*- 004 * #%L 005 * org.hl7.fhir.r4 006 * %% 007 * Copyright (C) 2014 - 2019 Health Level 7 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023 024import org.apache.commons.codec.binary.Base64; 025import org.apache.commons.io.output.ByteArrayOutputStream; 026import org.apache.commons.lang3.NotImplementedException; 027import org.hl7.fhir.exceptions.DefinitionException; 028import org.hl7.fhir.exceptions.FHIRException; 029import org.hl7.fhir.exceptions.FHIRFormatError; 030import org.hl7.fhir.exceptions.TerminologyServiceException; 031import org.hl7.fhir.r4.conformance.ProfileUtilities; 032import org.hl7.fhir.r4.conformance.ProfileUtilities.ProfileKnowledgeProvider; 033import org.hl7.fhir.r4.context.IWorkerContext; 034import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult; 035import org.hl7.fhir.r4.formats.FormatUtilities; 036import org.hl7.fhir.r4.formats.IParser.OutputStyle; 037import org.hl7.fhir.r4.formats.XmlParser; 038import org.hl7.fhir.r4.model.*; 039import org.hl7.fhir.r4.model.Bundle.*; 040import org.hl7.fhir.r4.model.CapabilityStatement.*; 041import org.hl7.fhir.r4.model.CodeSystem.*; 042import org.hl7.fhir.r4.model.CompartmentDefinition.CompartmentDefinitionResourceComponent; 043import org.hl7.fhir.r4.model.Composition.SectionComponent; 044import org.hl7.fhir.r4.model.ConceptMap.ConceptMapGroupComponent; 045import org.hl7.fhir.r4.model.ConceptMap.OtherElementComponent; 046import org.hl7.fhir.r4.model.ConceptMap.SourceElementComponent; 047import org.hl7.fhir.r4.model.ConceptMap.TargetElementComponent; 048import org.hl7.fhir.r4.model.Enumeration; 049import org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem; 050import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence; 051import org.hl7.fhir.r4.model.HumanName.NameUse; 052import org.hl7.fhir.r4.model.Narrative.NarrativeStatus; 053import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent; 054import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; 055import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; 056import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; 057import org.hl7.fhir.r4.model.Timing.EventTiming; 058import org.hl7.fhir.r4.model.Timing.TimingRepeatComponent; 059import org.hl7.fhir.r4.model.Timing.UnitsOfTime; 060import org.hl7.fhir.r4.model.ValueSet.FilterOperator; 061import org.hl7.fhir.r4.model.ValueSet.*; 062import org.hl7.fhir.r4.terminologies.CodeSystemUtilities; 063import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 064import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext; 065import org.hl7.fhir.r4.utils.LiquidEngine.LiquidDocument; 066import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 067import org.hl7.fhir.utilities.MarkDownProcessor; 068import org.hl7.fhir.utilities.MarkDownProcessor.Dialect; 069import org.hl7.fhir.utilities.TerminologyServiceOptions; 070import org.hl7.fhir.utilities.Utilities; 071import org.hl7.fhir.utilities.xhtml.NodeType; 072import org.hl7.fhir.utilities.xhtml.XhtmlComposer; 073import org.hl7.fhir.utilities.xhtml.XhtmlNode; 074import org.hl7.fhir.utilities.xhtml.XhtmlParser; 075import org.hl7.fhir.utilities.xml.XMLUtil; 076import org.hl7.fhir.utilities.xml.XmlGenerator; 077import org.w3c.dom.Element; 078 079import java.io.IOException; 080import java.io.UnsupportedEncodingException; 081import java.text.ParseException; 082import java.text.SimpleDateFormat; 083import java.util.*; 084 085/* 086Copyright (c) 2011+, HL7, Inc 087 All rights reserved. 088 089 Redistribution and use in source and binary forms, with or without modification, 090 are permitted provided that the following conditions are met: 091 092 * Redistributions of source code must retain the above copyright notice, this 093 list of conditions and the following disclaimer. 094 * Redistributions in binary form must reproduce the above copyright notice, 095 this list of conditions and the following disclaimer in the documentation 096 and/or other materials provided with the distribution. 097 * Neither the name of HL7 nor the names of its contributors may be used to 098 endorse or promote products derived from this software without specific 099 prior written permission. 100 101 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 102 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 103 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 104 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 105 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 106 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 107 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 108 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 109 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 110 POSSIBILITY OF SUCH DAMAGE. 111 112*/ 113 114public class NarrativeGenerator implements INarrativeGenerator { 115 116 public interface ILiquidTemplateProvider { 117 118 String findTemplate(ResourceContext rcontext, DomainResource r); 119 120 } 121 122 public interface ITypeParser { 123 Base parseType(String xml, String type) throws FHIRFormatError, IOException, FHIRException ; 124 } 125 126 public class ConceptMapRenderInstructions { 127 private String name; 128 private String url; 129 private boolean doDescription; 130 public ConceptMapRenderInstructions(String name, String url, boolean doDescription) { 131 super(); 132 this.name = name; 133 this.url = url; 134 this.doDescription = doDescription; 135 } 136 public String getName() { 137 return name; 138 } 139 public String getUrl() { 140 return url; 141 } 142 public boolean isDoDescription() { 143 return doDescription; 144 } 145 146 } 147 148 public class UsedConceptMap { 149 150 private ConceptMapRenderInstructions details; 151 private String link; 152 private ConceptMap map; 153 public UsedConceptMap(ConceptMapRenderInstructions details, String link, ConceptMap map) { 154 super(); 155 this.details = details; 156 this.link = link; 157 this.map = map; 158 } 159 public ConceptMapRenderInstructions getDetails() { 160 return details; 161 } 162 public ConceptMap getMap() { 163 return map; 164 } 165 public String getLink() { 166 return link; 167 } 168 } 169 170 public static class ResourceContext { 171 Bundle bundleResource; 172 173 DomainResource resourceResource; 174 175 public ResourceContext(Bundle bundle, DomainResource dr) { 176 super(); 177 this.bundleResource = bundle; 178 this.resourceResource = dr; 179 } 180 181 public ResourceContext(Element bundle, Element doc) { 182 } 183 184 public ResourceContext(org.hl7.fhir.r4.elementmodel.Element bundle, org.hl7.fhir.r4.elementmodel.Element er) { 185 } 186 187 public Resource resolve(String value) { 188 if (value.startsWith("#")) { 189 for (Resource r : resourceResource.getContained()) { 190 if (r.getId().equals(value.substring(1))) 191 return r; 192 } 193 return null; 194 } 195 if (bundleResource != null) { 196 for (BundleEntryComponent be : bundleResource.getEntry()) { 197 if (be.getFullUrl().equals(value)) 198 return be.getResource(); 199 if (value.equals(be.getResource().fhirType()+"/"+be.getResource().getId())) 200 return be.getResource(); 201 } 202 } 203 return null; 204 } 205 206 } 207 208 private static final String ABSTRACT_CODE_HINT = "This code is not selectable ('Abstract')"; 209 210 public interface IReferenceResolver { 211 212 ResourceWithReference resolve(String url); 213 214 } 215 216 private Bundle bundle; 217 private String definitionsTarget; 218 private String corePath; 219 private String destDir; 220 private String snomedEdition; 221 private ProfileKnowledgeProvider pkp; 222 private MarkDownProcessor markdown = new MarkDownProcessor(Dialect.COMMON_MARK); 223 private ITypeParser parser; // when generating for an element model 224 private ILiquidTemplateProvider templateProvider; 225 private IEvaluationContext services; 226 227 public boolean generate(Bundle b, boolean evenIfAlreadyHasNarrative, Set<String> outputTracker) throws EOperationOutcome, FHIRException, IOException { 228 boolean res = false; 229 this.bundle = b; 230 for (BundleEntryComponent be : b.getEntry()) { 231 if (be.hasResource() && be.getResource() instanceof DomainResource) { 232 DomainResource dr = (DomainResource) be.getResource(); 233 if (evenIfAlreadyHasNarrative || !dr.getText().hasDiv()) 234 res = generate(new ResourceContext(b, dr), dr, outputTracker) || res; 235 } 236 } 237 return res; 238 } 239 240 public boolean generate(DomainResource r, Set<String> outputTracker) throws EOperationOutcome, FHIRException, IOException { 241 return generate(null, r, outputTracker); 242 } 243 244 public boolean generate(ResourceContext rcontext, DomainResource r, Set<String> outputTracker) throws EOperationOutcome, FHIRException, IOException { 245 if (rcontext == null) 246 rcontext = new ResourceContext(null, r); 247 248 if (templateProvider != null) { 249 String liquidTemplate = templateProvider.findTemplate(rcontext, r); 250 if (liquidTemplate != null) { 251 return generateByLiquid(rcontext, r, liquidTemplate, outputTracker); 252 } 253 } 254 if (r instanceof ConceptMap) { 255 return generate(rcontext, (ConceptMap) r); // Maintainer = Grahame 256 } else if (r instanceof ValueSet) { 257 return generate(rcontext, (ValueSet) r, true); // Maintainer = Grahame 258 } else if (r instanceof CodeSystem) { 259 return generate(rcontext, (CodeSystem) r, true, null); // Maintainer = Grahame 260 } else if (r instanceof OperationOutcome) { 261 return generate(rcontext, (OperationOutcome) r); // Maintainer = Grahame 262 } else if (r instanceof CapabilityStatement) { 263 return generate(rcontext, (CapabilityStatement) r); // Maintainer = Grahame 264 } else if (r instanceof CompartmentDefinition) { 265 return generate(rcontext, (CompartmentDefinition) r); // Maintainer = Grahame 266 } else if (r instanceof OperationDefinition) { 267 return generate(rcontext, (OperationDefinition) r); // Maintainer = Grahame 268 } else if (r instanceof StructureDefinition) { 269 return generate(rcontext, (StructureDefinition) r, outputTracker); // Maintainer = Grahame 270 } else if (r instanceof ImplementationGuide) { 271 return generate(rcontext, (ImplementationGuide) r); // Maintainer = Lloyd (until Grahame wants to take over . . . :)) 272 } else if (r instanceof DiagnosticReport) { 273 inject(r, generateDiagnosticReport(new ResourceWrapperDirect(r)), NarrativeStatus.GENERATED); // Maintainer = Grahame 274 return true; 275 } else { 276 StructureDefinition p = null; 277 if (r.hasMeta()) 278 for (UriType pu : r.getMeta().getProfile()) 279 if (p == null) 280 p = context.fetchResource(StructureDefinition.class, pu.getValue()); 281 if (p == null) 282 p = context.fetchResource(StructureDefinition.class, r.getResourceType().toString()); 283 if (p == null) 284 p = context.fetchTypeDefinition(r.getResourceType().toString().toLowerCase()); 285 if (p != null) 286 return generateByProfile(rcontext, p, true); 287 else 288 return false; 289 } 290 } 291 292 private boolean generateByLiquid(ResourceContext rcontext, DomainResource r, String liquidTemplate, Set<String> outputTracker) { 293 294 LiquidEngine engine = new LiquidEngine(context, services); 295 XhtmlNode x; 296 try { 297 LiquidDocument doc = engine.parse(liquidTemplate, "template"); 298 String html = engine.evaluate(doc, r, rcontext); 299 x = new XhtmlParser().parseFragment(html); 300 if (!x.getName().equals("div")) 301 throw new FHIRException("Error in template: Root element is not 'div'"); 302 } catch (FHIRException | IOException e) { 303 x = new XhtmlNode(NodeType.Element, "div"); 304 x.para().b().setAttribute("style", "color: maroon").tx("Exception generating Narrative: "+e.getMessage()); 305 } 306 inject(r, x, NarrativeStatus.GENERATED); 307 return true; 308 } 309 310 private interface PropertyWrapper { 311 public String getName(); 312 public boolean hasValues(); 313 public List<BaseWrapper> getValues(); 314 public String getTypeCode(); 315 public String getDefinition(); 316 public int getMinCardinality(); 317 public int getMaxCardinality(); 318 public StructureDefinition getStructure(); 319 public BaseWrapper value(); 320 } 321 322 private interface ResourceWrapper { 323 public List<ResourceWrapper> getContained(); 324 public String getId(); 325 public XhtmlNode getNarrative() throws FHIRFormatError, IOException, FHIRException; 326 public String getName(); 327 public List<PropertyWrapper> children(); 328 } 329 330 private interface BaseWrapper { 331 public Base getBase() throws UnsupportedEncodingException, IOException, FHIRException; 332 public List<PropertyWrapper> children(); 333 public PropertyWrapper getChildByName(String tail); 334 } 335 336 private class BaseWrapperElement implements BaseWrapper { 337 private Element element; 338 private String type; 339 private StructureDefinition structure; 340 private ElementDefinition definition; 341 private List<ElementDefinition> children; 342 private List<PropertyWrapper> list; 343 344 public BaseWrapperElement(Element element, String type, StructureDefinition structure, ElementDefinition definition) { 345 this.element = element; 346 this.type = type; 347 this.structure = structure; 348 this.definition = definition; 349 } 350 351 @Override 352 public Base getBase() throws UnsupportedEncodingException, IOException, FHIRException { 353 if (type == null || type.equals("Resource") || type.equals("BackboneElement") || type.equals("Element")) 354 return null; 355 356 String xml; 357 try { 358 xml = new XmlGenerator().generate(element); 359 } catch (org.hl7.fhir.exceptions.FHIRException e) { 360 throw new FHIRException(e.getMessage(), e); 361 } 362 return parseType(xml, type); 363 } 364 365 @Override 366 public List<PropertyWrapper> children() { 367 if (list == null) { 368 children = ProfileUtilities.getChildList(structure, definition); 369 list = new ArrayList<NarrativeGenerator.PropertyWrapper>(); 370 for (ElementDefinition child : children) { 371 List<Element> elements = new ArrayList<Element>(); 372 XMLUtil.getNamedChildrenWithWildcard(element, tail(child.getPath()), elements); 373 list.add(new PropertyWrapperElement(structure, child, elements)); 374 } 375 } 376 return list; 377 } 378 379 @Override 380 public PropertyWrapper getChildByName(String name) { 381 for (PropertyWrapper p : children()) 382 if (p.getName().equals(name)) 383 return p; 384 return null; 385 } 386 387 } 388 389 private class PropertyWrapperElement implements PropertyWrapper { 390 391 private StructureDefinition structure; 392 private ElementDefinition definition; 393 private List<Element> values; 394 private List<BaseWrapper> list; 395 396 public PropertyWrapperElement(StructureDefinition structure, ElementDefinition definition, List<Element> values) { 397 this.structure = structure; 398 this.definition = definition; 399 this.values = values; 400 } 401 402 @Override 403 public String getName() { 404 return tail(definition.getPath()); 405 } 406 407 @Override 408 public boolean hasValues() { 409 return values.size() > 0; 410 } 411 412 @Override 413 public List<BaseWrapper> getValues() { 414 if (list == null) { 415 list = new ArrayList<NarrativeGenerator.BaseWrapper>(); 416 for (Element e : values) 417 list.add(new BaseWrapperElement(e, determineType(e), structure, definition)); 418 } 419 return list; 420 } 421 private String determineType(Element e) { 422 if (definition.getType().isEmpty()) 423 return null; 424 if (definition.getType().size() == 1) { 425 if (definition.getType().get(0).getWorkingCode().equals("Element") || definition.getType().get(0).getWorkingCode().equals("BackboneElement")) 426 return null; 427 return definition.getType().get(0).getWorkingCode(); 428 } 429 String t = e.getNodeName().substring(tail(definition.getPath()).length()-3); 430 431 if (isPrimitive(Utilities.uncapitalize(t))) 432 return Utilities.uncapitalize(t); 433 else 434 return t; 435 } 436 437 private boolean isPrimitive(String code) { 438 StructureDefinition sd = context.fetchTypeDefinition(code); 439 return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; 440 } 441 442 @Override 443 public String getTypeCode() { 444 if (definition == null || definition.getType().size() != 1) 445 throw new Error("not handled"); 446 return definition.getType().get(0).getWorkingCode(); 447 } 448 449 @Override 450 public String getDefinition() { 451 if (definition == null) 452 throw new Error("not handled"); 453 return definition.getDefinition(); 454 } 455 456 @Override 457 public int getMinCardinality() { 458 if (definition == null) 459 throw new Error("not handled"); 460 return definition.getMin(); 461 } 462 463 @Override 464 public int getMaxCardinality() { 465 if (definition == null) 466 throw new Error("not handled"); 467 return definition.getMax().equals("*") ? Integer.MAX_VALUE : Integer.parseInt(definition.getMax()); 468 } 469 470 @Override 471 public StructureDefinition getStructure() { 472 return structure; 473 } 474 475 @Override 476 public BaseWrapper value() { 477 if (getValues().size() != 1) 478 throw new Error("Access single value, but value count is "+getValues().size()); 479 return getValues().get(0); 480 } 481 482 } 483 484 private class BaseWrapperMetaElement implements BaseWrapper { 485 private org.hl7.fhir.r4.elementmodel.Element element; 486 private String type; 487 private StructureDefinition structure; 488 private ElementDefinition definition; 489 private List<ElementDefinition> children; 490 private List<PropertyWrapper> list; 491 492 public BaseWrapperMetaElement(org.hl7.fhir.r4.elementmodel.Element element, String type, StructureDefinition structure, ElementDefinition definition) { 493 this.element = element; 494 this.type = type; 495 this.structure = structure; 496 this.definition = definition; 497 } 498 499 @Override 500 public Base getBase() throws UnsupportedEncodingException, IOException, FHIRException { 501 if (type == null || type.equals("Resource") || type.equals("BackboneElement") || type.equals("Element")) 502 return null; 503 504 if (element.hasElementProperty()) 505 return null; 506 ByteArrayOutputStream xml = new ByteArrayOutputStream(); 507 try { 508 new org.hl7.fhir.r4.elementmodel.XmlParser(context).compose(element, xml, OutputStyle.PRETTY, null); 509 } catch (Exception e) { 510 throw new FHIRException(e.getMessage(), e); 511 } 512 return parseType(xml.toString(), type); 513 } 514 515 @Override 516 public List<PropertyWrapper> children() { 517 if (list == null) { 518 children = ProfileUtilities.getChildList(structure, definition); 519 list = new ArrayList<NarrativeGenerator.PropertyWrapper>(); 520 for (ElementDefinition child : children) { 521 List<org.hl7.fhir.r4.elementmodel.Element> elements = new ArrayList<org.hl7.fhir.r4.elementmodel.Element>(); 522 String name = tail(child.getPath()); 523 if (name.endsWith("[x]")) 524 element.getNamedChildrenWithWildcard(name, elements); 525 else 526 element.getNamedChildren(name, elements); 527 list.add(new PropertyWrapperMetaElement(structure, child, elements)); 528 } 529 } 530 return list; 531 } 532 533 @Override 534 public PropertyWrapper getChildByName(String name) { 535 for (PropertyWrapper p : children()) 536 if (p.getName().equals(name)) 537 return p; 538 return null; 539 } 540 541 } 542 public class ResourceWrapperMetaElement implements ResourceWrapper { 543 private org.hl7.fhir.r4.elementmodel.Element wrapped; 544 private List<ResourceWrapper> list; 545 private List<PropertyWrapper> list2; 546 private StructureDefinition definition; 547 public ResourceWrapperMetaElement(org.hl7.fhir.r4.elementmodel.Element wrapped) { 548 this.wrapped = wrapped; 549 this.definition = wrapped.getProperty().getStructure(); 550 } 551 552 @Override 553 public List<ResourceWrapper> getContained() { 554 if (list == null) { 555 List<org.hl7.fhir.r4.elementmodel.Element> children = wrapped.getChildrenByName("contained"); 556 list = new ArrayList<NarrativeGenerator.ResourceWrapper>(); 557 for (org.hl7.fhir.r4.elementmodel.Element e : children) { 558 list.add(new ResourceWrapperMetaElement(e)); 559 } 560 } 561 return list; 562 } 563 564 @Override 565 public String getId() { 566 return wrapped.getNamedChildValue("id"); 567 } 568 569 @Override 570 public XhtmlNode getNarrative() throws IOException, FHIRException { 571 org.hl7.fhir.r4.elementmodel.Element txt = wrapped.getNamedChild("text"); 572 if (txt == null) 573 return null; 574 org.hl7.fhir.r4.elementmodel.Element div = txt.getNamedChild("div"); 575 if (div == null) 576 return null; 577 else 578 return div.getXhtml(); 579 } 580 581 @Override 582 public String getName() { 583 return wrapped.getName(); 584 } 585 586 @Override 587 public List<PropertyWrapper> children() { 588 if (list2 == null) { 589 List<ElementDefinition> children = ProfileUtilities.getChildList(definition, definition.getSnapshot().getElement().get(0)); 590 list2 = new ArrayList<NarrativeGenerator.PropertyWrapper>(); 591 for (ElementDefinition child : children) { 592 List<org.hl7.fhir.r4.elementmodel.Element> elements = new ArrayList<org.hl7.fhir.r4.elementmodel.Element>(); 593 if (child.getPath().endsWith("[x]")) 594 wrapped.getNamedChildrenWithWildcard(tail(child.getPath()), elements); 595 else 596 wrapped.getNamedChildren(tail(child.getPath()), elements); 597 list2.add(new PropertyWrapperMetaElement(definition, child, elements)); 598 } 599 } 600 return list2; 601 } 602 } 603 604 private class PropertyWrapperMetaElement implements PropertyWrapper { 605 606 private StructureDefinition structure; 607 private ElementDefinition definition; 608 private List<org.hl7.fhir.r4.elementmodel.Element> values; 609 private List<BaseWrapper> list; 610 611 public PropertyWrapperMetaElement(StructureDefinition structure, ElementDefinition definition, List<org.hl7.fhir.r4.elementmodel.Element> values) { 612 this.structure = structure; 613 this.definition = definition; 614 this.values = values; 615 } 616 617 @Override 618 public String getName() { 619 return tail(definition.getPath()); 620 } 621 622 @Override 623 public boolean hasValues() { 624 return values.size() > 0; 625 } 626 627 @Override 628 public List<BaseWrapper> getValues() { 629 if (list == null) { 630 list = new ArrayList<NarrativeGenerator.BaseWrapper>(); 631 for (org.hl7.fhir.r4.elementmodel.Element e : values) 632 list.add(new BaseWrapperMetaElement(e, e.fhirType(), structure, definition)); 633 } 634 return list; 635 } 636 637 @Override 638 public String getTypeCode() { 639 return definition.typeSummary(); 640 } 641 642 @Override 643 public String getDefinition() { 644 return definition.getDefinition(); 645 } 646 647 @Override 648 public int getMinCardinality() { 649 return definition.getMin(); 650 } 651 652 @Override 653 public int getMaxCardinality() { 654 return "*".equals(definition.getMax()) ? Integer.MAX_VALUE : Integer.valueOf(definition.getMax()); 655 } 656 657 @Override 658 public StructureDefinition getStructure() { 659 return structure; 660 } 661 662 @Override 663 public BaseWrapper value() { 664 if (getValues().size() != 1) 665 throw new Error("Access single value, but value count is "+getValues().size()); 666 return getValues().get(0); 667 } 668 669 } 670 671 private class ResourceWrapperElement implements ResourceWrapper { 672 673 private Element wrapped; 674 private StructureDefinition definition; 675 private List<ResourceWrapper> list; 676 private List<PropertyWrapper> list2; 677 678 public ResourceWrapperElement(Element wrapped, StructureDefinition definition) { 679 this.wrapped = wrapped; 680 this.definition = definition; 681 } 682 683 @Override 684 public List<ResourceWrapper> getContained() { 685 if (list == null) { 686 List<Element> children = new ArrayList<Element>(); 687 XMLUtil.getNamedChildren(wrapped, "contained", children); 688 list = new ArrayList<NarrativeGenerator.ResourceWrapper>(); 689 for (Element e : children) { 690 Element c = XMLUtil.getFirstChild(e); 691 list.add(new ResourceWrapperElement(c, context.fetchTypeDefinition(c.getNodeName()))); 692 } 693 } 694 return list; 695 } 696 697 @Override 698 public String getId() { 699 return XMLUtil.getNamedChildValue(wrapped, "id"); 700 } 701 702 @Override 703 public XhtmlNode getNarrative() throws FHIRFormatError, IOException, FHIRException { 704 Element txt = XMLUtil.getNamedChild(wrapped, "text"); 705 if (txt == null) 706 return null; 707 Element div = XMLUtil.getNamedChild(txt, "div"); 708 if (div == null) 709 return null; 710 try { 711 return new XhtmlParser().parse(new XmlGenerator().generate(div), "div"); 712 } catch (org.hl7.fhir.exceptions.FHIRFormatError e) { 713 throw new FHIRFormatError(e.getMessage(), e); 714 } catch (org.hl7.fhir.exceptions.FHIRException e) { 715 throw new FHIRException(e.getMessage(), e); 716 } 717 } 718 719 @Override 720 public String getName() { 721 return wrapped.getNodeName(); 722 } 723 724 @Override 725 public List<PropertyWrapper> children() { 726 if (list2 == null) { 727 List<ElementDefinition> children = ProfileUtilities.getChildList(definition, definition.getSnapshot().getElement().get(0)); 728 list2 = new ArrayList<NarrativeGenerator.PropertyWrapper>(); 729 for (ElementDefinition child : children) { 730 List<Element> elements = new ArrayList<Element>(); 731 XMLUtil.getNamedChildrenWithWildcard(wrapped, tail(child.getPath()), elements); 732 list2.add(new PropertyWrapperElement(definition, child, elements)); 733 } 734 } 735 return list2; 736 } 737 } 738 739 private class PropertyWrapperDirect implements PropertyWrapper { 740 private Property wrapped; 741 private List<BaseWrapper> list; 742 743 private PropertyWrapperDirect(Property wrapped) { 744 super(); 745 if (wrapped == null) 746 throw new Error("wrapped == null"); 747 this.wrapped = wrapped; 748 } 749 750 @Override 751 public String getName() { 752 return wrapped.getName(); 753 } 754 755 @Override 756 public boolean hasValues() { 757 return wrapped.hasValues(); 758 } 759 760 @Override 761 public List<BaseWrapper> getValues() { 762 if (list == null) { 763 list = new ArrayList<NarrativeGenerator.BaseWrapper>(); 764 for (Base b : wrapped.getValues()) 765 list.add(b == null ? null : new BaseWrapperDirect(b)); 766 } 767 return list; 768 } 769 770 @Override 771 public String getTypeCode() { 772 return wrapped.getTypeCode(); 773 } 774 775 @Override 776 public String getDefinition() { 777 return wrapped.getDefinition(); 778 } 779 780 @Override 781 public int getMinCardinality() { 782 return wrapped.getMinCardinality(); 783 } 784 785 @Override 786 public int getMaxCardinality() { 787 return wrapped.getMinCardinality(); 788 } 789 790 @Override 791 public StructureDefinition getStructure() { 792 return wrapped.getStructure(); 793 } 794 795 @Override 796 public BaseWrapper value() { 797 if (getValues().size() != 1) 798 throw new Error("Access single value, but value count is "+getValues().size()); 799 return getValues().get(0); 800 } 801 802 public String toString() { 803 return "#."+wrapped.toString(); 804 } 805 } 806 807 private class BaseWrapperDirect implements BaseWrapper { 808 private Base wrapped; 809 private List<PropertyWrapper> list; 810 811 private BaseWrapperDirect(Base wrapped) { 812 super(); 813 if (wrapped == null) 814 throw new Error("wrapped == null"); 815 this.wrapped = wrapped; 816 } 817 818 @Override 819 public Base getBase() { 820 return wrapped; 821 } 822 823 @Override 824 public List<PropertyWrapper> children() { 825 if (list == null) { 826 list = new ArrayList<NarrativeGenerator.PropertyWrapper>(); 827 for (Property p : wrapped.children()) 828 list.add(new PropertyWrapperDirect(p)); 829 } 830 return list; 831 832 } 833 834 @Override 835 public PropertyWrapper getChildByName(String name) { 836 Property p = wrapped.getChildByName(name); 837 if (p == null) 838 return null; 839 else 840 return new PropertyWrapperDirect(p); 841 } 842 843 } 844 845 public class ResourceWrapperDirect implements ResourceWrapper { 846 private Resource wrapped; 847 848 public ResourceWrapperDirect(Resource wrapped) { 849 super(); 850 if (wrapped == null) 851 throw new Error("wrapped == null"); 852 this.wrapped = wrapped; 853 } 854 855 @Override 856 public List<ResourceWrapper> getContained() { 857 List<ResourceWrapper> list = new ArrayList<NarrativeGenerator.ResourceWrapper>(); 858 if (wrapped instanceof DomainResource) { 859 DomainResource dr = (DomainResource) wrapped; 860 for (Resource c : dr.getContained()) { 861 list.add(new ResourceWrapperDirect(c)); 862 } 863 } 864 return list; 865 } 866 867 @Override 868 public String getId() { 869 return wrapped.getId(); 870 } 871 872 @Override 873 public XhtmlNode getNarrative() { 874 if (wrapped instanceof DomainResource) { 875 DomainResource dr = (DomainResource) wrapped; 876 if (dr.hasText() && dr.getText().hasDiv()) 877 return dr.getText().getDiv(); 878 } 879 return null; 880 } 881 882 @Override 883 public String getName() { 884 return wrapped.getResourceType().toString(); 885 } 886 887 @Override 888 public List<PropertyWrapper> children() { 889 List<PropertyWrapper> list = new ArrayList<PropertyWrapper>(); 890 for (Property c : wrapped.children()) 891 list.add(new PropertyWrapperDirect(c)); 892 return list; 893 } 894 } 895 896 public static class ResourceWithReference { 897 898 private String reference; 899 private ResourceWrapper resource; 900 901 public ResourceWithReference(String reference, ResourceWrapper resource) { 902 this.reference = reference; 903 this.resource = resource; 904 } 905 906 public String getReference() { 907 return reference; 908 } 909 910 public ResourceWrapper getResource() { 911 return resource; 912 } 913 } 914 915 private String prefix; 916 private IWorkerContext context; 917 private String basePath; 918 private String tooCostlyNoteEmpty; 919 private String tooCostlyNoteNotEmpty; 920 private IReferenceResolver resolver; 921 private int headerLevelContext; 922 private List<ConceptMapRenderInstructions> renderingMaps = new ArrayList<ConceptMapRenderInstructions>(); 923 private boolean pretty; 924 private boolean canonicalUrlsAsLinks; 925 private TerminologyServiceOptions terminologyServiceOptions = new TerminologyServiceOptions(); 926 927 public NarrativeGenerator(String prefix, String basePath, IWorkerContext context) { 928 super(); 929 this.prefix = prefix; 930 this.context = context; 931 this.basePath = basePath; 932 init(); 933 } 934 935 public NarrativeGenerator setLiquidServices(ILiquidTemplateProvider templateProvider, IEvaluationContext services) { 936 this.templateProvider = templateProvider; 937 this.services = services; 938 return this; 939 } 940 941 public Base parseType(String xml, String type) throws IOException, FHIRException { 942 if (parser != null) 943 return parser.parseType(xml, type); 944 else 945 return new XmlParser().parseAnyType(xml, type); 946 } 947 948 public NarrativeGenerator(String prefix, String basePath, IWorkerContext context, IReferenceResolver resolver) { 949 super(); 950 this.prefix = prefix; 951 this.context = context; 952 this.basePath = basePath; 953 this.resolver = resolver; 954 init(); 955 } 956 957 958 private void init() { 959 renderingMaps.add(new ConceptMapRenderInstructions("Canonical Status", "http://hl7.org/fhir/ValueSet/resource-status", false)); 960 } 961 962 public List<ConceptMapRenderInstructions> getRenderingMaps() { 963 return renderingMaps; 964 } 965 966 public int getHeaderLevelContext() { 967 return headerLevelContext; 968 } 969 970 public NarrativeGenerator setHeaderLevelContext(int headerLevelContext) { 971 this.headerLevelContext = headerLevelContext; 972 return this; 973 } 974 975 public String getTooCostlyNoteEmpty() { 976 return tooCostlyNoteEmpty; 977 } 978 979 980 public NarrativeGenerator setTooCostlyNoteEmpty(String tooCostlyNoteEmpty) { 981 this.tooCostlyNoteEmpty = tooCostlyNoteEmpty; 982 return this; 983 } 984 985 986 public String getTooCostlyNoteNotEmpty() { 987 return tooCostlyNoteNotEmpty; 988 } 989 990 991 public NarrativeGenerator setTooCostlyNoteNotEmpty(String tooCostlyNoteNotEmpty) { 992 this.tooCostlyNoteNotEmpty = tooCostlyNoteNotEmpty; 993 return this; 994 } 995 996 997 // dom based version, for build program 998 public String generate(Element doc) throws IOException, org.hl7.fhir.exceptions.FHIRException { 999 return generate(null, doc); 1000 } 1001 public String generate(ResourceContext rcontext, Element doc) throws IOException, org.hl7.fhir.exceptions.FHIRException { 1002 if (rcontext == null) 1003 rcontext = new ResourceContext(null, doc); 1004 String rt = "http://hl7.org/fhir/StructureDefinition/"+doc.getNodeName(); 1005 StructureDefinition p = context.fetchResource(StructureDefinition.class, rt); 1006 return generateByProfile(doc, p, true); 1007 } 1008 1009 // dom based version, for build program 1010 public String generate(org.hl7.fhir.r4.elementmodel.Element er, boolean showCodeDetails, ITypeParser parser) throws IOException, FHIRException { 1011 return generate(null, er, showCodeDetails, parser); 1012 } 1013 1014 public String generate(ResourceContext rcontext, org.hl7.fhir.r4.elementmodel.Element er, boolean showCodeDetails, ITypeParser parser) throws IOException, FHIRException { 1015 if (rcontext == null) 1016 rcontext = new ResourceContext(null, er); 1017 this.parser = parser; 1018 1019 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 1020 x.para().b().tx("Generated Narrative"+(showCodeDetails ? " with Details" : "")); 1021 try { 1022 ResourceWrapperMetaElement resw = new ResourceWrapperMetaElement(er); 1023 BaseWrapperMetaElement base = new BaseWrapperMetaElement(er, null, er.getProperty().getStructure(), er.getProperty().getDefinition()); 1024 base.children(); 1025 generateByProfile(resw, er.getProperty().getStructure(), base, er.getProperty().getStructure().getSnapshot().getElement(), er.getProperty().getDefinition(), base.children, x, er.fhirType(), showCodeDetails, 0, rcontext); 1026 1027 } catch (Exception e) { 1028 e.printStackTrace(); 1029 x.para().b().setAttribute("style", "color: maroon").tx("Exception generating Narrative: "+e.getMessage()); 1030 } 1031 inject(er, x, NarrativeStatus.GENERATED); 1032 return new XhtmlComposer(XhtmlComposer.XML, pretty).compose(x); 1033 } 1034 1035 private boolean generateByProfile(ResourceContext rc, StructureDefinition profile, boolean showCodeDetails) { 1036 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 1037 x.para().b().tx("Generated Narrative"+(showCodeDetails ? " with Details" : "")); 1038 try { 1039 generateByProfile(rc.resourceResource, profile, rc.resourceResource, profile.getSnapshot().getElement(), profile.getSnapshot().getElement().get(0), getChildrenForPath(profile.getSnapshot().getElement(), rc.resourceResource.getResourceType().toString()), x, rc.resourceResource.getResourceType().toString(), showCodeDetails, rc); 1040 } catch (Exception e) { 1041 e.printStackTrace(); 1042 x.para().b().setAttribute("style", "color: maroon").tx("Exception generating Narrative: "+e.getMessage()); 1043 } 1044 inject(rc.resourceResource, x, NarrativeStatus.GENERATED); 1045 return true; 1046 } 1047 1048 private String generateByProfile(Element er, StructureDefinition profile, boolean showCodeDetails) throws IOException, org.hl7.fhir.exceptions.FHIRException { 1049 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 1050 x.para().b().tx("Generated Narrative"+(showCodeDetails ? " with Details" : "")); 1051 try { 1052 generateByProfile(er, profile, er, profile.getSnapshot().getElement(), profile.getSnapshot().getElement().get(0), getChildrenForPath(profile.getSnapshot().getElement(), er.getLocalName()), x, er.getLocalName(), showCodeDetails); 1053 } catch (Exception e) { 1054 e.printStackTrace(); 1055 x.para().b().setAttribute("style", "color: maroon").tx("Exception generating Narrative: "+e.getMessage()); 1056 } 1057 inject(er, x, NarrativeStatus.GENERATED); 1058 String b = new XhtmlComposer(XhtmlComposer.XML, pretty).compose(x); 1059 return b; 1060 } 1061 1062 private void generateByProfile(Element eres, StructureDefinition profile, Element ee, List<ElementDefinition> allElements, ElementDefinition defn, List<ElementDefinition> children, XhtmlNode x, String path, boolean showCodeDetails) throws FHIRException, UnsupportedEncodingException, IOException { 1063 1064 ResourceWrapperElement resw = new ResourceWrapperElement(eres, profile); 1065 BaseWrapperElement base = new BaseWrapperElement(ee, null, profile, profile.getSnapshot().getElement().get(0)); 1066 generateByProfile(resw, profile, base, allElements, defn, children, x, path, showCodeDetails, 0, null); 1067 } 1068 1069 1070 private void generateByProfile(Resource res, StructureDefinition profile, Base e, List<ElementDefinition> allElements, ElementDefinition defn, List<ElementDefinition> children, XhtmlNode x, String path, boolean showCodeDetails, ResourceContext rc) throws FHIRException, UnsupportedEncodingException, IOException { 1071 generateByProfile(new ResourceWrapperDirect(res), profile, new BaseWrapperDirect(e), allElements, defn, children, x, path, showCodeDetails, 0, rc); 1072 } 1073 1074 private void generateByProfile(ResourceWrapper res, StructureDefinition profile, BaseWrapper e, List<ElementDefinition> allElements, ElementDefinition defn, List<ElementDefinition> children, XhtmlNode x, String path, boolean showCodeDetails, int indent, ResourceContext rc) throws FHIRException, UnsupportedEncodingException, IOException { 1075 if (children.isEmpty()) { 1076 renderLeaf(res, e, defn, x, false, showCodeDetails, readDisplayHints(defn), path, indent, rc); 1077 } else { 1078 for (PropertyWrapper p : splitExtensions(profile, e.children())) { 1079 if (p.hasValues()) { 1080 ElementDefinition child = getElementDefinition(children, path+"."+p.getName(), p); 1081 if (child != null) { 1082 Map<String, String> displayHints = readDisplayHints(child); 1083 if (!exemptFromRendering(child)) { 1084 List<ElementDefinition> grandChildren = getChildrenForPath(allElements, path+"."+p.getName()); 1085 filterGrandChildren(grandChildren, path+"."+p.getName(), p); 1086 if (p.getValues().size() > 0 && child != null) { 1087 if (isPrimitive(child)) { 1088 XhtmlNode para = x.para(); 1089 String name = p.getName(); 1090 if (name.endsWith("[x]")) 1091 name = name.substring(0, name.length() - 3); 1092 if (showCodeDetails || !isDefaultValue(displayHints, p.getValues())) { 1093 para.b().addText(name); 1094 para.tx(": "); 1095 if (renderAsList(child) && p.getValues().size() > 1) { 1096 XhtmlNode list = x.ul(); 1097 for (BaseWrapper v : p.getValues()) 1098 renderLeaf(res, v, child, list.li(), false, showCodeDetails, displayHints, path, indent, rc); 1099 } else { 1100 boolean first = true; 1101 for (BaseWrapper v : p.getValues()) { 1102 if (first) 1103 first = false; 1104 else 1105 para.tx(", "); 1106 renderLeaf(res, v, child, para, false, showCodeDetails, displayHints, path, indent, rc); 1107 } 1108 } 1109 } 1110 } else if (canDoTable(path, p, grandChildren)) { 1111 x.addTag(getHeader()).addText(Utilities.capitalize(Utilities.camelCase(Utilities.pluralizeMe(p.getName())))); 1112 XhtmlNode tbl = x.table( "grid"); 1113 XhtmlNode tr = tbl.tr(); 1114 tr.td().tx("-"); // work around problem with empty table rows 1115 addColumnHeadings(tr, grandChildren); 1116 for (BaseWrapper v : p.getValues()) { 1117 if (v != null) { 1118 tr = tbl.tr(); 1119 tr.td().tx("*"); // work around problem with empty table rows 1120 addColumnValues(res, tr, grandChildren, v, showCodeDetails, displayHints, path, indent, rc); 1121 } 1122 } 1123 } else { 1124 for (BaseWrapper v : p.getValues()) { 1125 if (v != null) { 1126 XhtmlNode bq = x.addTag("blockquote"); 1127 bq.para().b().addText(p.getName()); 1128 generateByProfile(res, profile, v, allElements, child, grandChildren, bq, path+"."+p.getName(), showCodeDetails, indent+1, rc); 1129 } 1130 } 1131 } 1132 } 1133 } 1134 } 1135 } 1136 } 1137 } 1138 } 1139 1140 private String getHeader() { 1141 int i = 3; 1142 while (i <= headerLevelContext) 1143 i++; 1144 if (i > 6) 1145 i = 6; 1146 return "h"+Integer.toString(i); 1147 } 1148 1149 private void filterGrandChildren(List<ElementDefinition> grandChildren, String string, PropertyWrapper prop) { 1150 List<ElementDefinition> toRemove = new ArrayList<ElementDefinition>(); 1151 toRemove.addAll(grandChildren); 1152 for (BaseWrapper b : prop.getValues()) { 1153 List<ElementDefinition> list = new ArrayList<ElementDefinition>(); 1154 for (ElementDefinition ed : toRemove) { 1155 PropertyWrapper p = b.getChildByName(tail(ed.getPath())); 1156 if (p != null && p.hasValues()) 1157 list.add(ed); 1158 } 1159 toRemove.removeAll(list); 1160 } 1161 grandChildren.removeAll(toRemove); 1162 } 1163 1164 private List<PropertyWrapper> splitExtensions(StructureDefinition profile, List<PropertyWrapper> children) throws UnsupportedEncodingException, IOException, FHIRException { 1165 List<PropertyWrapper> results = new ArrayList<PropertyWrapper>(); 1166 Map<String, PropertyWrapper> map = new HashMap<String, PropertyWrapper>(); 1167 for (PropertyWrapper p : children) 1168 if (p.getName().equals("extension") || p.getName().equals("modifierExtension")) { 1169 // we're going to split these up, and create a property for each url 1170 if (p.hasValues()) { 1171 for (BaseWrapper v : p.getValues()) { 1172 Extension ex = (Extension) v.getBase(); 1173 String url = ex.getUrl(); 1174 StructureDefinition ed = context.fetchResource(StructureDefinition.class, url); 1175 if (p.getName().equals("modifierExtension") && ed == null) 1176 throw new DefinitionException("Unknown modifier extension "+url); 1177 PropertyWrapper pe = map.get(p.getName()+"["+url+"]"); 1178 if (pe == null) { 1179 if (ed == null) { 1180 if (url.startsWith("http://hl7.org/fhir") && !url.startsWith("http://hl7.org/fhir/us")) 1181 throw new DefinitionException("unknown extension "+url); 1182 // System.out.println("unknown extension "+url); 1183 pe = new PropertyWrapperDirect(new Property(p.getName()+"["+url+"]", p.getTypeCode(), p.getDefinition(), p.getMinCardinality(), p.getMaxCardinality(), ex)); 1184 } else { 1185 ElementDefinition def = ed.getSnapshot().getElement().get(0); 1186 pe = new PropertyWrapperDirect(new Property(p.getName()+"["+url+"]", "Extension", def.getDefinition(), def.getMin(), def.getMax().equals("*") ? Integer.MAX_VALUE : Integer.parseInt(def.getMax()), ex)); 1187 ((PropertyWrapperDirect) pe).wrapped.setStructure(ed); 1188 } 1189 results.add(pe); 1190 } else 1191 pe.getValues().add(v); 1192 } 1193 } 1194 } else 1195 results.add(p); 1196 return results; 1197 } 1198 1199 @SuppressWarnings("rawtypes") 1200 private boolean isDefaultValue(Map<String, String> displayHints, List<BaseWrapper> list) throws UnsupportedEncodingException, IOException, FHIRException { 1201 if (list.size() != 1) 1202 return false; 1203 if (list.get(0).getBase() instanceof PrimitiveType) 1204 return isDefault(displayHints, (PrimitiveType) list.get(0).getBase()); 1205 else 1206 return false; 1207 } 1208 1209 private boolean isDefault(Map<String, String> displayHints, PrimitiveType primitiveType) { 1210 String v = primitiveType.asStringValue(); 1211 if (!Utilities.noString(v) && displayHints.containsKey("default") && v.equals(displayHints.get("default"))) 1212 return true; 1213 return false; 1214 } 1215 1216 private boolean exemptFromRendering(ElementDefinition child) { 1217 if (child == null) 1218 return false; 1219 if ("Composition.subject".equals(child.getPath())) 1220 return true; 1221 if ("Composition.section".equals(child.getPath())) 1222 return true; 1223 return false; 1224 } 1225 1226 private boolean renderAsList(ElementDefinition child) { 1227 if (child.getType().size() == 1) { 1228 String t = child.getType().get(0).getWorkingCode(); 1229 if (t.equals("Address") || t.equals("Reference")) 1230 return true; 1231 } 1232 return false; 1233 } 1234 1235 private void addColumnHeadings(XhtmlNode tr, List<ElementDefinition> grandChildren) { 1236 for (ElementDefinition e : grandChildren) 1237 tr.td().b().addText(Utilities.capitalize(tail(e.getPath()))); 1238 } 1239 1240 private void addColumnValues(ResourceWrapper res, XhtmlNode tr, List<ElementDefinition> grandChildren, BaseWrapper v, boolean showCodeDetails, Map<String, String> displayHints, String path, int indent, ResourceContext rc) throws FHIRException, UnsupportedEncodingException, IOException { 1241 for (ElementDefinition e : grandChildren) { 1242 PropertyWrapper p = v.getChildByName(e.getPath().substring(e.getPath().lastIndexOf(".")+1)); 1243 if (p == null || p.getValues().size() == 0 || p.getValues().get(0) == null) 1244 tr.td().tx(" "); 1245 else 1246 renderLeaf(res, p.getValues().get(0), e, tr.td(), false, showCodeDetails, displayHints, path, indent, rc); 1247 } 1248 } 1249 1250 private String tail(String path) { 1251 return path.substring(path.lastIndexOf(".")+1); 1252 } 1253 1254 private boolean canDoTable(String path, PropertyWrapper p, List<ElementDefinition> grandChildren) { 1255 for (ElementDefinition e : grandChildren) { 1256 List<PropertyWrapper> values = getValues(path, p, e); 1257 if (values.size() > 1 || !isPrimitive(e) || !canCollapse(e)) 1258 return false; 1259 } 1260 return true; 1261 } 1262 1263 private List<PropertyWrapper> getValues(String path, PropertyWrapper p, ElementDefinition e) { 1264 List<PropertyWrapper> res = new ArrayList<PropertyWrapper>(); 1265 for (BaseWrapper v : p.getValues()) { 1266 for (PropertyWrapper g : v.children()) { 1267 if ((path+"."+p.getName()+"."+g.getName()).equals(e.getPath())) 1268 res.add(p); 1269 } 1270 } 1271 return res; 1272 } 1273 1274 private boolean canCollapse(ElementDefinition e) { 1275 // we can collapse any data type 1276 return !e.getType().isEmpty(); 1277 } 1278 1279 private boolean isPrimitive(ElementDefinition e) { 1280 //we can tell if e is a primitive because it has types 1281 if (e.getType().isEmpty()) 1282 return false; 1283 if (e.getType().size() == 1 && isBase(e.getType().get(0).getWorkingCode())) 1284 return false; 1285 return true; 1286// return !e.getType().isEmpty() 1287 } 1288 1289 private boolean isBase(String code) { 1290 return code.equals("Element") || code.equals("BackboneElement"); 1291 } 1292 1293 private ElementDefinition getElementDefinition(List<ElementDefinition> elements, String path, PropertyWrapper p) { 1294 for (ElementDefinition element : elements) 1295 if (element.getPath().equals(path)) 1296 return element; 1297 if (path.endsWith("\"]") && p.getStructure() != null) 1298 return p.getStructure().getSnapshot().getElement().get(0); 1299 return null; 1300 } 1301 1302 private void renderLeaf(ResourceWrapper res, BaseWrapper ew, ElementDefinition defn, XhtmlNode x, boolean title, boolean showCodeDetails, Map<String, String> displayHints, String path, int indent, ResourceContext rc) throws FHIRException, UnsupportedEncodingException, IOException { 1303 if (ew == null) 1304 return; 1305 1306 1307 Base e = ew.getBase(); 1308 1309 if (e instanceof StringType) 1310 x.addText(((StringType) e).getValue()); 1311 else if (e instanceof CodeType) 1312 x.addText(((CodeType) e).getValue()); 1313 else if (e instanceof IdType) 1314 x.addText(((IdType) e).getValue()); 1315 else if (e instanceof Extension) 1316 return; 1317 else if (e instanceof InstantType) 1318 x.addText(((InstantType) e).toHumanDisplay()); 1319 else if (e instanceof DateTimeType) { 1320 if (e.hasPrimitiveValue()) 1321 x.addText(((DateTimeType) e).toHumanDisplay()); 1322 } else if (e instanceof Base64BinaryType) 1323 x.addText(new Base64().encodeAsString(((Base64BinaryType) e).getValue())); 1324 else if (e instanceof org.hl7.fhir.r4.model.DateType) 1325 x.addText(((org.hl7.fhir.r4.model.DateType) e).toHumanDisplay()); 1326 else if (e instanceof Enumeration) { 1327 Object ev = ((Enumeration<?>) e).getValue(); 1328 x.addText(ev == null ? "" : ev.toString()); // todo: look up a display name if there is one 1329 } else if (e instanceof BooleanType) 1330 x.addText(((BooleanType) e).getValue().toString()); 1331 else if (e instanceof CodeableConcept) { 1332 renderCodeableConcept((CodeableConcept) e, x, showCodeDetails); 1333 } else if (e instanceof Coding) { 1334 renderCoding((Coding) e, x, showCodeDetails); 1335 } else if (e instanceof Annotation) { 1336 renderAnnotation((Annotation) e, x); 1337 } else if (e instanceof Identifier) { 1338 renderIdentifier((Identifier) e, x); 1339 } else if (e instanceof org.hl7.fhir.r4.model.IntegerType) { 1340 x.addText(Integer.toString(((org.hl7.fhir.r4.model.IntegerType) e).getValue())); 1341 } else if (e instanceof org.hl7.fhir.r4.model.DecimalType) { 1342 x.addText(((org.hl7.fhir.r4.model.DecimalType) e).getValue().toString()); 1343 } else if (e instanceof HumanName) { 1344 renderHumanName((HumanName) e, x); 1345 } else if (e instanceof SampledData) { 1346 renderSampledData((SampledData) e, x); 1347 } else if (e instanceof Address) { 1348 renderAddress((Address) e, x); 1349 } else if (e instanceof ContactPoint) { 1350 renderContactPoint((ContactPoint) e, x); 1351 } else if (e instanceof UriType) { 1352 renderUri((UriType) e, x, defn.getPath(), rc != null && rc.resourceResource != null ? rc.resourceResource.getId() : null); 1353 } else if (e instanceof Timing) { 1354 renderTiming((Timing) e, x); 1355 } else if (e instanceof Range) { 1356 renderRange((Range) e, x); 1357 } else if (e instanceof Quantity) { 1358 renderQuantity((Quantity) e, x, showCodeDetails); 1359 } else if (e instanceof Ratio) { 1360 renderQuantity(((Ratio) e).getNumerator(), x, showCodeDetails); 1361 x.tx("/"); 1362 renderQuantity(((Ratio) e).getDenominator(), x, showCodeDetails); 1363 } else if (e instanceof Period) { 1364 Period p = (Period) e; 1365 x.addText(!p.hasStart() ? "??" : p.getStartElement().toHumanDisplay()); 1366 x.tx(" --> "); 1367 x.addText(!p.hasEnd() ? "(ongoing)" : p.getEndElement().toHumanDisplay()); 1368 } else if (e instanceof Reference) { 1369 Reference r = (Reference) e; 1370 XhtmlNode c = x; 1371 ResourceWithReference tr = null; 1372 if (r.hasReferenceElement()) { 1373 tr = resolveReference(res, r.getReference(), rc); 1374 1375 if (!r.getReference().startsWith("#")) { 1376 if (tr != null && tr.getReference() != null) 1377 c = x.ah(tr.getReference()); 1378 else 1379 c = x.ah(r.getReference()); 1380 } 1381 } 1382 // what to display: if text is provided, then that. if the reference was resolved, then show the generated narrative 1383 if (r.hasDisplayElement()) { 1384 c.addText(r.getDisplay()); 1385 if (tr != null && tr.getResource() != null) { 1386 c.tx(". Generated Summary: "); 1387 generateResourceSummary(c, tr.getResource(), true, r.getReference().startsWith("#"), rc); 1388 } 1389 } else if (tr != null && tr.getResource() != null) { 1390 generateResourceSummary(c, tr.getResource(), r.getReference().startsWith("#"), r.getReference().startsWith("#"), rc); 1391 } else { 1392 c.addText(r.getReference()); 1393 } 1394 } else if (e instanceof Resource) { 1395 return; 1396 } else if (e instanceof ElementDefinition) { 1397 x.tx("todo-bundle"); 1398 } else if (e != null && !(e instanceof Attachment) && !(e instanceof Narrative) && !(e instanceof Meta)) { 1399 StructureDefinition sd = context.fetchTypeDefinition(e.fhirType()); 1400 if (sd == null) 1401 throw new NotImplementedException("type "+e.getClass().getName()+" not handled yet, and no structure found"); 1402 else 1403 generateByProfile(res, sd, ew, sd.getSnapshot().getElement(), sd.getSnapshot().getElementFirstRep(), 1404 getChildrenForPath(sd.getSnapshot().getElement(), sd.getSnapshot().getElementFirstRep().getPath()), x, path, showCodeDetails, indent + 1, rc); 1405 } 1406 } 1407 1408 private boolean displayLeaf(ResourceWrapper res, BaseWrapper ew, ElementDefinition defn, XhtmlNode x, String name, boolean showCodeDetails, ResourceContext rc) throws FHIRException, UnsupportedEncodingException, IOException { 1409 if (ew == null) 1410 return false; 1411 Base e = ew.getBase(); 1412 if (e == null) 1413 return false; 1414 1415 Map<String, String> displayHints = readDisplayHints(defn); 1416 1417 if (name.endsWith("[x]")) 1418 name = name.substring(0, name.length() - 3); 1419 1420 if (!showCodeDetails && e instanceof PrimitiveType && isDefault(displayHints, ((PrimitiveType) e))) 1421 return false; 1422 1423 if (e instanceof StringType) { 1424 x.addText(name+": "+((StringType) e).getValue()); 1425 return true; 1426 } else if (e instanceof CodeType) { 1427 x.addText(name+": "+((CodeType) e).getValue()); 1428 return true; 1429 } else if (e instanceof IdType) { 1430 x.addText(name+": "+((IdType) e).getValue()); 1431 return true; 1432 } else if (e instanceof UriType) { 1433 x.addText(name+": "+((UriType) e).getValue()); 1434 return true; 1435 } else if (e instanceof DateTimeType) { 1436 x.addText(name+": "+((DateTimeType) e).toHumanDisplay()); 1437 return true; 1438 } else if (e instanceof InstantType) { 1439 x.addText(name+": "+((InstantType) e).toHumanDisplay()); 1440 return true; 1441 } else if (e instanceof Extension) { 1442// x.tx("Extensions: todo"); 1443 return false; 1444 } else if (e instanceof org.hl7.fhir.r4.model.DateType) { 1445 x.addText(name+": "+((org.hl7.fhir.r4.model.DateType) e).toHumanDisplay()); 1446 return true; 1447 } else if (e instanceof Enumeration) { 1448 x.addText(((Enumeration<?>) e).getValue().toString()); // todo: look up a display name if there is one 1449 return true; 1450 } else if (e instanceof BooleanType) { 1451 if (((BooleanType) e).getValue()) { 1452 x.addText(name); 1453 return true; 1454 } 1455 } else if (e instanceof CodeableConcept) { 1456 renderCodeableConcept((CodeableConcept) e, x, showCodeDetails); 1457 return true; 1458 } else if (e instanceof Coding) { 1459 renderCoding((Coding) e, x, showCodeDetails); 1460 return true; 1461 } else if (e instanceof Annotation) { 1462 renderAnnotation((Annotation) e, x, showCodeDetails); 1463 return true; 1464 } else if (e instanceof org.hl7.fhir.r4.model.IntegerType) { 1465 x.addText(Integer.toString(((org.hl7.fhir.r4.model.IntegerType) e).getValue())); 1466 return true; 1467 } else if (e instanceof org.hl7.fhir.r4.model.DecimalType) { 1468 x.addText(((org.hl7.fhir.r4.model.DecimalType) e).getValue().toString()); 1469 return true; 1470 } else if (e instanceof Identifier) { 1471 renderIdentifier((Identifier) e, x); 1472 return true; 1473 } else if (e instanceof HumanName) { 1474 renderHumanName((HumanName) e, x); 1475 return true; 1476 } else if (e instanceof SampledData) { 1477 renderSampledData((SampledData) e, x); 1478 return true; 1479 } else if (e instanceof Address) { 1480 renderAddress((Address) e, x); 1481 return true; 1482 } else if (e instanceof ContactPoint) { 1483 renderContactPoint((ContactPoint) e, x); 1484 return true; 1485 } else if (e instanceof Timing) { 1486 renderTiming((Timing) e, x); 1487 return true; 1488 } else if (e instanceof Quantity) { 1489 renderQuantity((Quantity) e, x, showCodeDetails); 1490 return true; 1491 } else if (e instanceof Ratio) { 1492 renderQuantity(((Ratio) e).getNumerator(), x, showCodeDetails); 1493 x.tx("/"); 1494 renderQuantity(((Ratio) e).getDenominator(), x, showCodeDetails); 1495 return true; 1496 } else if (e instanceof Period) { 1497 Period p = (Period) e; 1498 x.addText(name+": "); 1499 x.addText(!p.hasStart() ? "??" : p.getStartElement().toHumanDisplay()); 1500 x.tx(" --> "); 1501 x.addText(!p.hasEnd() ? "(ongoing)" : p.getEndElement().toHumanDisplay()); 1502 return true; 1503 } else if (e instanceof Reference) { 1504 Reference r = (Reference) e; 1505 if (r.hasDisplayElement()) 1506 x.addText(r.getDisplay()); 1507 else if (r.hasReferenceElement()) { 1508 ResourceWithReference tr = resolveReference(res, r.getReference(), rc); 1509 x.addText(tr == null ? r.getReference() : "????"); // getDisplayForReference(tr.getReference())); 1510 } else 1511 x.tx("??"); 1512 return true; 1513 } else if (e instanceof Narrative) { 1514 return false; 1515 } else if (e instanceof Resource) { 1516 return false; 1517 } else if (e instanceof ContactDetail) { 1518 return false; 1519 } else if (e instanceof Range) { 1520 return false; 1521 } else if (e instanceof Meta) { 1522 return false; 1523 } else if (e instanceof Dosage) { 1524 return false; 1525 } else if (e instanceof Signature) { 1526 return false; 1527 } else if (e instanceof UsageContext) { 1528 return false; 1529 } else if (e instanceof RelatedArtifact) { 1530 return false; 1531 } else if (e instanceof ElementDefinition) { 1532 return false; 1533 } else if (!(e instanceof Attachment)) 1534 throw new NotImplementedException("type "+e.getClass().getName()+" not handled yet"); 1535 return false; 1536 } 1537 1538 1539 private Map<String, String> readDisplayHints(ElementDefinition defn) throws DefinitionException { 1540 Map<String, String> hints = new HashMap<String, String>(); 1541 if (defn != null) { 1542 String displayHint = ToolingExtensions.getDisplayHint(defn); 1543 if (!Utilities.noString(displayHint)) { 1544 String[] list = displayHint.split(";"); 1545 for (String item : list) { 1546 String[] parts = item.split(":"); 1547 if (parts.length != 2) 1548 throw new DefinitionException("error reading display hint: '"+displayHint+"'"); 1549 hints.put(parts[0].trim(), parts[1].trim()); 1550 } 1551 } 1552 } 1553 return hints; 1554 } 1555 1556 public static String displayPeriod(Period p) { 1557 String s = !p.hasStart() ? "??" : p.getStartElement().toHumanDisplay(); 1558 s = s + " --> "; 1559 return s + (!p.hasEnd() ? "(ongoing)" : p.getEndElement().toHumanDisplay()); 1560 } 1561 1562 private void generateResourceSummary(XhtmlNode x, ResourceWrapper res, boolean textAlready, boolean showCodeDetails, ResourceContext rc) throws FHIRException, UnsupportedEncodingException, IOException { 1563 if (!textAlready) { 1564 XhtmlNode div = res.getNarrative(); 1565 if (div != null) { 1566 if (div.allChildrenAreText()) 1567 x.getChildNodes().addAll(div.getChildNodes()); 1568 if (div.getChildNodes().size() == 1 && div.getChildNodes().get(0).allChildrenAreText()) 1569 x.getChildNodes().addAll(div.getChildNodes().get(0).getChildNodes()); 1570 } 1571 x.tx("Generated Summary: "); 1572 } 1573 String path = res.getName(); 1574 StructureDefinition profile = context.fetchResource(StructureDefinition.class, path); 1575 if (profile == null) 1576 x.tx("unknown resource " +path); 1577 else { 1578 boolean firstElement = true; 1579 boolean last = false; 1580 for (PropertyWrapper p : res.children()) { 1581 ElementDefinition child = getElementDefinition(profile.getSnapshot().getElement(), path+"."+p.getName(), p); 1582 if (p.getValues().size() > 0 && p.getValues().get(0) != null && child != null && isPrimitive(child) && includeInSummary(child)) { 1583 if (firstElement) 1584 firstElement = false; 1585 else if (last) 1586 x.tx("; "); 1587 boolean first = true; 1588 last = false; 1589 for (BaseWrapper v : p.getValues()) { 1590 if (first) 1591 first = false; 1592 else if (last) 1593 x.tx(", "); 1594 last = displayLeaf(res, v, child, x, p.getName(), showCodeDetails, rc) || last; 1595 } 1596 } 1597 } 1598 } 1599 } 1600 1601 1602 private boolean includeInSummary(ElementDefinition child) { 1603 if (child.getIsModifier()) 1604 return true; 1605 if (child.getMustSupport()) 1606 return true; 1607 if (child.getType().size() == 1) { 1608 String t = child.getType().get(0).getWorkingCode(); 1609 if (t.equals("Address") || t.equals("Contact") || t.equals("Reference") || t.equals("Uri") || t.equals("Url") || t.equals("Canonical")) 1610 return false; 1611 } 1612 return true; 1613 } 1614 1615 private ResourceWithReference resolveReference(ResourceWrapper res, String url, ResourceContext rc) { 1616 if (url == null) 1617 return null; 1618 if (url.startsWith("#")) { 1619 for (ResourceWrapper r : res.getContained()) { 1620 if (r.getId().equals(url.substring(1))) 1621 return new ResourceWithReference(null, r); 1622 } 1623 return null; 1624 } 1625 1626 if (rc!=null) { 1627 Resource bundleResource = rc.resolve(url); 1628 if (bundleResource!=null) { 1629 String bundleUrl = "#" + bundleResource.getResourceType().name().toLowerCase() + "_" + bundleResource.getId(); 1630 return new ResourceWithReference(bundleUrl, new ResourceWrapperDirect(bundleResource)); 1631 } 1632 } 1633 1634 Resource ae = context.fetchResource(null, url); 1635 if (ae != null) 1636 return new ResourceWithReference(url, new ResourceWrapperDirect(ae)); 1637 else if (resolver != null) { 1638 return resolver.resolve(url); 1639 } else 1640 return null; 1641 } 1642 1643 private void renderCodeableConcept(CodeableConcept cc, XhtmlNode x, boolean showCodeDetails) { 1644 String s = cc.getText(); 1645 if (Utilities.noString(s)) { 1646 for (Coding c : cc.getCoding()) { 1647 if (c.hasDisplayElement()) { 1648 s = c.getDisplay(); 1649 break; 1650 } 1651 } 1652 } 1653 if (Utilities.noString(s)) { 1654 // still? ok, let's try looking it up 1655 for (Coding c : cc.getCoding()) { 1656 if (c.hasCodeElement() && c.hasSystemElement()) { 1657 s = lookupCode(c.getSystem(), c.getCode()); 1658 if (!Utilities.noString(s)) 1659 break; 1660 } 1661 } 1662 } 1663 1664 if (Utilities.noString(s)) { 1665 if (cc.getCoding().isEmpty()) 1666 s = ""; 1667 else 1668 s = cc.getCoding().get(0).getCode(); 1669 } 1670 1671 if (showCodeDetails) { 1672 x.addText(s+" "); 1673 XhtmlNode sp = x.span("background: LightGoldenRodYellow", null); 1674 sp.tx("(Details "); 1675 boolean first = true; 1676 for (Coding c : cc.getCoding()) { 1677 if (first) { 1678 sp.tx(": "); 1679 first = false; 1680 } else 1681 sp.tx("; "); 1682 sp.tx("{"+describeSystem(c.getSystem())+" code '"+c.getCode()+"' = '"+lookupCode(c.getSystem(), c.getCode())+(c.hasDisplay() ? "', given as '"+c.getDisplay()+"'}" : "")); 1683 } 1684 sp.tx(")"); 1685 } else { 1686 1687 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 1688 for (Coding c : cc.getCoding()) { 1689 if (c.hasCodeElement() && c.hasSystemElement()) { 1690 b.append("{"+c.getSystem()+" "+c.getCode()+"}"); 1691 } 1692 } 1693 1694 x.span(null, "Codes: "+b.toString()).addText(s); 1695 } 1696 } 1697 1698 private void renderAnnotation(Annotation a, XhtmlNode x, boolean showCodeDetails) throws FHIRException { 1699 StringBuilder s = new StringBuilder(); 1700 if (a.hasAuthor()) { 1701 s.append("Author: "); 1702 1703 if (a.hasAuthorReference()) 1704 s.append(a.getAuthorReference().getReference()); 1705 else if (a.hasAuthorStringType()) 1706 s.append(a.getAuthorStringType().getValue()); 1707 } 1708 1709 1710 if (a.hasTimeElement()) { 1711 if (s.length() > 0) 1712 s.append("; "); 1713 1714 s.append("Made: ").append(a.getTimeElement().toHumanDisplay()); 1715 } 1716 1717 if (a.hasText()) { 1718 if (s.length() > 0) 1719 s.append("; "); 1720 1721 s.append("Annotation: ").append(a.getText()); 1722 } 1723 1724 x.addText(s.toString()); 1725 } 1726 1727 private void renderCoding(Coding c, XhtmlNode x, boolean showCodeDetails) { 1728 String s = ""; 1729 if (c.hasDisplayElement()) 1730 s = c.getDisplay(); 1731 if (Utilities.noString(s)) 1732 s = lookupCode(c.getSystem(), c.getCode()); 1733 1734 if (Utilities.noString(s)) 1735 s = c.getCode(); 1736 1737 if (showCodeDetails) { 1738 x.addText(s+" (Details: "+describeSystem(c.getSystem())+" code "+c.getCode()+" = '"+lookupCode(c.getSystem(), c.getCode())+"', stated as '"+c.getDisplay()+"')"); 1739 } else 1740 x.span(null, "{"+c.getSystem()+" "+c.getCode()+"}").addText(s); 1741 } 1742 1743 public static String describeSystem(String system) { 1744 if (system == null) 1745 return "[not stated]"; 1746 if (system.equals("http://loinc.org")) 1747 return "LOINC"; 1748 if (system.startsWith("http://snomed.info")) 1749 return "SNOMED CT"; 1750 if (system.equals("http://www.nlm.nih.gov/research/umls/rxnorm")) 1751 return "RxNorm"; 1752 if (system.equals("http://hl7.org/fhir/sid/icd-9")) 1753 return "ICD-9"; 1754 if (system.equals("http://dicom.nema.org/resources/ontology/DCM")) 1755 return "DICOM"; 1756 if (system.equals("http://unitsofmeasure.org")) 1757 return "UCUM"; 1758 1759 return system; 1760 } 1761 1762 private String lookupCode(String system, String code) { 1763 ValidationResult t = context.validateCode(terminologyServiceOptions , system, code, null); 1764 1765 if (t != null && t.getDisplay() != null) 1766 return t.getDisplay(); 1767 else 1768 return code; 1769 1770 } 1771 1772 private ConceptDefinitionComponent findCode(String code, List<ConceptDefinitionComponent> list) { 1773 for (ConceptDefinitionComponent t : list) { 1774 if (code.equals(t.getCode())) 1775 return t; 1776 ConceptDefinitionComponent c = findCode(code, t.getConcept()); 1777 if (c != null) 1778 return c; 1779 } 1780 return null; 1781 } 1782 1783 public String displayCodeableConcept(CodeableConcept cc) { 1784 String s = cc.getText(); 1785 if (Utilities.noString(s)) { 1786 for (Coding c : cc.getCoding()) { 1787 if (c.hasDisplayElement()) { 1788 s = c.getDisplay(); 1789 break; 1790 } 1791 } 1792 } 1793 if (Utilities.noString(s)) { 1794 // still? ok, let's try looking it up 1795 for (Coding c : cc.getCoding()) { 1796 if (c.hasCode() && c.hasSystem()) { 1797 s = lookupCode(c.getSystem(), c.getCode()); 1798 if (!Utilities.noString(s)) 1799 break; 1800 } 1801 } 1802 } 1803 1804 if (Utilities.noString(s)) { 1805 if (cc.getCoding().isEmpty()) 1806 s = ""; 1807 else 1808 s = cc.getCoding().get(0).getCode(); 1809 } 1810 return s; 1811 } 1812 1813 private void renderIdentifier(Identifier ii, XhtmlNode x) { 1814 x.addText(displayIdentifier(ii)); 1815 } 1816 1817 private void renderTiming(Timing s, XhtmlNode x) throws FHIRException { 1818 x.addText(displayTiming(s)); 1819 } 1820 1821 private void renderQuantity(Quantity q, XhtmlNode x, boolean showCodeDetails) { 1822 if (q.hasComparator()) 1823 x.addText(q.getComparator().toCode()); 1824 x.addText(q.getValue().toString()); 1825 if (q.hasUnit()) 1826 x.tx(" "+q.getUnit()); 1827 else if (q.hasCode()) 1828 x.tx(" "+q.getCode()); 1829 if (showCodeDetails && q.hasCode()) { 1830 x.span("background: LightGoldenRodYellow", null).tx(" (Details: "+describeSystem(q.getSystem())+" code "+q.getCode()+" = '"+lookupCode(q.getSystem(), q.getCode())+"')"); 1831 } 1832 } 1833 1834 private void renderRange(Range q, XhtmlNode x) { 1835 if (q.hasLow()) 1836 x.addText(q.getLow().getValue().toString()); 1837 else 1838 x.tx("?"); 1839 x.tx("-"); 1840 if (q.hasHigh()) 1841 x.addText(q.getHigh().getValue().toString()); 1842 else 1843 x.tx("?"); 1844 if (q.getLow().hasUnit()) 1845 x.tx(" "+q.getLow().getUnit()); 1846 } 1847 1848 public String displayRange(Range q) { 1849 StringBuilder b = new StringBuilder(); 1850 if (q.hasLow()) 1851 b.append(q.getLow().getValue().toString()); 1852 else 1853 b.append("?"); 1854 b.append("-"); 1855 if (q.hasHigh()) 1856 b.append(q.getHigh().getValue().toString()); 1857 else 1858 b.append("?"); 1859 if (q.getLow().hasUnit()) 1860 b.append(" "+q.getLow().getUnit()); 1861 return b.toString(); 1862 } 1863 1864 private void renderHumanName(HumanName name, XhtmlNode x) { 1865 x.addText(displayHumanName(name)); 1866 } 1867 1868 private void renderAnnotation(Annotation annot, XhtmlNode x) { 1869 x.addText(annot.getText()); 1870 } 1871 1872 private void renderAddress(Address address, XhtmlNode x) { 1873 x.addText(displayAddress(address)); 1874 } 1875 1876 private void renderContactPoint(ContactPoint contact, XhtmlNode x) { 1877 x.addText(displayContactPoint(contact)); 1878 } 1879 1880 private void renderUri(UriType uri, XhtmlNode x, String path, String id) { 1881 String url = uri.getValue(); 1882 if (isCanonical(path)) { 1883 MetadataResource mr = context.fetchResource(null, url); 1884 if (mr != null) { 1885 if (path.startsWith(mr.fhirType()+".") && mr.getId().equals(id)) { 1886 url = null; // don't link to self whatever 1887 } else if (mr.hasUserData("path")) 1888 url = mr.getUserString("path"); 1889 } else if (!canonicalUrlsAsLinks) 1890 url = null; 1891 } 1892 if (url == null) 1893 x.b().tx(uri.getValue()); 1894 else if (uri.getValue().startsWith("mailto:")) 1895 x.ah(uri.getValue()).addText(uri.getValue().substring(7)); 1896 else 1897 x.ah(uri.getValue()).addText(uri.getValue()); 1898 } 1899 1900 private boolean isCanonical(String path) { 1901 if (!path.endsWith(".url")) 1902 return false; 1903 StructureDefinition sd = context.fetchTypeDefinition(path.substring(0, path.length()-4)); 1904 if (sd == null) 1905 return false; 1906 if (Utilities.existsInList(path.substring(0, path.length()-4), "CapabilityStatement", "StructureDefinition", "ImplementationGuide", "SearchParameter", "MessageDefinition", "OperationDefinition", "CompartmentDefinition", "StructureMap", "GraphDefinition", 1907 "ExampleScenario", "CodeSystem", "ValueSet", "ConceptMap", "NamingSystem", "TerminologyCapabilities")) 1908 return true; 1909 return sd.getBaseDefinitionElement().hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-codegen-super"); 1910 } 1911 1912 private void renderSampledData(SampledData sampledData, XhtmlNode x) { 1913 x.addText(displaySampledData(sampledData)); 1914 } 1915 1916 private String displaySampledData(SampledData s) { 1917 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 1918 if (s.hasOrigin()) 1919 b.append("Origin: "+displayQuantity(s.getOrigin())); 1920 1921 if (s.hasPeriod()) 1922 b.append("Period: "+s.getPeriod().toString()); 1923 1924 if (s.hasFactor()) 1925 b.append("Factor: "+s.getFactor().toString()); 1926 1927 if (s.hasLowerLimit()) 1928 b.append("Lower: "+s.getLowerLimit().toString()); 1929 1930 if (s.hasUpperLimit()) 1931 b.append("Upper: "+s.getUpperLimit().toString()); 1932 1933 if (s.hasDimensions()) 1934 b.append("Dimensions: "+s.getDimensions()); 1935 1936 if (s.hasData()) 1937 b.append("Data: "+s.getData()); 1938 1939 return b.toString(); 1940 } 1941 1942 private String displayQuantity(Quantity q) { 1943 StringBuilder s = new StringBuilder(); 1944 1945 s.append("(system = '").append(describeSystem(q.getSystem())) 1946 .append("' code ").append(q.getCode()) 1947 .append(" = '").append(lookupCode(q.getSystem(), q.getCode())).append("')"); 1948 1949 return s.toString(); 1950 } 1951 1952 private String displayTiming(Timing s) throws FHIRException { 1953 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 1954 if (s.hasCode()) 1955 b.append("Code: "+displayCodeableConcept(s.getCode())); 1956 1957 if (s.getEvent().size() > 0) { 1958 CommaSeparatedStringBuilder c = new CommaSeparatedStringBuilder(); 1959 for (DateTimeType p : s.getEvent()) { 1960 c.append(p.toHumanDisplay()); 1961 } 1962 b.append("Events: "+ c.toString()); 1963 } 1964 1965 if (s.hasRepeat()) { 1966 TimingRepeatComponent rep = s.getRepeat(); 1967 if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasStart()) 1968 b.append("Starting "+rep.getBoundsPeriod().getStartElement().toHumanDisplay()); 1969 if (rep.hasCount()) 1970 b.append("Count "+Integer.toString(rep.getCount())+" times"); 1971 if (rep.hasDuration()) 1972 b.append("Duration "+rep.getDuration().toPlainString()+displayTimeUnits(rep.getPeriodUnit())); 1973 1974 if (rep.hasWhen()) { 1975 String st = ""; 1976 if (rep.hasOffset()) { 1977 st = Integer.toString(rep.getOffset())+"min "; 1978 } 1979 b.append("Do "+st); 1980 for (Enumeration<EventTiming> wh : rep.getWhen()) 1981 b.append(displayEventCode(wh.getValue())); 1982 } else { 1983 String st = ""; 1984 if (!rep.hasFrequency() || (!rep.hasFrequencyMax() && rep.getFrequency() == 1) ) 1985 st = "Once"; 1986 else { 1987 st = Integer.toString(rep.getFrequency()); 1988 if (rep.hasFrequencyMax()) 1989 st = st + "-"+Integer.toString(rep.getFrequency()); 1990 } 1991 if (rep.hasPeriod()) { 1992 st = st + " per "+rep.getPeriod().toPlainString(); 1993 if (rep.hasPeriodMax()) 1994 st = st + "-"+rep.getPeriodMax().toPlainString(); 1995 st = st + " "+displayTimeUnits(rep.getPeriodUnit()); 1996 } 1997 b.append("Do "+st); 1998 } 1999 if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasEnd()) 2000 b.append("Until "+rep.getBoundsPeriod().getEndElement().toHumanDisplay()); 2001 } 2002 return b.toString(); 2003 } 2004 2005 private String displayEventCode(EventTiming when) { 2006 switch (when) { 2007 case C: return "at meals"; 2008 case CD: return "at lunch"; 2009 case CM: return "at breakfast"; 2010 case CV: return "at dinner"; 2011 case AC: return "before meals"; 2012 case ACD: return "before lunch"; 2013 case ACM: return "before breakfast"; 2014 case ACV: return "before dinner"; 2015 case HS: return "before sleeping"; 2016 case PC: return "after meals"; 2017 case PCD: return "after lunch"; 2018 case PCM: return "after breakfast"; 2019 case PCV: return "after dinner"; 2020 case WAKE: return "after waking"; 2021 default: return "??"; 2022 } 2023 } 2024 2025 private String displayTimeUnits(UnitsOfTime units) { 2026 if (units == null) 2027 return "??"; 2028 switch (units) { 2029 case A: return "years"; 2030 case D: return "days"; 2031 case H: return "hours"; 2032 case MIN: return "minutes"; 2033 case MO: return "months"; 2034 case S: return "seconds"; 2035 case WK: return "weeks"; 2036 default: return "??"; 2037 } 2038 } 2039 2040 public static String displayHumanName(HumanName name) { 2041 StringBuilder s = new StringBuilder(); 2042 if (name.hasText()) 2043 s.append(name.getText()); 2044 else { 2045 for (StringType p : name.getGiven()) { 2046 s.append(p.getValue()); 2047 s.append(" "); 2048 } 2049 if (name.hasFamily()) { 2050 s.append(name.getFamily()); 2051 s.append(" "); 2052 } 2053 } 2054 if (name.hasUse() && name.getUse() != NameUse.USUAL) 2055 s.append("("+name.getUse().toString()+")"); 2056 return s.toString(); 2057 } 2058 2059 private String displayAddress(Address address) { 2060 StringBuilder s = new StringBuilder(); 2061 if (address.hasText()) 2062 s.append(address.getText()); 2063 else { 2064 for (StringType p : address.getLine()) { 2065 s.append(p.getValue()); 2066 s.append(" "); 2067 } 2068 if (address.hasCity()) { 2069 s.append(address.getCity()); 2070 s.append(" "); 2071 } 2072 if (address.hasState()) { 2073 s.append(address.getState()); 2074 s.append(" "); 2075 } 2076 2077 if (address.hasPostalCode()) { 2078 s.append(address.getPostalCode()); 2079 s.append(" "); 2080 } 2081 2082 if (address.hasCountry()) { 2083 s.append(address.getCountry()); 2084 s.append(" "); 2085 } 2086 } 2087 if (address.hasUse()) 2088 s.append("("+address.getUse().toString()+")"); 2089 return s.toString(); 2090 } 2091 2092 public static String displayContactPoint(ContactPoint contact) { 2093 StringBuilder s = new StringBuilder(); 2094 s.append(describeSystem(contact.getSystem())); 2095 if (Utilities.noString(contact.getValue())) 2096 s.append("-unknown-"); 2097 else 2098 s.append(contact.getValue()); 2099 if (contact.hasUse()) 2100 s.append("("+contact.getUse().toString()+")"); 2101 return s.toString(); 2102 } 2103 2104 private static String describeSystem(ContactPointSystem system) { 2105 if (system == null) 2106 return ""; 2107 switch (system) { 2108 case PHONE: return "ph: "; 2109 case FAX: return "fax: "; 2110 default: 2111 return ""; 2112 } 2113 } 2114 2115 private String displayIdentifier(Identifier ii) { 2116 String s = Utilities.noString(ii.getValue()) ? "??" : ii.getValue(); 2117 2118 if (ii.hasType()) { 2119 if (ii.getType().hasText()) 2120 s = ii.getType().getText()+" = "+s; 2121 else if (ii.getType().hasCoding() && ii.getType().getCoding().get(0).hasDisplay()) 2122 s = ii.getType().getCoding().get(0).getDisplay()+" = "+s; 2123 else if (ii.getType().hasCoding() && ii.getType().getCoding().get(0).hasCode()) 2124 s = lookupCode(ii.getType().getCoding().get(0).getSystem(), ii.getType().getCoding().get(0).getCode())+" = "+s; 2125 } 2126 2127 if (ii.hasUse()) 2128 s = s + " ("+ii.getUse().toString()+")"; 2129 return s; 2130 } 2131 2132 private List<ElementDefinition> getChildrenForPath(List<ElementDefinition> elements, String path) throws DefinitionException { 2133 // do we need to do a name reference substitution? 2134 for (ElementDefinition e : elements) { 2135 if (e.getPath().equals(path) && e.hasContentReference()) { 2136 String ref = e.getContentReference(); 2137 ElementDefinition t = null; 2138 // now, resolve the name 2139 for (ElementDefinition e1 : elements) { 2140 if (ref.equals("#"+e1.getId())) 2141 t = e1; 2142 } 2143 if (t == null) 2144 throw new DefinitionException("Unable to resolve content reference "+ref+" trying to resolve "+path); 2145 path = t.getPath(); 2146 break; 2147 } 2148 } 2149 2150 List<ElementDefinition> results = new ArrayList<ElementDefinition>(); 2151 for (ElementDefinition e : elements) { 2152 if (e.getPath().startsWith(path+".") && !e.getPath().substring(path.length()+1).contains(".")) 2153 results.add(e); 2154 } 2155 return results; 2156 } 2157 2158 2159 public boolean generate(ResourceContext rcontext, ConceptMap cm) throws FHIRFormatError, DefinitionException, IOException { 2160 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 2161 x.h2().addText(cm.getName()+" ("+cm.getUrl()+")"); 2162 2163 XhtmlNode p = x.para(); 2164 p.tx("Mapping from "); 2165 if (cm.hasSource()) 2166 AddVsRef(rcontext, cm.getSource().primitiveValue(), p); 2167 else 2168 p.tx("(not specified)"); 2169 p.tx(" to "); 2170 if (cm.hasTarget()) 2171 AddVsRef(rcontext, cm.getTarget().primitiveValue(), p); 2172 else 2173 p.tx("(not specified)"); 2174 2175 p = x.para(); 2176 if (cm.getExperimental()) 2177 p.addText(Utilities.capitalize(cm.getStatus().toString())+" (not intended for production usage). "); 2178 else 2179 p.addText(Utilities.capitalize(cm.getStatus().toString())+". "); 2180 p.tx("Published on "+(cm.hasDate() ? cm.getDateElement().toHumanDisplay() : "??")+" by "+cm.getPublisher()); 2181 if (!cm.getContact().isEmpty()) { 2182 p.tx(" ("); 2183 boolean firsti = true; 2184 for (ContactDetail ci : cm.getContact()) { 2185 if (firsti) 2186 firsti = false; 2187 else 2188 p.tx(", "); 2189 if (ci.hasName()) 2190 p.addText(ci.getName()+": "); 2191 boolean first = true; 2192 for (ContactPoint c : ci.getTelecom()) { 2193 if (first) 2194 first = false; 2195 else 2196 p.tx(", "); 2197 addTelecom(p, c); 2198 } 2199 } 2200 p.tx(")"); 2201 } 2202 p.tx(". "); 2203 p.addText(cm.getCopyright()); 2204 if (!Utilities.noString(cm.getDescription())) 2205 addMarkdown(x, cm.getDescription()); 2206 2207 x.br(); 2208 CodeSystem cs = context.fetchCodeSystem("http://hl7.org/fhir/concept-map-equivalence"); 2209 String eqpath = cs.getUserString("path"); 2210 2211 for (ConceptMapGroupComponent grp : cm.getGroup()) { 2212 String src = grp.getSource(); 2213 boolean comment = false; 2214 boolean ok = true; 2215 Map<String, HashSet<String>> sources = new HashMap<String, HashSet<String>>(); 2216 Map<String, HashSet<String>> targets = new HashMap<String, HashSet<String>>(); 2217 sources.put("code", new HashSet<String>()); 2218 targets.put("code", new HashSet<String>()); 2219 SourceElementComponent cc = grp.getElement().get(0); 2220 String dst = grp.getTarget(); 2221 sources.get("code").add(grp.getSource()); 2222 targets.get("code").add(grp.getTarget()); 2223 for (SourceElementComponent ccl : grp.getElement()) { 2224 ok = ok && ccl.getTarget().size() == 1 && ccl.getTarget().get(0).getDependsOn().isEmpty() && ccl.getTarget().get(0).getProduct().isEmpty(); 2225 for (TargetElementComponent ccm : ccl.getTarget()) { 2226 comment = comment || !Utilities.noString(ccm.getComment()); 2227 for (OtherElementComponent d : ccm.getDependsOn()) { 2228 if (!sources.containsKey(d.getProperty())) 2229 sources.put(d.getProperty(), new HashSet<String>()); 2230 sources.get(d.getProperty()).add(d.getSystem()); 2231 } 2232 for (OtherElementComponent d : ccm.getProduct()) { 2233 if (!targets.containsKey(d.getProperty())) 2234 targets.put(d.getProperty(), new HashSet<String>()); 2235 targets.get(d.getProperty()).add(d.getSystem()); 2236 } 2237 2238 } 2239 } 2240 2241 String display; 2242 if (ok) { 2243 // simple 2244 XhtmlNode tbl = x.table( "grid"); 2245 XhtmlNode tr = tbl.tr(); 2246 tr.td().b().tx("Source Code"); 2247 tr.td().b().tx("Equivalence"); 2248 tr.td().b().tx("Destination Code"); 2249 if (comment) 2250 tr.td().b().tx("Comment"); 2251 for (SourceElementComponent ccl : grp.getElement()) { 2252 tr = tbl.tr(); 2253 XhtmlNode td = tr.td(); 2254 td.addText(ccl.getCode()); 2255 display = getDisplayForConcept(grp.getSource(), ccl.getCode()); 2256 if (display != null && !isSameCodeAndDisplay(ccl.getCode(), display)) 2257 td.tx(" ("+display+")"); 2258 TargetElementComponent ccm = ccl.getTarget().get(0); 2259 tr.td().addText(!ccm.hasEquivalence() ? "" : ccm.getEquivalence().toCode()); 2260 td = tr.td(); 2261 td.addText(ccm.getCode()); 2262 display = getDisplayForConcept(grp.getTarget(), ccm.getCode()); 2263 if (display != null && !isSameCodeAndDisplay(ccm.getCode(), display)) 2264 td.tx(" ("+display+")"); 2265 if (comment) 2266 tr.td().addText(ccm.getComment()); 2267 } 2268 } else { 2269 XhtmlNode tbl = x.table( "grid"); 2270 XhtmlNode tr = tbl.tr(); 2271 XhtmlNode td; 2272 tr.td().colspan(Integer.toString(sources.size())).b().tx("Source Concept Details"); 2273 tr.td().b().tx("Equivalence"); 2274 tr.td().colspan(Integer.toString(targets.size())).b().tx("Destination Concept Details"); 2275 if (comment) 2276 tr.td().b().tx("Comment"); 2277 tr = tbl.tr(); 2278 if (sources.get("code").size() == 1) { 2279 String url = sources.get("code").iterator().next(); 2280 renderCSDetailsLink(tr, url); 2281 } else 2282 tr.td().b().tx("Code"); 2283 for (String s : sources.keySet()) { 2284 if (!s.equals("code")) { 2285 if (sources.get(s).size() == 1) { 2286 String url = sources.get(s).iterator().next(); 2287 renderCSDetailsLink(tr, url); 2288 } else 2289 tr.td().b().addText(getDescForConcept(s)); 2290 } 2291 } 2292 tr.td(); 2293 if (targets.get("code").size() == 1) { 2294 String url = targets.get("code").iterator().next(); 2295 renderCSDetailsLink(tr, url); 2296 } else 2297 tr.td().b().tx("Code"); 2298 for (String s : targets.keySet()) { 2299 if (!s.equals("code")) { 2300 if (targets.get(s).size() == 1) { 2301 String url = targets.get(s).iterator().next(); 2302 renderCSDetailsLink(tr, url); 2303 } else 2304 tr.td().b().addText(getDescForConcept(s)); 2305 } 2306 } 2307 if (comment) 2308 tr.td(); 2309 2310 for (int si = 0; si < grp.getElement().size(); si++) { 2311 SourceElementComponent ccl = grp.getElement().get(si); 2312 boolean slast = si == grp.getElement().size()-1; 2313 boolean first = true; 2314 for (int ti = 0; ti < ccl.getTarget().size(); ti++) { 2315 TargetElementComponent ccm = ccl.getTarget().get(ti); 2316 boolean last = ti == ccl.getTarget().size()-1; 2317 tr = tbl.tr(); 2318 td = tr.td(); 2319 if (!first && !last) 2320 td.setAttribute("style", "border-top-style: none; border-bottom-style: none"); 2321 else if (!first) 2322 td.setAttribute("style", "border-top-style: none"); 2323 else if (!last) 2324 td.setAttribute("style", "border-bottom-style: none"); 2325 if (first) { 2326 if (sources.get("code").size() == 1) 2327 td.addText(ccl.getCode()); 2328 else 2329 td.addText(grp.getSource()+" / "+ccl.getCode()); 2330 display = getDisplayForConcept(grp.getSource(), ccl.getCode()); 2331 if (display != null) 2332 td.tx(" ("+display+")"); 2333 } 2334 for (String s : sources.keySet()) { 2335 if (!s.equals("code")) { 2336 td = tr.td(); 2337 if (first) { 2338 td.addText(getValue(ccm.getDependsOn(), s, sources.get(s).size() != 1)); 2339 display = getDisplay(ccm.getDependsOn(), s); 2340 if (display != null) 2341 td.tx(" ("+display+")"); 2342 } 2343 } 2344 } 2345 first = false; 2346 if (!ccm.hasEquivalence()) 2347 tr.td().tx(":"+"("+ConceptMapEquivalence.EQUIVALENT.toCode()+")"); 2348 else 2349 tr.td().ah(eqpath+"#"+ccm.getEquivalence().toCode()).tx(ccm.getEquivalence().toCode()); 2350 td = tr.td(); 2351 if (targets.get("code").size() == 1) 2352 td.addText(ccm.getCode()); 2353 else 2354 td.addText(grp.getTarget()+" / "+ccm.getCode()); 2355 display = getDisplayForConcept(grp.getTarget(), ccm.getCode()); 2356 if (display != null) 2357 td.tx(" ("+display+")"); 2358 2359 for (String s : targets.keySet()) { 2360 if (!s.equals("code")) { 2361 td = tr.td(); 2362 td.addText(getValue(ccm.getProduct(), s, targets.get(s).size() != 1)); 2363 display = getDisplay(ccm.getProduct(), s); 2364 if (display != null) 2365 td.tx(" ("+display+")"); 2366 } 2367 } 2368 if (comment) 2369 tr.td().addText(ccm.getComment()); 2370 } 2371 } 2372 } 2373 } 2374 2375 inject(cm, x, NarrativeStatus.GENERATED); 2376 return true; 2377 } 2378 2379 public void renderCSDetailsLink(XhtmlNode tr, String url) { 2380 CodeSystem cs; 2381 XhtmlNode td; 2382 cs = context.fetchCodeSystem(url); 2383 td = tr.td(); 2384 td.b().tx("Code"); 2385 td.tx(" from "); 2386 if (cs == null) 2387 td.tx(url); 2388 else 2389 td.ah(cs.getUserString("path")).attribute("title", url).tx(cs.present()); 2390 } 2391 2392 private boolean isSameCodeAndDisplay(String code, String display) { 2393 String c = code.replace(" ", "").replace("-", "").toLowerCase(); 2394 String d = display.replace(" ", "").replace("-", "").toLowerCase(); 2395 return c.equals(d); 2396 } 2397 2398 private void inject(DomainResource r, XhtmlNode x, NarrativeStatus status) { 2399 if (!x.hasAttribute("xmlns")) 2400 x.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); 2401 if (!r.hasText() || !r.getText().hasDiv() || r.getText().getDiv().getChildNodes().isEmpty()) { 2402 r.setText(new Narrative()); 2403 r.getText().setDiv(x); 2404 r.getText().setStatus(status); 2405 } else { 2406 XhtmlNode n = r.getText().getDiv(); 2407 n.hr(); 2408 n.getChildNodes().addAll(x.getChildNodes()); 2409 } 2410 } 2411 2412 public Element getNarrative(Element er) { 2413 Element txt = XMLUtil.getNamedChild(er, "text"); 2414 if (txt == null) 2415 return null; 2416 return XMLUtil.getNamedChild(txt, "div"); 2417 } 2418 2419 2420 private void inject(Element er, XhtmlNode x, NarrativeStatus status) { 2421 if (!x.hasAttribute("xmlns")) 2422 x.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); 2423 Element txt = XMLUtil.getNamedChild(er, "text"); 2424 if (txt == null) { 2425 txt = er.getOwnerDocument().createElementNS(FormatUtilities.FHIR_NS, "text"); 2426 Element n = XMLUtil.getFirstChild(er); 2427 while (n != null && (n.getNodeName().equals("id") || n.getNodeName().equals("meta") || n.getNodeName().equals("implicitRules") || n.getNodeName().equals("language"))) 2428 n = XMLUtil.getNextSibling(n); 2429 if (n == null) 2430 er.appendChild(txt); 2431 else 2432 er.insertBefore(txt, n); 2433 } 2434 Element st = XMLUtil.getNamedChild(txt, "status"); 2435 if (st == null) { 2436 st = er.getOwnerDocument().createElementNS(FormatUtilities.FHIR_NS, "status"); 2437 Element n = XMLUtil.getFirstChild(txt); 2438 if (n == null) 2439 txt.appendChild(st); 2440 else 2441 txt.insertBefore(st, n); 2442 } 2443 st.setAttribute("value", status.toCode()); 2444 Element div = XMLUtil.getNamedChild(txt, "div"); 2445 if (div == null) { 2446 div = er.getOwnerDocument().createElementNS(FormatUtilities.XHTML_NS, "div"); 2447 div.setAttribute("xmlns", FormatUtilities.XHTML_NS); 2448 txt.appendChild(div); 2449 } 2450 if (div.hasChildNodes()) 2451 div.appendChild(er.getOwnerDocument().createElementNS(FormatUtilities.XHTML_NS, "hr")); 2452 new XhtmlComposer(XhtmlComposer.XML, pretty).compose(div, x); 2453 } 2454 2455 private void inject(org.hl7.fhir.r4.elementmodel.Element er, XhtmlNode x, NarrativeStatus status) throws IOException, FHIRException { 2456 if (!x.hasAttribute("xmlns")) 2457 x.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); 2458 org.hl7.fhir.r4.elementmodel.Element txt = er.getNamedChild("text"); 2459 if (txt == null) { 2460 txt = new org.hl7.fhir.r4.elementmodel.Element("text", er.getProperty().getChild(null, "text")); 2461 int i = 0; 2462 while (i < er.getChildren().size() && (er.getChildren().get(i).getName().equals("id") || er.getChildren().get(i).getName().equals("meta") || er.getChildren().get(i).getName().equals("implicitRules") || er.getChildren().get(i).getName().equals("language"))) 2463 i++; 2464 if (i >= er.getChildren().size()) 2465 er.getChildren().add(txt); 2466 else 2467 er.getChildren().add(i, txt); 2468 } 2469 org.hl7.fhir.r4.elementmodel.Element st = txt.getNamedChild("status"); 2470 if (st == null) { 2471 st = new org.hl7.fhir.r4.elementmodel.Element("status", txt.getProperty().getChild(null, "status")); 2472 txt.getChildren().add(0, st); 2473 } 2474 st.setValue(status.toCode()); 2475 org.hl7.fhir.r4.elementmodel.Element div = txt.getNamedChild("div"); 2476 if (div == null) { 2477 div = new org.hl7.fhir.r4.elementmodel.Element("div", txt.getProperty().getChild(null, "div")); 2478 txt.getChildren().add(div); 2479 div.setValue(new XhtmlComposer(XhtmlComposer.XML, pretty).compose(x)); 2480 } 2481 div.setXhtml(x); 2482 } 2483 2484 private String getDisplay(List<OtherElementComponent> list, String s) { 2485 for (OtherElementComponent c : list) { 2486 if (s.equals(c.getProperty())) 2487 return getDisplayForConcept(c.getSystem(), c.getValue()); 2488 } 2489 return null; 2490 } 2491 2492 private String getDisplayForConcept(String system, String value) { 2493 if (value == null || system == null) 2494 return null; 2495 ValidationResult cl = context.validateCode(terminologyServiceOptions, system, value, null); 2496 return cl == null ? null : cl.getDisplay(); 2497 } 2498 2499 2500 2501 private String getDescForConcept(String s) { 2502 if (s.startsWith("http://hl7.org/fhir/v2/element/")) 2503 return "v2 "+s.substring("http://hl7.org/fhir/v2/element/".length()); 2504 return s; 2505 } 2506 2507 private String getValue(List<OtherElementComponent> list, String s, boolean withSystem) { 2508 for (OtherElementComponent c : list) { 2509 if (s.equals(c.getProperty())) 2510 if (withSystem) 2511 return c.getSystem()+" / "+c.getValue(); 2512 else 2513 return c.getValue(); 2514 } 2515 return null; 2516 } 2517 2518 private void addTelecom(XhtmlNode p, ContactPoint c) { 2519 if (c.getSystem() == ContactPointSystem.PHONE) { 2520 p.tx("Phone: "+c.getValue()); 2521 } else if (c.getSystem() == ContactPointSystem.FAX) { 2522 p.tx("Fax: "+c.getValue()); 2523 } else if (c.getSystem() == ContactPointSystem.EMAIL) { 2524 p.ah( "mailto:"+c.getValue()).addText(c.getValue()); 2525 } else if (c.getSystem() == ContactPointSystem.URL) { 2526 if (c.getValue().length() > 30) 2527 p.ah(c.getValue()).addText(c.getValue().substring(0, 30)+"..."); 2528 else 2529 p.ah(c.getValue()).addText(c.getValue()); 2530 } 2531 } 2532 2533 /** 2534 * This generate is optimised for the FHIR build process itself in as much as it 2535 * generates hyperlinks in the narrative that are only going to be correct for 2536 * the purposes of the build. This is to be reviewed in the future. 2537 * 2538 * @param vs 2539 * @param codeSystems 2540 * @throws IOException 2541 * @throws DefinitionException 2542 * @throws FHIRFormatError 2543 * @throws Exception 2544 */ 2545 public boolean generate(ResourceContext rcontext, CodeSystem cs, boolean header, String lang) throws FHIRFormatError, DefinitionException, IOException { 2546 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 2547 boolean hasExtensions = false; 2548 hasExtensions = generateDefinition(x, cs, header, lang); 2549 inject(cs, x, hasExtensions ? NarrativeStatus.EXTENSIONS : NarrativeStatus.GENERATED); 2550 return true; 2551 } 2552 2553 private boolean generateDefinition(XhtmlNode x, CodeSystem cs, boolean header, String lang) throws FHIRFormatError, DefinitionException, IOException { 2554 boolean hasExtensions = false; 2555 2556 if (header) { 2557 XhtmlNode h = x.h2(); 2558 h.addText(cs.hasTitle() ? cs.getTitle() : cs.getName()); 2559 addMarkdown(x, cs.getDescription()); 2560 if (cs.hasCopyright()) 2561 generateCopyright(x, cs, lang); 2562 } 2563 2564 generateProperties(x, cs, lang); 2565 generateFilters(x, cs, lang); 2566 List<UsedConceptMap> maps = new ArrayList<UsedConceptMap>(); 2567 hasExtensions = generateCodeSystemContent(x, cs, hasExtensions, maps, lang); 2568 2569 return hasExtensions; 2570 } 2571 2572 private void generateFilters(XhtmlNode x, CodeSystem cs, String lang) { 2573 if (cs.hasFilter()) { 2574 x.para().b().tx(context.translator().translate("xhtml-gen-cs", "Filters", lang)); 2575 XhtmlNode tbl = x.table("grid"); 2576 XhtmlNode tr = tbl.tr(); 2577 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Code", lang)); 2578 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Description", lang)); 2579 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "operator", lang)); 2580 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Value", lang)); 2581 for (CodeSystemFilterComponent f : cs.getFilter()) { 2582 tr = tbl.tr(); 2583 tr.td().tx(f.getCode()); 2584 tr.td().tx(f.getDescription()); 2585 XhtmlNode td = tr.td(); 2586 for (Enumeration<org.hl7.fhir.r4.model.CodeSystem.FilterOperator> t : f.getOperator()) 2587 td.tx(t.asStringValue()+" "); 2588 tr.td().tx(f.getValue()); 2589 } 2590 } 2591 } 2592 2593 private void generateProperties(XhtmlNode x, CodeSystem cs, String lang) { 2594 if (cs.hasProperty()) { 2595 x.para().b().tx(context.translator().translate("xhtml-gen-cs", "Properties", lang)); 2596 XhtmlNode tbl = x.table("grid"); 2597 XhtmlNode tr = tbl.tr(); 2598 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Code", lang)); 2599 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "URL", lang)); 2600 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Description", lang)); 2601 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Type", lang)); 2602 for (PropertyComponent p : cs.getProperty()) { 2603 tr = tbl.tr(); 2604 tr.td().tx(p.getCode()); 2605 tr.td().tx(p.getUri()); 2606 tr.td().tx(p.getDescription()); 2607 tr.td().tx(p.hasType() ? p.getType().toCode() : ""); 2608 } 2609 } 2610 } 2611 2612 private boolean generateCodeSystemContent(XhtmlNode x, CodeSystem cs, boolean hasExtensions, List<UsedConceptMap> maps, String lang) throws FHIRFormatError, DefinitionException, IOException { 2613 XhtmlNode p = x.para(); 2614 if (cs.getContent() == CodeSystemContentMode.COMPLETE) 2615 p.tx(context.translator().translateAndFormat("xhtml-gen-cs", lang, "This code system %s defines the following codes", cs.getUrl())+":"); 2616 else if (cs.getContent() == CodeSystemContentMode.EXAMPLE) 2617 p.tx(context.translator().translateAndFormat("xhtml-gen-cs", lang, "This code system %s defines many codes, of which the following are some examples", cs.getUrl())+":"); 2618 else if (cs.getContent() == CodeSystemContentMode.FRAGMENT ) 2619 p.tx(context.translator().translateAndFormat("xhtml-gen-cs", lang, "This code system %s defines many codes, of which the following are a subset", cs.getUrl())+":"); 2620 else if (cs.getContent() == CodeSystemContentMode.NOTPRESENT ) { 2621 p.tx(context.translator().translateAndFormat("xhtml-gen-cs", lang, "This code system %s defines many codes, but they are not represented here", cs.getUrl())); 2622 return false; 2623 } 2624 XhtmlNode t = x.table( "codes"); 2625 boolean commentS = false; 2626 boolean deprecated = false; 2627 boolean display = false; 2628 boolean hierarchy = false; 2629 boolean version = false; 2630 for (ConceptDefinitionComponent c : cs.getConcept()) { 2631 commentS = commentS || conceptsHaveComments(c); 2632 deprecated = deprecated || conceptsHaveDeprecated(cs, c); 2633 display = display || conceptsHaveDisplay(c); 2634 version = version || conceptsHaveVersion(c); 2635 hierarchy = hierarchy || c.hasConcept(); 2636 } 2637 addMapHeaders(addTableHeaderRowStandard(t, hierarchy, display, true, commentS, version, deprecated, lang), maps); 2638 for (ConceptDefinitionComponent c : cs.getConcept()) { 2639 hasExtensions = addDefineRowToTable(t, c, 0, hierarchy, display, commentS, version, deprecated, maps, cs.getUrl(), cs, lang) || hasExtensions; 2640 } 2641// if (langs.size() > 0) { 2642// Collections.sort(langs); 2643// x.para().b().tx("Additional Language Displays"); 2644// t = x.table( "codes"); 2645// XhtmlNode tr = t.tr(); 2646// tr.td().b().tx("Code"); 2647// for (String lang : langs) 2648// tr.td().b().addText(describeLang(lang)); 2649// for (ConceptDefinitionComponent c : cs.getConcept()) { 2650// addLanguageRow(c, t, langs); 2651// } 2652// } 2653 return hasExtensions; 2654 } 2655 2656 private int countConcepts(List<ConceptDefinitionComponent> list) { 2657 int count = list.size(); 2658 for (ConceptDefinitionComponent c : list) 2659 if (c.hasConcept()) 2660 count = count + countConcepts(c.getConcept()); 2661 return count; 2662 } 2663 2664 private void generateCopyright(XhtmlNode x, CodeSystem cs, String lang) { 2665 XhtmlNode p = x.para(); 2666 p.b().tx(context.translator().translate("xhtml-gen-cs", "Copyright Statement:", lang)); 2667 smartAddText(p, " " + cs.getCopyright()); 2668 } 2669 2670 2671 /** 2672 * This generate is optimised for the FHIR build process itself in as much as it 2673 * generates hyperlinks in the narrative that are only going to be correct for 2674 * the purposes of the build. This is to be reviewed in the future. 2675 * 2676 * @param vs 2677 * @param codeSystems 2678 * @throws FHIRException 2679 * @throws IOException 2680 * @throws Exception 2681 */ 2682 public boolean generate(ResourceContext rcontext, ValueSet vs, boolean header) throws FHIRException, IOException { 2683 generate(rcontext, vs, null, header); 2684 return true; 2685 } 2686 2687 public void generate(ResourceContext rcontext, ValueSet vs, ValueSet src, boolean header) throws FHIRException, IOException { 2688 List<UsedConceptMap> maps = findReleventMaps(vs); 2689 2690 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 2691 boolean hasExtensions; 2692 if (vs.hasExpansion()) { 2693 // for now, we just accept an expansion if there is one 2694 hasExtensions = generateExpansion(x, vs, src, header, maps); 2695 } else { 2696 hasExtensions = generateComposition(rcontext, x, vs, header, maps); 2697 } 2698 inject(vs, x, hasExtensions ? NarrativeStatus.EXTENSIONS : NarrativeStatus.GENERATED); 2699 } 2700 2701 private List<UsedConceptMap> findReleventMaps(ValueSet vs) throws FHIRException { 2702 List<UsedConceptMap> res = new ArrayList<UsedConceptMap>(); 2703 for (MetadataResource md : context.allConformanceResources()) { 2704 if (md instanceof ConceptMap) { 2705 ConceptMap cm = (ConceptMap) md; 2706 if (isSource(vs, cm.getSource())) { 2707 ConceptMapRenderInstructions re = findByTarget(cm.getTarget()); 2708 if (re != null) { 2709 ValueSet vst = cm.hasTarget() ? context.fetchResource(ValueSet.class, cm.hasTargetCanonicalType() ? cm.getTargetCanonicalType().getValue() : cm.getTargetUriType().asStringValue()) : null; 2710 res.add(new UsedConceptMap(re, vst == null ? cm.getUserString("path") : vst.getUserString("path"), cm)); 2711 } 2712 } 2713 } 2714 } 2715 return res; 2716// Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>(); 2717// for (ConceptMap a : context.findMapsForSource(vs.getUrl())) { 2718// String url = ""; 2719// ValueSet vsr = context.fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference()); 2720// if (vsr != null) 2721// url = (String) vsr.getUserData("filename"); 2722// mymaps.put(a, url); 2723// } 2724// Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>(); 2725// for (ConceptMap a : context.findMapsForSource(cs.getValueSet())) { 2726// String url = ""; 2727// ValueSet vsr = context.fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference()); 2728// if (vsr != null) 2729// url = (String) vsr.getUserData("filename"); 2730// mymaps.put(a, url); 2731// } 2732 // also, look in the contained resources for a concept map 2733// for (Resource r : cs.getContained()) { 2734// if (r instanceof ConceptMap) { 2735// ConceptMap cm = (ConceptMap) r; 2736// if (((Reference) cm.getSource()).getReference().equals(cs.getValueSet())) { 2737// String url = ""; 2738// ValueSet vsr = context.fetchResource(ValueSet.class, ((Reference) cm.getTarget()).getReference()); 2739// if (vsr != null) 2740// url = (String) vsr.getUserData("filename"); 2741// mymaps.put(cm, url); 2742// } 2743// } 2744// } 2745 } 2746 2747 private ConceptMapRenderInstructions findByTarget(Type source) { 2748 String src = source.primitiveValue(); 2749 if (src != null) 2750 for (ConceptMapRenderInstructions t : renderingMaps) { 2751 if (src.equals(t.url)) 2752 return t; 2753 } 2754 return null; 2755 } 2756 2757 private boolean isSource(ValueSet vs, Type source) { 2758 return vs.hasUrl() && source != null && vs.getUrl().equals(source.primitiveValue()); 2759 } 2760 2761 private Integer countMembership(ValueSet vs) { 2762 int count = 0; 2763 if (vs.hasExpansion()) 2764 count = count + conceptCount(vs.getExpansion().getContains()); 2765 else { 2766 if (vs.hasCompose()) { 2767 if (vs.getCompose().hasExclude()) { 2768 try { 2769 ValueSetExpansionOutcome vse = context.expandVS(vs, true, false); 2770 count = 0; 2771 count += conceptCount(vse.getValueset().getExpansion().getContains()); 2772 return count; 2773 } catch (Exception e) { 2774 return null; 2775 } 2776 } 2777 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 2778 if (inc.hasFilter()) 2779 return null; 2780 if (!inc.hasConcept()) 2781 return null; 2782 count = count + inc.getConcept().size(); 2783 } 2784 } 2785 } 2786 return count; 2787 } 2788 2789 private int conceptCount(List<ValueSetExpansionContainsComponent> list) { 2790 int count = 0; 2791 for (ValueSetExpansionContainsComponent c : list) { 2792 if (!c.getAbstract()) 2793 count++; 2794 count = count + conceptCount(c.getContains()); 2795 } 2796 return count; 2797 } 2798 2799 private boolean generateExpansion(XhtmlNode x, ValueSet vs, ValueSet src, boolean header, List<UsedConceptMap> maps) throws FHIRFormatError, DefinitionException, IOException { 2800 boolean hasExtensions = false; 2801 List<String> langs = new ArrayList<String>(); 2802 2803 2804 if (header) { 2805 XhtmlNode h = x.addTag(getHeader()); 2806 h.tx("Value Set Contents"); 2807 if (IsNotFixedExpansion(vs)) 2808 addMarkdown(x, vs.getDescription()); 2809 if (vs.hasCopyright()) 2810 generateCopyright(x, vs); 2811 } 2812 if (ToolingExtensions.hasExtension(vs.getExpansion(), "http://hl7.org/fhir/StructureDefinition/valueset-toocostly")) 2813 x.para().setAttribute("style", "border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(vs.getExpansion().getContains().isEmpty() ? tooCostlyNoteEmpty : tooCostlyNoteNotEmpty ); 2814 else { 2815 Integer count = countMembership(vs); 2816 if (count == null) 2817 x.para().tx("This value set does not contain a fixed number of concepts"); 2818 else 2819 x.para().tx("This value set contains "+count.toString()+" concepts"); 2820 } 2821 2822 generateVersionNotice(x, vs.getExpansion()); 2823 2824 CodeSystem allCS = null; 2825 boolean doLevel = false; 2826 for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) { 2827 if (cc.hasContains()) { 2828 doLevel = true; 2829 break; 2830 } 2831 } 2832 2833 boolean doSystem = true; // checkDoSystem(vs, src); 2834 boolean doDefinition = checkDoDefinition(vs.getExpansion().getContains()); 2835 if (doSystem && allFromOneSystem(vs)) { 2836 doSystem = false; 2837 XhtmlNode p = x.para(); 2838 p.tx("All codes from system "); 2839 allCS = context.fetchCodeSystem(vs.getExpansion().getContains().get(0).getSystem()); 2840 String ref = null; 2841 if (allCS != null) 2842 ref = getCsRef(allCS); 2843 if (ref == null) 2844 p.code(vs.getExpansion().getContains().get(0).getSystem()); 2845 else 2846 p.ah(prefix+ref).code(vs.getExpansion().getContains().get(0).getSystem()); 2847 } 2848 XhtmlNode t = x.table( "codes"); 2849 XhtmlNode tr = t.tr(); 2850 if (doLevel) 2851 tr.td().b().tx("Lvl"); 2852 tr.td().attribute("style", "white-space:nowrap").b().tx("Code"); 2853 if (doSystem) 2854 tr.td().b().tx("System"); 2855 tr.td().b().tx("Display"); 2856 if (doDefinition) 2857 tr.td().b().tx("Definition"); 2858 2859 addMapHeaders(tr, maps); 2860 for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { 2861 addExpansionRowToTable(t, c, 0, doLevel, doSystem, doDefinition, maps, allCS, langs); 2862 } 2863 2864 // now, build observed languages 2865 2866 if (langs.size() > 0) { 2867 Collections.sort(langs); 2868 x.para().b().tx("Additional Language Displays"); 2869 t = x.table( "codes"); 2870 tr = t.tr(); 2871 tr.td().b().tx("Code"); 2872 for (String lang : langs) 2873 tr.td().b().addText(describeLang(lang)); 2874 for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) { 2875 addLanguageRow(c, t, langs); 2876 } 2877 } 2878 2879 return hasExtensions; 2880 } 2881 2882 @SuppressWarnings("rawtypes") 2883 private void generateVersionNotice(XhtmlNode x, ValueSetExpansionComponent expansion) { 2884 Map<String, String> versions = new HashMap<String, String>(); 2885 for (ValueSetExpansionParameterComponent p : expansion.getParameter()) { 2886 if (p.getName().equals("version")) { 2887 String[] parts = ((PrimitiveType) p.getValue()).asStringValue().split("\\|"); 2888 if (parts.length == 2) 2889 versions.put(parts[0], parts[1]); 2890 } 2891 } 2892 if (!versions.isEmpty()) { 2893 StringBuilder b = new StringBuilder(); 2894 b.append("Expansion based on "); 2895 boolean first = true; 2896 for (String s : versions.keySet()) { 2897 if (first) 2898 first = false; 2899 else 2900 b.append(", "); 2901 if (!s.equals("http://snomed.info/sct")) 2902 b.append(describeSystem(s)+" version "+versions.get(s)); 2903 else { 2904 String[] parts = versions.get(s).split("\\/"); 2905 if (parts.length >= 5) { 2906 String m = describeModule(parts[4]); 2907 if (parts.length == 7) 2908 b.append("SNOMED CT "+m+" edition "+formatSCTDate(parts[6])); 2909 else 2910 b.append("SNOMED CT "+m+" edition"); 2911 } else 2912 b.append(describeSystem(s)+" version "+versions.get(s)); 2913 } 2914 } 2915 2916 x.para().setAttribute("style", "border: black 1px dotted; background-color: #EEEEEE; padding: 8px").addText(b.toString()); 2917 } 2918 } 2919 2920 private String formatSCTDate(String ds) { 2921 SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd"); 2922 Date date; 2923 try { 2924 date = format.parse(ds); 2925 } catch (ParseException e) { 2926 return ds; 2927 } 2928 return new SimpleDateFormat("dd-MMM yyyy", new Locale("en", "US")).format(date); 2929 } 2930 2931 private String describeModule(String module) { 2932 if ("900000000000207008".equals(module)) 2933 return "International"; 2934 if ("731000124108".equals(module)) 2935 return "United States"; 2936 if ("32506021000036107".equals(module)) 2937 return "Australian"; 2938 if ("449081005".equals(module)) 2939 return "Spanish"; 2940 if ("554471000005108".equals(module)) 2941 return "Danish"; 2942 if ("11000146104".equals(module)) 2943 return "Dutch"; 2944 if ("45991000052106".equals(module)) 2945 return "Swedish"; 2946 if ("999000041000000102".equals(module)) 2947 return "United Kingdon"; 2948 return module; 2949 } 2950 2951 private boolean hasVersionParameter(ValueSetExpansionComponent expansion) { 2952 for (ValueSetExpansionParameterComponent p : expansion.getParameter()) { 2953 if (p.getName().equals("version")) 2954 return true; 2955 } 2956 return false; 2957 } 2958 2959 private void addLanguageRow(ValueSetExpansionContainsComponent c, XhtmlNode t, List<String> langs) { 2960 XhtmlNode tr = t.tr(); 2961 tr.td().addText(c.getCode()); 2962 for (String lang : langs) { 2963 String d = null; 2964 for (Extension ext : c.getExtension()) { 2965 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 2966 String l = ToolingExtensions.readStringExtension(ext, "lang"); 2967 if (lang.equals(l)) 2968 d = ToolingExtensions.readStringExtension(ext, "content"); 2969 } 2970 } 2971 tr.td().addText(d == null ? "" : d); 2972 } 2973 for (ValueSetExpansionContainsComponent cc : c.getContains()) { 2974 addLanguageRow(cc, t, langs); 2975 } 2976 } 2977 2978 2979 private String describeLang(String lang) { 2980 ValueSet v = context.fetchResource(ValueSet.class, "http://hl7.org/fhir/ValueSet/languages"); 2981 if (v != null) { 2982 ConceptReferenceComponent l = null; 2983 for (ConceptReferenceComponent cc : v.getCompose().getIncludeFirstRep().getConcept()) { 2984 if (cc.getCode().equals(lang)) 2985 l = cc; 2986 } 2987 if (l == null) { 2988 if (lang.contains("-")) 2989 lang = lang.substring(0, lang.indexOf("-")); 2990 for (ConceptReferenceComponent cc : v.getCompose().getIncludeFirstRep().getConcept()) { 2991 if (cc.getCode().equals(lang) || cc.getCode().startsWith(lang+"-")) 2992 l = cc; 2993 } 2994 } 2995 if (l != null) { 2996 if (lang.contains("-")) 2997 lang = lang.substring(0, lang.indexOf("-")); 2998 String en = l.getDisplay(); 2999 String nativelang = null; 3000 for (ConceptReferenceDesignationComponent cd : l.getDesignation()) { 3001 if (cd.getLanguage().equals(lang)) 3002 nativelang = cd.getValue(); 3003 } 3004 if (nativelang == null) 3005 return en+" ("+lang+")"; 3006 else 3007 return nativelang+" ("+en+", "+lang+")"; 3008 } 3009 } 3010 return lang; 3011 } 3012 3013 3014 private boolean checkDoDefinition(List<ValueSetExpansionContainsComponent> contains) { 3015 for (ValueSetExpansionContainsComponent c : contains) { 3016 CodeSystem cs = context.fetchCodeSystem(c.getSystem()); 3017 if (cs != null) 3018 return true; 3019 if (checkDoDefinition(c.getContains())) 3020 return true; 3021 } 3022 return false; 3023 } 3024 3025 3026 private boolean allFromOneSystem(ValueSet vs) { 3027 if (vs.getExpansion().getContains().isEmpty()) 3028 return false; 3029 String system = vs.getExpansion().getContains().get(0).getSystem(); 3030 for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) { 3031 if (!checkSystemMatches(system, cc)) 3032 return false; 3033 } 3034 return true; 3035 } 3036 3037 3038 private boolean checkSystemMatches(String system, ValueSetExpansionContainsComponent cc) { 3039 if (!system.equals(cc.getSystem())) 3040 return false; 3041 for (ValueSetExpansionContainsComponent cc1 : cc.getContains()) { 3042 if (!checkSystemMatches(system, cc1)) 3043 return false; 3044 } 3045 return true; 3046 } 3047 3048 3049 private boolean checkDoSystem(ValueSet vs, ValueSet src) { 3050 if (src != null) 3051 vs = src; 3052 return vs.hasCompose(); 3053 } 3054 3055 private boolean IsNotFixedExpansion(ValueSet vs) { 3056 if (vs.hasCompose()) 3057 return false; 3058 3059 3060 // it's not fixed if it has any includes that are not version fixed 3061 for (ConceptSetComponent cc : vs.getCompose().getInclude()) { 3062 if (cc.hasValueSet()) 3063 return true; 3064 if (!cc.hasVersion()) 3065 return true; 3066 } 3067 return false; 3068 } 3069 3070 3071 private void addLanguageRow(ConceptDefinitionComponent c, XhtmlNode t, List<String> langs) { 3072 XhtmlNode tr = t.tr(); 3073 tr.td().addText(c.getCode()); 3074 for (String lang : langs) { 3075 ConceptDefinitionDesignationComponent d = null; 3076 for (ConceptDefinitionDesignationComponent designation : c.getDesignation()) { 3077 if (designation.hasLanguage()) { 3078 if (lang.equals(designation.getLanguage())) 3079 d = designation; 3080 } 3081 } 3082 tr.td().addText(d == null ? "" : d.getValue()); 3083 } 3084 } 3085 3086// private void scanLangs(ConceptDefinitionComponent c, List<String> langs) { 3087// for (ConceptDefinitionDesignationComponent designation : c.getDesignation()) { 3088// if (designation.hasLanguage()) { 3089// String lang = designation.getLanguage(); 3090// if (langs != null && !langs.contains(lang) && c.hasDisplay() && !c.getDisplay().equalsIgnoreCase(designation.getValue())) 3091// langs.add(lang); 3092// } 3093// } 3094// for (ConceptDefinitionComponent g : c.getConcept()) 3095// scanLangs(g, langs); 3096// } 3097 3098 private void addMapHeaders(XhtmlNode tr, List<UsedConceptMap> maps) throws FHIRFormatError, DefinitionException, IOException { 3099 for (UsedConceptMap m : maps) { 3100 XhtmlNode td = tr.td(); 3101 XhtmlNode b = td.b(); 3102 XhtmlNode a = b.ah(prefix+m.getLink()); 3103 a.addText(m.getDetails().getName()); 3104 if (m.getDetails().isDoDescription() && m.getMap().hasDescription()) 3105 addMarkdown(td, m.getMap().getDescription()); 3106 } 3107 } 3108 3109 private void smartAddText(XhtmlNode p, String text) { 3110 if (text == null) 3111 return; 3112 3113 String[] lines = text.split("\\r\\n"); 3114 for (int i = 0; i < lines.length; i++) { 3115 if (i > 0) 3116 p.br(); 3117 p.addText(lines[i]); 3118 } 3119 } 3120 3121 private boolean conceptsHaveComments(ConceptDefinitionComponent c) { 3122 if (ToolingExtensions.hasCSComment(c)) 3123 return true; 3124 for (ConceptDefinitionComponent g : c.getConcept()) 3125 if (conceptsHaveComments(g)) 3126 return true; 3127 return false; 3128 } 3129 3130 private boolean conceptsHaveDisplay(ConceptDefinitionComponent c) { 3131 if (c.hasDisplay()) 3132 return true; 3133 for (ConceptDefinitionComponent g : c.getConcept()) 3134 if (conceptsHaveDisplay(g)) 3135 return true; 3136 return false; 3137 } 3138 3139 private boolean conceptsHaveVersion(ConceptDefinitionComponent c) { 3140 if (c.hasUserData("cs.version.notes")) 3141 return true; 3142 for (ConceptDefinitionComponent g : c.getConcept()) 3143 if (conceptsHaveVersion(g)) 3144 return true; 3145 return false; 3146 } 3147 3148 private boolean conceptsHaveDeprecated(CodeSystem cs, ConceptDefinitionComponent c) { 3149 if (CodeSystemUtilities.isDeprecated(cs, c)) 3150 return true; 3151 for (ConceptDefinitionComponent g : c.getConcept()) 3152 if (conceptsHaveDeprecated(cs, g)) 3153 return true; 3154 return false; 3155 } 3156 3157 private void generateCopyright(XhtmlNode x, ValueSet vs) { 3158 XhtmlNode p = x.para(); 3159 p.b().tx("Copyright Statement:"); 3160 smartAddText(p, " " + vs.getCopyright()); 3161 } 3162 3163 3164 private XhtmlNode addTableHeaderRowStandard(XhtmlNode t, boolean hasHierarchy, boolean hasDisplay, boolean definitions, boolean comments, boolean version, boolean deprecated, String lang) { 3165 XhtmlNode tr = t.tr(); 3166 if (hasHierarchy) 3167 tr.td().b().tx("Lvl"); 3168 tr.td().attribute("style", "white-space:nowrap").b().tx(context.translator().translate("xhtml-gen-cs", "Code", lang)); 3169 if (hasDisplay) 3170 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Display", lang)); 3171 if (definitions) 3172 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Definition", lang)); 3173 if (deprecated) 3174 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Deprecated", lang)); 3175 if (comments) 3176 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Comments", lang)); 3177 if (version) 3178 tr.td().b().tx(context.translator().translate("xhtml-gen-cs", "Version", lang)); 3179 return tr; 3180 } 3181 3182 private void addExpansionRowToTable(XhtmlNode t, ValueSetExpansionContainsComponent c, int i, boolean doLevel, boolean doSystem, boolean doDefinition, List<UsedConceptMap> maps, CodeSystem allCS, List<String> langs) { 3183 XhtmlNode tr = t.tr(); 3184 XhtmlNode td = tr.td(); 3185 3186 String tgt = makeAnchor(c.getSystem(), c.getCode()); 3187 td.an(tgt); 3188 3189 if (doLevel) { 3190 td.addText(Integer.toString(i)); 3191 td = tr.td(); 3192 } 3193 String s = Utilities.padLeft("", '\u00A0', i*2); 3194 td.attribute("style", "white-space:nowrap").addText(s); 3195 addCodeToTable(c.getAbstract(), c.getSystem(), c.getCode(), c.getDisplay(), td); 3196 if (doSystem) { 3197 td = tr.td(); 3198 td.addText(c.getSystem()); 3199 } 3200 td = tr.td(); 3201 if (c.hasDisplayElement()) 3202 td.addText(c.getDisplay()); 3203 3204 if (doDefinition) { 3205 CodeSystem cs = allCS; 3206 if (cs == null) 3207 cs = context.fetchCodeSystem(c.getSystem()); 3208 td = tr.td(); 3209 if (cs != null) 3210 td.addText(CodeSystemUtilities.getCodeDefinition(cs, c.getCode())); 3211 } 3212 for (UsedConceptMap m : maps) { 3213 td = tr.td(); 3214 List<TargetElementComponentWrapper> mappings = findMappingsForCode(c.getCode(), m.getMap()); 3215 boolean first = true; 3216 for (TargetElementComponentWrapper mapping : mappings) { 3217 if (!first) 3218 td.br(); 3219 first = false; 3220 XhtmlNode span = td.span(null, mapping.comp.getEquivalence().toString()); 3221 span.addText(getCharForEquivalence(mapping.comp)); 3222 addRefToCode(td, mapping.group.getTarget(), m.getLink(), mapping.comp.getCode()); 3223 if (!Utilities.noString(mapping.comp.getComment())) 3224 td.i().tx("("+mapping.comp.getComment()+")"); 3225 } 3226 } 3227 for (Extension ext : c.getExtension()) { 3228 if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { 3229 String lang = ToolingExtensions.readStringExtension(ext, "lang"); 3230 if (!Utilities.noString(lang) && !langs.contains(lang)) 3231 langs.add(lang); 3232 } 3233 } 3234 for (ValueSetExpansionContainsComponent cc : c.getContains()) { 3235 addExpansionRowToTable(t, cc, i+1, doLevel, doSystem, doDefinition, maps, allCS, langs); 3236 } 3237 } 3238 3239 private void addCodeToTable(boolean isAbstract, String system, String code, String display, XhtmlNode td) { 3240 CodeSystem e = context.fetchCodeSystem(system); 3241 if (e == null || e.getContent() != org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode.COMPLETE) { 3242 if (isAbstract) 3243 td.i().setAttribute("title", ABSTRACT_CODE_HINT).addText(code); 3244 else if ("http://snomed.info/sct".equals(system)) { 3245 td.ah(sctLink(code)).addText(code); 3246 } else if ("http://loinc.org".equals(system)) { 3247 td.ah("http://details.loinc.org/LOINC/"+code+".html").addText(code); 3248 } else 3249 td.addText(code); 3250 } else { 3251 String href = prefix+getCsRef(e); 3252 if (href.contains("#")) 3253 href = href + "-"+Utilities.nmtokenize(code); 3254 else 3255 href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(code); 3256 if (isAbstract) 3257 td.ah(href).setAttribute("title", ABSTRACT_CODE_HINT).i().addText(code); 3258 else 3259 td.ah(href).addText(code); 3260 } 3261 } 3262 3263 public String sctLink(String code) { 3264// if (snomedEdition != null) 3265// http://browser.ihtsdotools.org/?perspective=full&conceptId1=428041000124106&edition=us-edition&release=v20180301&server=https://prod-browser-exten.ihtsdotools.org/api/snomed&langRefset=900000000000509007 3266 return "http://browser.ihtsdotools.org/?perspective=full&conceptId1="+code; 3267 } 3268 3269 private class TargetElementComponentWrapper { 3270 private ConceptMapGroupComponent group; 3271 private TargetElementComponent comp; 3272 public TargetElementComponentWrapper(ConceptMapGroupComponent group, TargetElementComponent comp) { 3273 super(); 3274 this.group = group; 3275 this.comp = comp; 3276 } 3277 3278 } 3279 3280 private String langDisplay(String l, boolean isShort) { 3281 ValueSet vs = context.fetchResource(ValueSet.class, "http://hl7.org/fhir/ValueSet/languages"); 3282 for (ConceptReferenceComponent vc : vs.getCompose().getInclude().get(0).getConcept()) { 3283 if (vc.getCode().equals(l)) { 3284 for (ConceptReferenceDesignationComponent cd : vc.getDesignation()) { 3285 if (cd.getLanguage().equals(l)) 3286 return cd.getValue()+(isShort ? "" : " ("+vc.getDisplay()+")"); 3287 } 3288 return vc.getDisplay(); 3289 } 3290 } 3291 return "??Lang"; 3292 } 3293 3294 private boolean addDefineRowToTable(XhtmlNode t, ConceptDefinitionComponent c, int i, boolean hasHierarchy, boolean hasDisplay, boolean comment, boolean version, boolean deprecated, List<UsedConceptMap> maps, String system, CodeSystem cs, String lang) throws FHIRFormatError, DefinitionException, IOException { 3295 boolean hasExtensions = false; 3296 XhtmlNode tr = t.tr(); 3297 XhtmlNode td = tr.td(); 3298 if (hasHierarchy) { 3299 td.addText(Integer.toString(i+1)); 3300 td = tr.td(); 3301 String s = Utilities.padLeft("", '\u00A0', i*2); 3302 td.addText(s); 3303 } 3304 td.attribute("style", "white-space:nowrap").addText(c.getCode()); 3305 XhtmlNode a; 3306 if (c.hasCodeElement()) { 3307 td.an(cs.getId()+"-" + Utilities.nmtokenize(c.getCode())); 3308 } 3309 3310 if (hasDisplay) { 3311 td = tr.td(); 3312 if (c.hasDisplayElement()) { 3313 if (lang == null) { 3314 td.addText(c.getDisplay()); 3315 } else if (lang.equals("*")) { 3316 boolean sl = false; 3317 for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) 3318 if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "display") && cd.hasLanguage() && !c.getDisplay().equalsIgnoreCase(cd.getValue())) 3319 sl = true; 3320 td.addText((sl ? cs.getLanguage("en")+": " : "")+c.getDisplay()); 3321 for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) { 3322 if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "display") && cd.hasLanguage() && !c.getDisplay().equalsIgnoreCase(cd.getValue())) { 3323 td.br(); 3324 td.addText(cd.getLanguage()+": "+cd.getValue()); 3325 } 3326 } 3327 } else if (lang.equals(cs.getLanguage()) || (lang.equals("en") && !cs.hasLanguage())) { 3328 td.addText(c.getDisplay()); 3329 } else { 3330 for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) { 3331 if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "display") && cd.hasLanguage() && cd.getLanguage().equals(lang)) { 3332 td.addText(cd.getValue()); 3333 } 3334 } 3335 } 3336 } 3337 } 3338 td = tr.td(); 3339 if (c != null && 3340 c.hasDefinitionElement()) { 3341 if (lang == null) { 3342 if (hasMarkdownInDefinitions(cs)) 3343 addMarkdown(td, c.getDefinition()); 3344 else 3345 td.addText(c.getDefinition()); 3346 } else if (lang.equals("*")) { 3347 boolean sl = false; 3348 for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) 3349 if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "definition") && cd.hasLanguage() && !c.getDefinition().equalsIgnoreCase(cd.getValue())) 3350 sl = true; 3351 td.addText((sl ? cs.getLanguage("en")+": " : "")+c.getDefinition()); 3352 for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) { 3353 if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "definition") && cd.hasLanguage() && !c.getDefinition().equalsIgnoreCase(cd.getValue())) { 3354 td.br(); 3355 td.addText(cd.getLanguage()+": "+cd.getValue()); 3356 } 3357 } 3358 } else if (lang.equals(cs.getLanguage()) || (lang.equals("en") && !cs.hasLanguage())) { 3359 td.addText(c.getDefinition()); 3360 } else { 3361 for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) { 3362 if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "definition") && cd.hasLanguage() && cd.getLanguage().equals(lang)) { 3363 td.addText(cd.getValue()); 3364 } 3365 } 3366 } 3367 } 3368 if (deprecated) { 3369 td = tr.td(); 3370 Boolean b = CodeSystemUtilities.isDeprecated(cs, c); 3371 if (b != null && b) { 3372 smartAddText(td, context.translator().translate("xhtml-gen-cs", "Deprecated", lang)); 3373 hasExtensions = true; 3374 if (ToolingExtensions.hasExtension(c, ToolingExtensions.EXT_REPLACED_BY)) { 3375 Coding cc = (Coding) ToolingExtensions.getExtension(c, ToolingExtensions.EXT_REPLACED_BY).getValue(); 3376 td.tx(" (replaced by "); 3377 String url = getCodingReference(cc, system); 3378 if (url != null) { 3379 td.ah(url).addText(cc.getCode()); 3380 td.tx(": "+cc.getDisplay()+")"); 3381 } else 3382 td.addText(cc.getCode()+" '"+cc.getDisplay()+"' in "+cc.getSystem()+")"); 3383 } 3384 } 3385 } 3386 if (comment) { 3387 td = tr.td(); 3388 Extension ext = c.getExtensionByUrl(ToolingExtensions.EXT_CS_COMMENT); 3389 if (ext != null) { 3390 hasExtensions = true; 3391 String bc = ext.hasValue() ? ext.getValue().primitiveValue() : null; 3392 Map<String, String> translations = ToolingExtensions.getLanguageTranslations(ext.getValue()); 3393 3394 if (lang == null) { 3395 if (bc != null) 3396 td.addText(bc); 3397 } else if (lang.equals("*")) { 3398 boolean sl = false; 3399 for (String l : translations.keySet()) 3400 if (bc == null || !bc.equalsIgnoreCase(translations.get(l))) 3401 sl = true; 3402 if (bc != null) { 3403 td.addText((sl ? cs.getLanguage("en")+": " : "")+bc); 3404 } 3405 for (String l : translations.keySet()) { 3406 if (bc == null || !bc.equalsIgnoreCase(translations.get(l))) { 3407 if (!td.getChildNodes().isEmpty()) 3408 td.br(); 3409 td.addText(l+": "+translations.get(l)); 3410 } 3411 } 3412 } else if (lang.equals(cs.getLanguage()) || (lang.equals("en") && !cs.hasLanguage())) { 3413 if (bc != null) 3414 td.addText(bc); 3415 } else { 3416 if (bc != null) 3417 translations.put(cs.getLanguage("en"), bc); 3418 for (String l : translations.keySet()) { 3419 if (l.equals(lang)) { 3420 td.addText(translations.get(l)); 3421 } 3422 } 3423 } 3424 } 3425 } 3426 if (version) { 3427 td = tr.td(); 3428 if (c.hasUserData("cs.version.notes")) 3429 td.addText(c.getUserString("cs.version.notes")); 3430 } 3431 for (UsedConceptMap m : maps) { 3432 td = tr.td(); 3433 List<TargetElementComponentWrapper> mappings = findMappingsForCode(c.getCode(), m.getMap()); 3434 boolean first = true; 3435 for (TargetElementComponentWrapper mapping : mappings) { 3436 if (!first) 3437 td.br(); 3438 first = false; 3439 XhtmlNode span = td.span(null, mapping.comp.hasEquivalence() ? mapping.comp.getEquivalence().toCode() : ""); 3440 span.addText(getCharForEquivalence(mapping.comp)); 3441 a = td.ah(prefix+m.getLink()+"#"+makeAnchor(mapping.group.getTarget(), mapping.comp.getCode())); 3442 a.addText(mapping.comp.getCode()); 3443 if (!Utilities.noString(mapping.comp.getComment())) 3444 td.i().tx("("+mapping.comp.getComment()+")"); 3445 } 3446 } 3447 for (String e : CodeSystemUtilities.getOtherChildren(cs, c)) { 3448 tr = t.tr(); 3449 td = tr.td(); 3450 String s = Utilities.padLeft("", '.', i*2); 3451 td.addText(s); 3452 a = td.ah("#"+Utilities.nmtokenize(e)); 3453 a.addText(c.getCode()); 3454 } 3455 for (ConceptDefinitionComponent cc : c.getConcept()) { 3456 hasExtensions = addDefineRowToTable(t, cc, i+1, hasHierarchy, hasDisplay, comment, version, deprecated, maps, system, cs, lang) || hasExtensions; 3457 } 3458 return hasExtensions; 3459 } 3460 3461 3462 private boolean hasMarkdownInDefinitions(CodeSystem cs) { 3463 return ToolingExtensions.readBoolExtension(cs, "http://hl7.org/fhir/StructureDefinition/codesystem-use-markdown"); 3464 } 3465 3466 private String makeAnchor(String codeSystem, String code) { 3467 String s = codeSystem+'-'+code; 3468 StringBuilder b = new StringBuilder(); 3469 for (char c : s.toCharArray()) { 3470 if (Character.isAlphabetic(c) || Character.isDigit(c) || c == '.') 3471 b.append(c); 3472 else 3473 b.append('-'); 3474 } 3475 return b.toString(); 3476 } 3477 3478 private String getCodingReference(Coding cc, String system) { 3479 if (cc.getSystem().equals(system)) 3480 return "#"+cc.getCode(); 3481 if (cc.getSystem().equals("http://snomed.info/sct")) 3482 return "http://snomed.info/sct/"+cc.getCode(); 3483 if (cc.getSystem().equals("http://loinc.org")) 3484 return "http://s.details.loinc.org/LOINC/"+cc.getCode()+".html"; 3485 return null; 3486 } 3487 3488 private String getCharForEquivalence(TargetElementComponent mapping) { 3489 if (!mapping.hasEquivalence()) 3490 return ""; 3491 switch (mapping.getEquivalence()) { 3492 case EQUAL : return "="; 3493 case EQUIVALENT : return "~"; 3494 case WIDER : return "<"; 3495 case NARROWER : return ">"; 3496 case INEXACT : return "><"; 3497 case UNMATCHED : return "-"; 3498 case DISJOINT : return "!="; 3499 default: return "?"; 3500 } 3501 } 3502 3503 private List<TargetElementComponentWrapper> findMappingsForCode(String code, ConceptMap map) { 3504 List<TargetElementComponentWrapper> mappings = new ArrayList<TargetElementComponentWrapper>(); 3505 3506 for (ConceptMapGroupComponent g : map.getGroup()) { 3507 for (SourceElementComponent c : g.getElement()) { 3508 if (c.getCode().equals(code)) 3509 for (TargetElementComponent cc : c.getTarget()) 3510 mappings.add(new TargetElementComponentWrapper(g, cc)); 3511 } 3512 } 3513 return mappings; 3514 } 3515 3516 private boolean generateComposition(ResourceContext rcontext, XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRException, IOException { 3517 boolean hasExtensions = false; 3518 List<String> langs = new ArrayList<String>(); 3519 3520 if (header) { 3521 XhtmlNode h = x.h2(); 3522 h.addText(vs.present()); 3523 addMarkdown(x, vs.getDescription()); 3524 if (vs.hasCopyrightElement()) 3525 generateCopyright(x, vs); 3526 } 3527 XhtmlNode p = x.para(); 3528 p.tx("This value set includes codes from the following code systems:"); 3529 3530 XhtmlNode ul = x.ul(); 3531 XhtmlNode li; 3532 for (ConceptSetComponent inc : vs.getCompose().getInclude()) { 3533 hasExtensions = genInclude(rcontext, ul, inc, "Include", langs, maps) || hasExtensions; 3534 } 3535 for (ConceptSetComponent exc : vs.getCompose().getExclude()) { 3536 hasExtensions = genInclude(rcontext, ul, exc, "Exclude", langs, maps) || hasExtensions; 3537 } 3538 3539 // now, build observed languages 3540 3541 if (langs.size() > 0) { 3542 Collections.sort(langs); 3543 x.para().b().tx("Additional Language Displays"); 3544 XhtmlNode t = x.table( "codes"); 3545 XhtmlNode tr = t.tr(); 3546 tr.td().b().tx("Code"); 3547 for (String lang : langs) 3548 tr.td().b().addText(describeLang(lang)); 3549 for (ConceptSetComponent c : vs.getCompose().getInclude()) { 3550 for (ConceptReferenceComponent cc : c.getConcept()) { 3551 addLanguageRow(cc, t, langs); 3552 } 3553 } 3554 } 3555 3556 return hasExtensions; 3557 } 3558 3559 private void addLanguageRow(ConceptReferenceComponent c, XhtmlNode t, List<String> langs) { 3560 XhtmlNode tr = t.tr(); 3561 tr.td().addText(c.getCode()); 3562 for (String lang : langs) { 3563 String d = null; 3564 for (ConceptReferenceDesignationComponent cd : c.getDesignation()) { 3565 String l = cd.getLanguage(); 3566 if (lang.equals(l)) 3567 d = cd.getValue(); 3568 } 3569 tr.td().addText(d == null ? "" : d); 3570 } 3571 } 3572 3573 private void AddVsRef(ResourceContext rcontext, String value, XhtmlNode li) { 3574 Resource res = rcontext == null ? null : rcontext.resolve(value); 3575 if (res != null && !(res instanceof MetadataResource)) { 3576 li.addText(value); 3577 return; 3578 } 3579 MetadataResource vs = (MetadataResource) res; 3580 if (vs == null) 3581 vs = context.fetchResource(ValueSet.class, value); 3582 if (vs == null) 3583 vs = context.fetchResource(StructureDefinition.class, value); 3584// if (vs == null) 3585 // vs = context.fetchResource(DataElement.class, value); 3586 if (vs == null) 3587 vs = context.fetchResource(Questionnaire.class, value); 3588 if (vs != null) { 3589 String ref = (String) vs.getUserData("path"); 3590 3591 ref = adjustForPath(ref); 3592 XhtmlNode a = li.ah(ref == null ? "??" : ref.replace("\\", "/")); 3593 a.addText(value); 3594 } else { 3595 CodeSystem cs = context.fetchCodeSystem(value); 3596 if (cs != null) { 3597 String ref = (String) cs.getUserData("path"); 3598 ref = adjustForPath(ref); 3599 XhtmlNode a = li.ah(ref == null ? "??" : ref.replace("\\", "/")); 3600 a.addText(value); 3601 } else if (value.equals("http://snomed.info/sct") || value.equals("http://snomed.info/id")) { 3602 XhtmlNode a = li.ah(value); 3603 a.tx("SNOMED-CT"); 3604 } 3605 else { 3606 if (value.startsWith("http://hl7.org") && !Utilities.existsInList(value, "http://hl7.org/fhir/sid/icd-10-us")) 3607 System.out.println("Unable to resolve value set "+value); 3608 li.addText(value); 3609 } 3610 } 3611 } 3612 3613 private String adjustForPath(String ref) { 3614 if (prefix == null) 3615 return ref; 3616 else 3617 return prefix+ref; 3618 } 3619 3620 private boolean genInclude(ResourceContext rcontext, XhtmlNode ul, ConceptSetComponent inc, String type, List<String> langs, List<UsedConceptMap> maps) throws FHIRException, IOException { 3621 boolean hasExtensions = false; 3622 XhtmlNode li; 3623 li = ul.li(); 3624 CodeSystem e = context.fetchCodeSystem(inc.getSystem()); 3625 3626 if (inc.hasSystem()) { 3627 if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) { 3628 li.addText(type+" all codes defined in "); 3629 addCsRef(inc, li, e); 3630 } else { 3631 if (inc.getConcept().size() > 0) { 3632 li.addText(type+" these codes as defined in "); 3633 addCsRef(inc, li, e); 3634 3635 XhtmlNode t = li.table("none"); 3636 boolean hasComments = false; 3637 boolean hasDefinition = false; 3638 for (ConceptReferenceComponent c : inc.getConcept()) { 3639 hasComments = hasComments || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT); 3640 hasDefinition = hasDefinition || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION); 3641 } 3642 if (hasComments || hasDefinition) 3643 hasExtensions = true; 3644 addMapHeaders(addTableHeaderRowStandard(t, false, true, hasDefinition, hasComments, false, false, null), maps); 3645 for (ConceptReferenceComponent c : inc.getConcept()) { 3646 XhtmlNode tr = t.tr(); 3647 XhtmlNode td = tr.td(); 3648 ConceptDefinitionComponent cc = getConceptForCode(e, c.getCode(), inc); 3649 addCodeToTable(false, inc.getSystem(), c.getCode(), c.hasDisplay()? c.getDisplay() : cc != null ? cc.getDisplay() : "", td); 3650 3651 td = tr.td(); 3652 if (!Utilities.noString(c.getDisplay())) 3653 td.addText(c.getDisplay()); 3654 else if (cc != null && !Utilities.noString(cc.getDisplay())) 3655 td.addText(cc.getDisplay()); 3656 3657 td = tr.td(); 3658 if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION)) 3659 smartAddText(td, ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_DEFINITION)); 3660 else if (cc != null && !Utilities.noString(cc.getDefinition())) 3661 smartAddText(td, cc.getDefinition()); 3662 3663 if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT)) { 3664 smartAddText(tr.td(), "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_VS_COMMENT)); 3665 } 3666 for (ConceptReferenceDesignationComponent cd : c.getDesignation()) { 3667 if (cd.hasLanguage() && !langs.contains(cd.getLanguage())) 3668 langs.add(cd.getLanguage()); 3669 } 3670 } 3671 } 3672 boolean first = true; 3673 for (ConceptSetFilterComponent f : inc.getFilter()) { 3674 if (first) { 3675 li.addText(type+" codes from "); 3676 first = false; 3677 } else 3678 li.tx(" and "); 3679 addCsRef(inc, li, e); 3680 li.tx(" where "+f.getProperty()+" "+describe(f.getOp())+" "); 3681 if (e != null && codeExistsInValueSet(e, f.getValue())) { 3682 String href = prefix+getCsRef(e); 3683 if (href.contains("#")) 3684 href = href + "-"+Utilities.nmtokenize(f.getValue()); 3685 else 3686 href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(f.getValue()); 3687 li.ah(href).addText(f.getValue()); 3688 } else if ("concept".equals(f.getProperty()) && inc.hasSystem()) { 3689 li.addText(f.getValue()); 3690 ValidationResult vr = context.validateCode(terminologyServiceOptions, inc.getSystem(), f.getValue(), null); 3691 if (vr.isOk()) { 3692 li.tx(" ("+vr.getDisplay()+")"); 3693 } 3694 } 3695 else 3696 li.addText(f.getValue()); 3697 String disp = ToolingExtensions.getDisplayHint(f); 3698 if (disp != null) 3699 li.tx(" ("+disp+")"); 3700 } 3701 } 3702 if (inc.hasValueSet()) { 3703 li.tx(", where the codes are contained in "); 3704 boolean first = true; 3705 for (UriType vs : inc.getValueSet()) { 3706 if (first) 3707 first = false; 3708 else 3709 li.tx(", "); 3710 AddVsRef(rcontext, vs.asStringValue(), li); 3711 } 3712 } 3713 } else { 3714 li.tx("Import all the codes that are contained in "); 3715 boolean first = true; 3716 for (UriType vs : inc.getValueSet()) { 3717 if (first) 3718 first = false; 3719 else 3720 li.tx(", "); 3721 AddVsRef(rcontext, vs.asStringValue(), li); 3722 } 3723 } 3724 return hasExtensions; 3725 } 3726 3727 private String describe(FilterOperator op) { 3728 switch (op) { 3729 case EQUAL: return " = "; 3730 case ISA: return " is-a "; 3731 case ISNOTA: return " is-not-a "; 3732 case REGEX: return " matches (by regex) "; 3733 case NULL: return " ?? "; 3734 case IN: return " in "; 3735 case NOTIN: return " not in "; 3736 case DESCENDENTOF: return " descends from "; 3737 case EXISTS: return " exists "; 3738 case GENERALIZES: return " generalizes "; 3739 } 3740 return null; 3741 } 3742 3743 private ConceptDefinitionComponent getConceptForCode(CodeSystem e, String code, ConceptSetComponent inc) { 3744 // first, look in the code systems 3745 if (e == null) 3746 e = context.fetchCodeSystem(inc.getSystem()); 3747 if (e != null) { 3748 ConceptDefinitionComponent v = getConceptForCode(e.getConcept(), code); 3749 if (v != null) 3750 return v; 3751 } 3752 3753 if (!context.hasCache()) { 3754 ValueSetExpansionComponent vse; 3755 try { 3756 ValueSetExpansionOutcome vso = context.expandVS(inc, false); 3757 ValueSet valueset = vso.getValueset(); 3758 if (valueset == null) 3759 throw new TerminologyServiceException("Error Expanding ValueSet: "+vso.getError()); 3760 vse = valueset.getExpansion(); 3761 3762 } catch (TerminologyServiceException e1) { 3763 return null; 3764 } 3765 if (vse != null) { 3766 ConceptDefinitionComponent v = getConceptForCodeFromExpansion(vse.getContains(), code); 3767 if (v != null) 3768 return v; 3769 } 3770 } 3771 3772 return context.validateCode(terminologyServiceOptions, inc.getSystem(), code, null).asConceptDefinition(); 3773 } 3774 3775 3776 3777 private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> list, String code) { 3778 for (ConceptDefinitionComponent c : list) { 3779 if (code.equals(c.getCode())) 3780 return c; 3781 ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code); 3782 if (v != null) 3783 return v; 3784 } 3785 return null; 3786 } 3787 3788 private ConceptDefinitionComponent getConceptForCodeFromExpansion(List<ValueSetExpansionContainsComponent> list, String code) { 3789 for (ValueSetExpansionContainsComponent c : list) { 3790 if (code.equals(c.getCode())) { 3791 ConceptDefinitionComponent res = new ConceptDefinitionComponent(); 3792 res.setCode(c.getCode()); 3793 res.setDisplay(c.getDisplay()); 3794 return res; 3795 } 3796 ConceptDefinitionComponent v = getConceptForCodeFromExpansion(c.getContains(), code); 3797 if (v != null) 3798 return v; 3799 } 3800 return null; 3801 } 3802 3803 private void addRefToCode(XhtmlNode td, String target, String vslink, String code) { 3804 CodeSystem cs = context.fetchCodeSystem(target); 3805 String cslink = getCsRef(cs); 3806 XhtmlNode a = null; 3807 if (cslink != null) 3808 a = td.ah(prefix+cslink+"#"+cs.getId()+"-"+code); 3809 else 3810 a = td.ah(prefix+vslink+"#"+code); 3811 a.addText(code); 3812 } 3813 3814 private <T extends Resource> void addCsRef(ConceptSetComponent inc, XhtmlNode li, T cs) { 3815 String ref = null; 3816 boolean addHtml = true; 3817 if (cs != null) { 3818 ref = (String) cs.getUserData("external.url"); 3819 if (Utilities.noString(ref)) 3820 ref = (String) cs.getUserData("filename"); 3821 else 3822 addHtml = false; 3823 if (Utilities.noString(ref)) 3824 ref = (String) cs.getUserData("path"); 3825 } 3826 String spec = getSpecialReference(inc.getSystem()); 3827 if (spec != null) { 3828 XhtmlNode a = li.ah(spec); 3829 a.code(inc.getSystem()); 3830 } else if (cs != null && ref != null) { 3831 if (!Utilities.noString(prefix) && ref.startsWith("http://hl7.org/fhir/")) 3832 ref = ref.substring(20)+"/index.html"; 3833 else if (addHtml && !ref.contains(".html")) 3834 ref = ref + ".html"; 3835 XhtmlNode a = li.ah(prefix+ref.replace("\\", "/")); 3836 a.code(inc.getSystem()); 3837 } else { 3838 li.code(inc.getSystem()); 3839 } 3840 } 3841 3842 private String getSpecialReference(String system) { 3843 if ("http://snomed.info/sct".equals(system)) 3844 return "http://www.snomed.org/"; 3845 if (Utilities.existsInList(system, "http://loinc.org", "http://unitsofmeasure.org", "http://www.nlm.nih.gov/research/umls/rxnorm", "http://ncimeta.nci.nih.gov", "http://fdasis.nlm.nih.gov", 3846 "http://www.radlex.org", "http://www.whocc.no/atc", "http://dicom.nema.org/resources/ontology/DCM", "http://www.genenames.org", "http://www.ensembl.org", "http://www.ncbi.nlm.nih.gov/nuccore", 3847 "http://www.ncbi.nlm.nih.gov/clinvar", "http://sequenceontology.org", "http://www.hgvs.org/mutnomen", "http://www.ncbi.nlm.nih.gov/projects/SNP", "http://cancer.sanger.ac.uk/cancergenome/projects/cosmic", 3848 "http://www.lrg-sequence.org", "http://www.omim.org", "http://www.ncbi.nlm.nih.gov/pubmed", "http://www.pharmgkb.org", "http://clinicaltrials.gov", "http://www.ebi.ac.uk/ipd/imgt/hla/")) 3849 return system; 3850 3851 return null; 3852 } 3853 3854 private String getCsRef(String system) { 3855 CodeSystem cs = context.fetchCodeSystem(system); 3856 return getCsRef(cs); 3857 } 3858 3859 private <T extends Resource> String getCsRef(T cs) { 3860 String ref = (String) cs.getUserData("filename"); 3861 if (ref == null) 3862 ref = (String) cs.getUserData("path"); 3863 if (ref == null) 3864 return "??.html"; 3865 if (!ref.contains(".html")) 3866 ref = ref + ".html"; 3867 return ref.replace("\\", "/"); 3868 } 3869 3870 private boolean codeExistsInValueSet(CodeSystem cs, String code) { 3871 for (ConceptDefinitionComponent c : cs.getConcept()) { 3872 if (inConcept(code, c)) 3873 return true; 3874 } 3875 return false; 3876 } 3877 3878 private boolean inConcept(String code, ConceptDefinitionComponent c) { 3879 if (c.hasCodeElement() && c.getCode().equals(code)) 3880 return true; 3881 for (ConceptDefinitionComponent g : c.getConcept()) { 3882 if (inConcept(code, g)) 3883 return true; 3884 } 3885 return false; 3886 } 3887 3888 /** 3889 * This generate is optimised for the build tool in that it tracks the source extension. 3890 * But it can be used for any other use. 3891 * 3892 * @param vs 3893 * @param codeSystems 3894 * @throws DefinitionException 3895 * @throws Exception 3896 */ 3897 public boolean generate(ResourceContext rcontext, OperationOutcome op) throws DefinitionException { 3898 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 3899 boolean hasSource = false; 3900 boolean success = true; 3901 for (OperationOutcomeIssueComponent i : op.getIssue()) { 3902 success = success && i.getSeverity() == IssueSeverity.INFORMATION; 3903 hasSource = hasSource || ExtensionHelper.hasExtension(i, ToolingExtensions.EXT_ISSUE_SOURCE); 3904 } 3905 if (success) 3906 x.para().tx("All OK"); 3907 if (op.getIssue().size() > 0) { 3908 XhtmlNode tbl = x.table("grid"); // on the basis that we'll most likely be rendered using the standard fhir css, but it doesn't really matter 3909 XhtmlNode tr = tbl.tr(); 3910 tr.td().b().tx("Severity"); 3911 tr.td().b().tx("Location"); 3912 tr.td().b().tx("Code"); 3913 tr.td().b().tx("Details"); 3914 tr.td().b().tx("Diagnostics"); 3915 if (hasSource) 3916 tr.td().b().tx("Source"); 3917 for (OperationOutcomeIssueComponent i : op.getIssue()) { 3918 tr = tbl.tr(); 3919 tr.td().addText(i.getSeverity().toString()); 3920 XhtmlNode td = tr.td(); 3921 boolean d = false; 3922 for (StringType s : i.getLocation()) { 3923 if (d) 3924 td.tx(", "); 3925 else 3926 d = true; 3927 td.addText(s.getValue()); 3928 } 3929 tr.td().addText(i.getCode().getDisplay()); 3930 tr.td().addText(gen(i.getDetails())); 3931 smartAddText(tr.td(), i.getDiagnostics()); 3932 if (hasSource) { 3933 Extension ext = ExtensionHelper.getExtension(i, ToolingExtensions.EXT_ISSUE_SOURCE); 3934 tr.td().addText(ext == null ? "" : gen(ext)); 3935 } 3936 } 3937 } 3938 inject(op, x, hasSource ? NarrativeStatus.EXTENSIONS : NarrativeStatus.GENERATED); 3939 return true; 3940 } 3941 3942 3943 public String genType(Type type) throws DefinitionException { 3944 if (type instanceof Coding) 3945 return gen((Coding) type); 3946 if (type instanceof CodeableConcept) 3947 return displayCodeableConcept((CodeableConcept) type); 3948 if (type instanceof Quantity) 3949 return displayQuantity((Quantity) type); 3950 if (type instanceof Range) 3951 return displayRange((Range) type); 3952 return null; 3953 } 3954 private String gen(Extension extension) throws DefinitionException { 3955 if (extension.getValue() instanceof CodeType) 3956 return ((CodeType) extension.getValue()).getValue(); 3957 if (extension.getValue() instanceof Coding) 3958 return gen((Coding) extension.getValue()); 3959 3960 throw new DefinitionException("Unhandled type "+extension.getValue().getClass().getName()); 3961 } 3962 3963 public String gen(CodeableConcept code) { 3964 if (code == null) 3965 return null; 3966 if (code.hasText()) 3967 return code.getText(); 3968 if (code.hasCoding()) 3969 return gen(code.getCoding().get(0)); 3970 return null; 3971 } 3972 3973 public String gen(Coding code) { 3974 if (code == null) 3975 return null; 3976 if (code.hasDisplayElement()) 3977 return code.getDisplay(); 3978 if (code.hasCodeElement()) 3979 return code.getCode(); 3980 return null; 3981 } 3982 3983 public boolean generate(ResourceContext rcontext, StructureDefinition sd, Set<String> outputTracker) throws EOperationOutcome, FHIRException, IOException { 3984 ProfileUtilities pu = new ProfileUtilities(context, null, pkp); 3985 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 3986 x.getChildNodes().add(pu.generateTable(definitionsTarget, sd, true, destDir, false, sd.getId(), false, corePath, "", false, false, outputTracker)); 3987 inject(sd, x, NarrativeStatus.GENERATED); 3988 return true; 3989 } 3990 public boolean generate(ResourceContext rcontext, ImplementationGuide ig) throws EOperationOutcome, FHIRException, IOException { 3991 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 3992 x.h2().addText(ig.getName()); 3993 x.para().tx("The official URL for this implementation guide is: "); 3994 x.pre().tx(ig.getUrl()); 3995 addMarkdown(x, ig.getDescription()); 3996 inject(ig, x, NarrativeStatus.GENERATED); 3997 return true; 3998 } 3999 public boolean generate(ResourceContext rcontext, OperationDefinition opd) throws EOperationOutcome, FHIRException, IOException { 4000 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 4001 x.h2().addText(opd.getName()); 4002 x.para().addText(Utilities.capitalize(opd.getKind().toString())+": "+opd.getName()); 4003 x.para().tx("The official URL for this operation definition is: "); 4004 x.pre().tx(opd.getUrl()); 4005 addMarkdown(x, opd.getDescription()); 4006 4007 if (opd.getSystem()) 4008 x.para().tx("URL: [base]/$"+opd.getCode()); 4009 for (CodeType c : opd.getResource()) { 4010 if (opd.getType()) 4011 x.para().tx("URL: [base]/"+c.getValue()+"/$"+opd.getCode()); 4012 if (opd.getInstance()) 4013 x.para().tx("URL: [base]/"+c.getValue()+"/[id]/$"+opd.getCode()); 4014 } 4015 4016 x.para().tx("Parameters"); 4017 XhtmlNode tbl = x.table( "grid"); 4018 XhtmlNode tr = tbl.tr(); 4019 tr.td().b().tx("Use"); 4020 tr.td().b().tx("Name"); 4021 tr.td().b().tx("Cardinality"); 4022 tr.td().b().tx("Type"); 4023 tr.td().b().tx("Binding"); 4024 tr.td().b().tx("Documentation"); 4025 for (OperationDefinitionParameterComponent p : opd.getParameter()) { 4026 genOpParam(rcontext, tbl, "", p); 4027 } 4028 addMarkdown(x, opd.getComment()); 4029 inject(opd, x, NarrativeStatus.GENERATED); 4030 return true; 4031 } 4032 4033 private void genOpParam(ResourceContext rcontext, XhtmlNode tbl, String path, OperationDefinitionParameterComponent p) throws EOperationOutcome, FHIRException, IOException { 4034 XhtmlNode tr; 4035 tr = tbl.tr(); 4036 tr.td().addText(p.getUse().toString()); 4037 tr.td().addText(path+p.getName()); 4038 tr.td().addText(Integer.toString(p.getMin())+".."+p.getMax()); 4039 XhtmlNode td = tr.td(); 4040 StructureDefinition sd = context.fetchTypeDefinition(p.getType()); 4041 if (sd == null) 4042 td.tx(p.hasType() ? p.getType() : ""); 4043 else if (sd.getAbstract() && p.hasExtension(ToolingExtensions.EXT_ALLOWED_TYPE)) { 4044 boolean first = true; 4045 for (Extension ex : p.getExtensionsByUrl(ToolingExtensions.EXT_ALLOWED_TYPE)) { 4046 if (first) first = false; else td.tx(" | "); 4047 String s = ex.getValue().primitiveValue(); 4048 StructureDefinition sdt = context.fetchTypeDefinition(s); 4049 if (sdt == null) 4050 td.tx(p.hasType() ? p.getType() : ""); 4051 else 4052 td.ah(sdt.getUserString("path")).tx(s); 4053 } 4054 } else 4055 td.ah(sd.getUserString("path")).tx(p.hasType() ? p.getType() : ""); 4056 if (p.hasSearchType()) { 4057 td.br(); 4058 td.tx("("); 4059 td.ah( corePath == null ? "search.html#"+p.getSearchType().toCode() : Utilities.pathURL(corePath, "search.html#"+p.getSearchType().toCode())).tx(p.getSearchType().toCode()); 4060 td.tx(")"); 4061 } 4062 td = tr.td(); 4063 if (p.hasBinding() && p.getBinding().hasValueSet()) { 4064 AddVsRef(rcontext, p.getBinding().getValueSet(), td); 4065 td.tx(" ("+p.getBinding().getStrength().getDisplay()+")"); 4066 } 4067 addMarkdown(tr.td(), p.getDocumentation()); 4068 if (!p.hasType()) { 4069 for (OperationDefinitionParameterComponent pp : p.getPart()) { 4070 genOpParam(rcontext, tbl, path+p.getName()+".", pp); 4071 } 4072 } 4073 } 4074 4075 private void addMarkdown(XhtmlNode x, String text) throws FHIRFormatError, IOException, DefinitionException { 4076 if (text != null) { 4077 // 1. custom FHIR extensions 4078 while (text.contains("[[[")) { 4079 String left = text.substring(0, text.indexOf("[[[")); 4080 String link = text.substring(text.indexOf("[[[")+3, text.indexOf("]]]")); 4081 String right = text.substring(text.indexOf("]]]")+3); 4082 String url = link; 4083 String[] parts = link.split("\\#"); 4084 StructureDefinition p = context.fetchResource(StructureDefinition.class, parts[0]); 4085 if (p == null) 4086 p = context.fetchTypeDefinition(parts[0]); 4087 if (p == null) 4088 p = context.fetchResource(StructureDefinition.class, link); 4089 if (p != null) { 4090 url = p.getUserString("path"); 4091 if (url == null) 4092 url = p.getUserString("filename"); 4093 } else 4094 throw new DefinitionException("Unable to resolve markdown link "+link); 4095 4096 text = left+"["+link+"]("+url+")"+right; 4097 } 4098 4099 // 2. markdown 4100 String s = markdown.process(Utilities.escapeXml(text), "narrative generator"); 4101 XhtmlParser p = new XhtmlParser(); 4102 XhtmlNode m; 4103 try { 4104 m = p.parse("<div>"+s+"</div>", "div"); 4105 } catch (org.hl7.fhir.exceptions.FHIRFormatError e) { 4106 throw new FHIRFormatError(e.getMessage(), e); 4107 } 4108 x.getChildNodes().addAll(m.getChildNodes()); 4109 } 4110 } 4111 4112 public boolean generate(ResourceContext rcontext, CompartmentDefinition cpd) { 4113 StringBuilder in = new StringBuilder(); 4114 StringBuilder out = new StringBuilder(); 4115 for (CompartmentDefinitionResourceComponent cc: cpd.getResource()) { 4116 CommaSeparatedStringBuilder rules = new CommaSeparatedStringBuilder(); 4117 if (!cc.hasParam()) { 4118 out.append(" <li><a href=\"").append(cc.getCode().toLowerCase()).append(".html\">").append(cc.getCode()).append("</a></li>\r\n"); 4119 } else if (!rules.equals("{def}")) { 4120 for (StringType p : cc.getParam()) 4121 rules.append(p.asStringValue()); 4122 in.append(" <tr><td><a href=\"").append(cc.getCode().toLowerCase()).append(".html\">").append(cc.getCode()).append("</a></td><td>").append(rules.toString()).append("</td></tr>\r\n"); 4123 } 4124 } 4125 XhtmlNode x; 4126 try { 4127 x = new XhtmlParser().parseFragment("<div><p>\r\nThe following resources may be in this compartment:\r\n</p>\r\n" + 4128 "<table class=\"grid\">\r\n"+ 4129 " <tr><td><b>Resource</b></td><td><b>Inclusion Criteria</b></td></tr>\r\n"+ 4130 in.toString()+ 4131 "</table>\r\n"+ 4132 "<p>\r\nA resource is in this compartment if the nominated search parameter (or chain) refers to the patient resource that defines the compartment.\r\n</p>\r\n" + 4133 "<p>\r\n\r\n</p>\r\n" + 4134 "<p>\r\nThe following resources are never in this compartment:\r\n</p>\r\n" + 4135 "<ul>\r\n"+ 4136 out.toString()+ 4137 "</ul></div>\r\n"); 4138 inject(cpd, x, NarrativeStatus.GENERATED); 4139 return true; 4140 } catch (Exception e) { 4141 e.printStackTrace(); 4142 return false; 4143 } 4144 } 4145 4146 public boolean generate(ResourceContext rcontext, CapabilityStatement conf) throws FHIRFormatError, DefinitionException, IOException { 4147 XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); 4148 x.h2().addText(conf.getName()); 4149 addMarkdown(x, conf.getDescription()); 4150 if (conf.getRest().size() > 0) { 4151 CapabilityStatementRestComponent rest = conf.getRest().get(0); 4152 XhtmlNode t = x.table(null); 4153 addTableRow(t, "Mode", rest.getMode().toString()); 4154 addTableRow(t, "Description", rest.getDocumentation()); 4155 4156 addTableRow(t, "Transaction", showOp(rest, SystemRestfulInteraction.TRANSACTION)); 4157 addTableRow(t, "System History", showOp(rest, SystemRestfulInteraction.HISTORYSYSTEM)); 4158 addTableRow(t, "System Search", showOp(rest, SystemRestfulInteraction.SEARCHSYSTEM)); 4159 4160 boolean hasVRead = false; 4161 boolean hasPatch = false; 4162 boolean hasDelete = false; 4163 boolean hasHistory = false; 4164 boolean hasUpdates = false; 4165 for (CapabilityStatementRestResourceComponent r : rest.getResource()) { 4166 hasVRead = hasVRead || hasOp(r, TypeRestfulInteraction.VREAD); 4167 hasPatch = hasPatch || hasOp(r, TypeRestfulInteraction.PATCH); 4168 hasDelete = hasDelete || hasOp(r, TypeRestfulInteraction.DELETE); 4169 hasHistory = hasHistory || hasOp(r, TypeRestfulInteraction.HISTORYTYPE); 4170 hasUpdates = hasUpdates || hasOp(r, TypeRestfulInteraction.HISTORYINSTANCE); 4171 } 4172 4173 t = x.table(null); 4174 XhtmlNode tr = t.tr(); 4175 tr.th().b().tx("Resource Type"); 4176 tr.th().b().tx("Profile"); 4177 tr.th().b().attribute("title", "GET a resource (read interaction)").tx("Read"); 4178 if (hasVRead) 4179 tr.th().b().attribute("title", "GET past versions of resources (vread interaction)").tx("V-Read"); 4180 tr.th().b().attribute("title", "GET all set of resources of the type (search interaction)").tx("Search"); 4181 tr.th().b().attribute("title", "PUT a new resource version (update interaction)").tx("Update"); 4182 if (hasPatch) 4183 tr.th().b().attribute("title", "PATCH a new resource version (patch interaction)").tx("Patch"); 4184 tr.th().b().attribute("title", "POST a new resource (create interaction)").tx("Create"); 4185 if (hasDelete) 4186 tr.th().b().attribute("title", "DELETE a resource (delete interaction)").tx("Delete"); 4187 if (hasUpdates) 4188 tr.th().b().attribute("title", "GET changes to a resource (history interaction on instance)").tx("Updates"); 4189 if (hasHistory) 4190 tr.th().b().attribute("title", "GET changes for all resources of the type (history interaction on type)").tx("History"); 4191 4192 for (CapabilityStatementRestResourceComponent r : rest.getResource()) { 4193 tr = t.tr(); 4194 tr.td().addText(r.getType()); 4195 if (r.hasProfile()) { 4196 tr.td().ah(prefix+r.getProfile()).addText(r.getProfile()); 4197 } 4198 tr.td().addText(showOp(r, TypeRestfulInteraction.READ)); 4199 if (hasVRead) 4200 tr.td().addText(showOp(r, TypeRestfulInteraction.VREAD)); 4201 tr.td().addText(showOp(r, TypeRestfulInteraction.SEARCHTYPE)); 4202 tr.td().addText(showOp(r, TypeRestfulInteraction.UPDATE)); 4203 if (hasPatch) 4204 tr.td().addText(showOp(r, TypeRestfulInteraction.PATCH)); 4205 tr.td().addText(showOp(r, TypeRestfulInteraction.CREATE)); 4206 if (hasDelete) 4207 tr.td().addText(showOp(r, TypeRestfulInteraction.DELETE)); 4208 if (hasUpdates) 4209 tr.td().addText(showOp(r, TypeRestfulInteraction.HISTORYINSTANCE)); 4210 if (hasHistory) 4211 tr.td().addText(showOp(r, TypeRestfulInteraction.HISTORYTYPE)); 4212 } 4213 } 4214 4215 inject(conf, x, NarrativeStatus.GENERATED); 4216 return true; 4217 } 4218 4219 private boolean hasOp(CapabilityStatementRestResourceComponent r, TypeRestfulInteraction on) { 4220 for (ResourceInteractionComponent op : r.getInteraction()) { 4221 if (op.getCode() == on) 4222 return true; 4223 } 4224 return false; 4225 } 4226 4227 private String showOp(CapabilityStatementRestResourceComponent r, TypeRestfulInteraction on) { 4228 for (ResourceInteractionComponent op : r.getInteraction()) { 4229 if (op.getCode() == on) 4230 return "y"; 4231 } 4232 return ""; 4233 } 4234 4235 private String showOp(CapabilityStatementRestComponent r, SystemRestfulInteraction on) { 4236 for (SystemInteractionComponent op : r.getInteraction()) { 4237 if (op.getCode() == on) 4238 return "y"; 4239 } 4240 return ""; 4241 } 4242 4243 private void addTableRow(XhtmlNode t, String name, String value) { 4244 XhtmlNode tr = t.tr(); 4245 tr.td().addText(name); 4246 tr.td().addText(value); 4247 } 4248 4249 public XhtmlNode generateDocumentNarrative(Bundle feed) { 4250 /* 4251 When the document is presented for human consumption, applications must present the collated narrative portions of the following resources in order: 4252 * The Composition resource 4253 * The Subject resource 4254 * Resources referenced in the section.content 4255 */ 4256 XhtmlNode root = new XhtmlNode(NodeType.Element, "div"); 4257 Composition comp = (Composition) feed.getEntry().get(0).getResource(); 4258 root.getChildNodes().add(comp.getText().getDiv()); 4259 Resource subject = ResourceUtilities.getById(feed, null, comp.getSubject().getReference()); 4260 if (subject != null && subject instanceof DomainResource) { 4261 root.hr(); 4262 root.getChildNodes().add(((DomainResource)subject).getText().getDiv()); 4263 } 4264 List<SectionComponent> sections = comp.getSection(); 4265 renderSections(feed, root, sections, 1); 4266 return root; 4267 } 4268 4269 private void renderSections(Bundle feed, XhtmlNode node, List<SectionComponent> sections, int level) { 4270 for (SectionComponent section : sections) { 4271 node.hr(); 4272 if (section.hasTitleElement()) 4273 node.addTag("h"+Integer.toString(level)).addText(section.getTitle()); 4274// else if (section.hasCode()) 4275// node.addTag("h"+Integer.toString(level)).addText(displayCodeableConcept(section.getCode())); 4276 4277// if (section.hasText()) { 4278// node.getChildNodes().add(section.getText().getDiv()); 4279// } 4280// 4281// if (!section.getSection().isEmpty()) { 4282// renderSections(feed, node.addTag("blockquote"), section.getSection(), level+1); 4283// } 4284 } 4285 } 4286 4287 4288 public class ObservationNode { 4289 private String ref; 4290 private ResourceWrapper obs; 4291 private List<ObservationNode> contained = new ArrayList<NarrativeGenerator.ObservationNode>(); 4292 } 4293 4294 public XhtmlNode generateDiagnosticReport(ResourceWrapper dr) { 4295 XhtmlNode root = new XhtmlNode(NodeType.Element, "div"); 4296 XhtmlNode h2 = root.h2(); 4297 displayCodeableConcept(h2, getProperty(dr, "code").value()); 4298 h2.tx(" "); 4299 PropertyWrapper pw = getProperty(dr, "category"); 4300 if (valued(pw)) { 4301 h2.tx("("); 4302 displayCodeableConcept(h2, pw.value()); 4303 h2.tx(") "); 4304 } 4305 displayDate(h2, getProperty(dr, "issued").value()); 4306 4307 XhtmlNode tbl = root.table( "grid"); 4308 XhtmlNode tr = tbl.tr(); 4309 XhtmlNode tdl = tr.td(); 4310 XhtmlNode tdr = tr.td(); 4311 populateSubjectSummary(tdl, getProperty(dr, "subject").value()); 4312 tdr.b().tx("Report Details"); 4313 tdr.br(); 4314 pw = getProperty(dr, "perfomer"); 4315 if (valued(pw)) { 4316 tdr.addText(pluralise("Performer", pw.getValues().size())+":"); 4317 for (BaseWrapper v : pw.getValues()) { 4318 tdr.tx(" "); 4319 displayReference(tdr, v); 4320 } 4321 tdr.br(); 4322 } 4323 pw = getProperty(dr, "identifier"); 4324 if (valued(pw)) { 4325 tdr.addText(pluralise("Identifier", pw.getValues().size())+":"); 4326 for (BaseWrapper v : pw.getValues()) { 4327 tdr.tx(" "); 4328 displayIdentifier(tdr, v); 4329 } 4330 tdr.br(); 4331 } 4332 pw = getProperty(dr, "request"); 4333 if (valued(pw)) { 4334 tdr.addText(pluralise("Request", pw.getValues().size())+":"); 4335 for (BaseWrapper v : pw.getValues()) { 4336 tdr.tx(" "); 4337 displayReferenceId(tdr, v); 4338 } 4339 tdr.br(); 4340 } 4341 4342 pw = getProperty(dr, "result"); 4343 if (valued(pw)) { 4344 List<ObservationNode> observations = fetchObservations(pw.getValues()); 4345 buildObservationsTable(root, observations); 4346 } 4347 4348 pw = getProperty(dr, "conclusion"); 4349 if (valued(pw)) 4350 displayText(root.para(), pw.value()); 4351 4352 pw = getProperty(dr, "result"); 4353 if (valued(pw)) { 4354 XhtmlNode p = root.para(); 4355 p.b().tx("Coded Diagnoses :"); 4356 for (BaseWrapper v : pw.getValues()) { 4357 tdr.tx(" "); 4358 displayCodeableConcept(tdr, v); 4359 } 4360 } 4361 return root; 4362 } 4363 4364 private void buildObservationsTable(XhtmlNode root, List<ObservationNode> observations) { 4365 XhtmlNode tbl = root.table( "none"); 4366 for (ObservationNode o : observations) { 4367 addObservationToTable(tbl, o, 0); 4368 } 4369 } 4370 4371 private void addObservationToTable(XhtmlNode tbl, ObservationNode o, int i) { 4372 XhtmlNode tr = tbl.tr(); 4373 if (o.obs == null) { 4374 XhtmlNode td = tr.td().colspan("6"); 4375 td.i().tx("This Observation could not be resolved"); 4376 } else { 4377 addObservationToTable(tr, o.obs, i); 4378 // todo: contained observations 4379 } 4380 for (ObservationNode c : o.contained) { 4381 addObservationToTable(tbl, c, i+1); 4382 } 4383 } 4384 4385 private void addObservationToTable(XhtmlNode tr, ResourceWrapper obs, int i) { 4386 // TODO Auto-generated method stub 4387 4388 // code (+bodysite) 4389 XhtmlNode td = tr.td(); 4390 PropertyWrapper pw = getProperty(obs, "result"); 4391 if (valued(pw)) { 4392 displayCodeableConcept(td, pw.value()); 4393 } 4394 pw = getProperty(obs, "bodySite"); 4395 if (valued(pw)) { 4396 td.tx(" ("); 4397 displayCodeableConcept(td, pw.value()); 4398 td.tx(")"); 4399 } 4400 4401 // value / dataAbsentReason (in red) 4402 td = tr.td(); 4403 pw = getProperty(obs, "value[x]"); 4404 if (valued(pw)) { 4405 if (pw.getTypeCode().equals("CodeableConcept")) 4406 displayCodeableConcept(td, pw.value()); 4407 else if (pw.getTypeCode().equals("string")) 4408 displayText(td, pw.value()); 4409 else 4410 td.addText(pw.getTypeCode()+" not rendered yet"); 4411 } 4412 4413 // units 4414 td = tr.td(); 4415 td.tx("to do"); 4416 4417 // reference range 4418 td = tr.td(); 4419 td.tx("to do"); 4420 4421 // flags (status other than F, interpretation, ) 4422 td = tr.td(); 4423 td.tx("to do"); 4424 4425 // issued if different to DR 4426 td = tr.td(); 4427 td.tx("to do"); 4428 } 4429 4430 private boolean valued(PropertyWrapper pw) { 4431 return pw != null && pw.hasValues(); 4432 } 4433 4434 private void displayText(XhtmlNode c, BaseWrapper v) { 4435 c.addText(v.toString()); 4436 } 4437 4438 private String pluralise(String name, int size) { 4439 return size == 1 ? name : name+"s"; 4440 } 4441 4442 private void displayIdentifier(XhtmlNode c, BaseWrapper v) { 4443 String hint = ""; 4444 PropertyWrapper pw = v.getChildByName("type"); 4445 if (valued(pw)) { 4446 hint = genCC(pw.value()); 4447 } else { 4448 pw = v.getChildByName("system"); 4449 if (valued(pw)) { 4450 hint = pw.value().toString(); 4451 } 4452 } 4453 displayText(c.span(null, hint), v.getChildByName("value").value()); 4454 } 4455 4456 private String genCoding(BaseWrapper value) { 4457 PropertyWrapper pw = value.getChildByName("display"); 4458 if (valued(pw)) 4459 return pw.value().toString(); 4460 pw = value.getChildByName("code"); 4461 if (valued(pw)) 4462 return pw.value().toString(); 4463 return ""; 4464 } 4465 4466 private String genCC(BaseWrapper value) { 4467 PropertyWrapper pw = value.getChildByName("text"); 4468 if (valued(pw)) 4469 return pw.value().toString(); 4470 pw = value.getChildByName("coding"); 4471 if (valued(pw)) 4472 return genCoding(pw.getValues().get(0)); 4473 return ""; 4474 } 4475 4476 private void displayReference(XhtmlNode c, BaseWrapper v) { 4477 c.tx("to do"); 4478 } 4479 4480 4481 private void displayDate(XhtmlNode c, BaseWrapper baseWrapper) { 4482 c.tx("to do"); 4483 } 4484 4485 private void displayCodeableConcept(XhtmlNode c, BaseWrapper property) { 4486 c.tx("to do"); 4487 } 4488 4489 private void displayReferenceId(XhtmlNode c, BaseWrapper v) { 4490 c.tx("to do"); 4491 } 4492 4493 private PropertyWrapper getProperty(ResourceWrapper res, String name) { 4494 for (PropertyWrapper t : res.children()) { 4495 if (t.getName().equals(name)) 4496 return t; 4497 } 4498 return null; 4499 } 4500 4501 private void populateSubjectSummary(XhtmlNode container, BaseWrapper subject) { 4502 ResourceWrapper r = fetchResource(subject); 4503 if (r == null) 4504 container.tx("Unable to get Patient Details"); 4505 else if (r.getName().equals("Patient")) 4506 generatePatientSummary(container, r); 4507 else 4508 container.tx("Not done yet"); 4509 } 4510 4511 private void generatePatientSummary(XhtmlNode c, ResourceWrapper r) { 4512 c.tx("to do"); 4513 } 4514 4515 private ResourceWrapper fetchResource(BaseWrapper subject) { 4516 if (resolver == null) 4517 return null; 4518 String url = subject.getChildByName("reference").value().toString(); 4519 ResourceWithReference rr = resolver.resolve(url); 4520 return rr == null ? null : rr.resource; 4521 } 4522 4523 private List<ObservationNode> fetchObservations(List<BaseWrapper> list) { 4524 return new ArrayList<NarrativeGenerator.ObservationNode>(); 4525 } 4526 4527 public XhtmlNode renderBundle(Bundle b) throws FHIRException { 4528 if (b.getType() == BundleType.DOCUMENT) { 4529 if (!b.hasEntry() || !(b.getEntryFirstRep().hasResource() && b.getEntryFirstRep().getResource() instanceof Composition)) 4530 throw new FHIRException("Invalid document - first entry is not a Composition"); 4531 Composition dr = (Composition) b.getEntryFirstRep().getResource(); 4532 return dr.getText().getDiv(); 4533 } else { 4534 XhtmlNode root = new XhtmlNode(NodeType.Element, "div"); 4535 root.para().addText("Bundle "+b.getId()+" of type "+b.getType().toCode()); 4536 int i = 0; 4537 for (BundleEntryComponent be : b.getEntry()) { 4538 i++; 4539 if (be.hasResource() && be.getResource().hasId()) 4540 root.an(be.getResource().getResourceType().name().toLowerCase() + "_" + be.getResource().getId()); 4541 root.hr(); 4542 root.para().addText("Entry "+Integer.toString(i)+(be.hasFullUrl() ? " - Full URL = " + be.getFullUrl() : "")); 4543 if (be.hasRequest()) 4544 renderRequest(root, be.getRequest()); 4545 if (be.hasSearch()) 4546 renderSearch(root, be.getSearch()); 4547 if (be.hasResponse()) 4548 renderResponse(root, be.getResponse()); 4549 if (be.hasResource()) { 4550 root.para().addText("Resource "+be.getResource().fhirType()+":"); 4551 if (be.hasResource() && be.getResource() instanceof DomainResource) { 4552 DomainResource dr = (DomainResource) be.getResource(); 4553 if ( dr.getText().hasDiv()) 4554 root.blockquote().getChildNodes().addAll(dr.getText().getDiv().getChildNodes()); 4555 } 4556 } 4557 } 4558 return root; 4559 } 4560 } 4561 4562 private void renderSearch(XhtmlNode root, BundleEntrySearchComponent search) { 4563 StringBuilder b = new StringBuilder(); 4564 b.append("Search: "); 4565 if (search.hasMode()) 4566 b.append("mode = "+search.getMode().toCode()); 4567 if (search.hasScore()) { 4568 if (search.hasMode()) 4569 b.append(","); 4570 b.append("score = "+search.getScore()); 4571 } 4572 root.para().addText(b.toString()); 4573 } 4574 4575 private void renderResponse(XhtmlNode root, BundleEntryResponseComponent response) { 4576 root.para().addText("Request:"); 4577 StringBuilder b = new StringBuilder(); 4578 b.append(response.getStatus()+"\r\n"); 4579 if (response.hasLocation()) 4580 b.append("Location: "+response.getLocation()+"\r\n"); 4581 if (response.hasEtag()) 4582 b.append("E-Tag: "+response.getEtag()+"\r\n"); 4583 if (response.hasLastModified()) 4584 b.append("LastModified: "+response.getEtag()+"\r\n"); 4585 root.pre().addText(b.toString()); 4586 } 4587 4588 private void renderRequest(XhtmlNode root, BundleEntryRequestComponent request) { 4589 root.para().addText("Response:"); 4590 StringBuilder b = new StringBuilder(); 4591 b.append(request.getMethod()+" "+request.getUrl()+"\r\n"); 4592 if (request.hasIfNoneMatch()) 4593 b.append("If-None-Match: "+request.getIfNoneMatch()+"\r\n"); 4594 if (request.hasIfModifiedSince()) 4595 b.append("If-Modified-Since: "+request.getIfModifiedSince()+"\r\n"); 4596 if (request.hasIfMatch()) 4597 b.append("If-Match: "+request.getIfMatch()+"\r\n"); 4598 if (request.hasIfNoneExist()) 4599 b.append("If-None-Exist: "+request.getIfNoneExist()+"\r\n"); 4600 root.pre().addText(b.toString()); 4601 } 4602 4603 public XhtmlNode renderBundle(org.hl7.fhir.r4.elementmodel.Element element) throws FHIRException { 4604 XhtmlNode root = new XhtmlNode(NodeType.Element, "div"); 4605 for (Base b : element.listChildrenByName("entry")) { 4606 org.hl7.fhir.r4.elementmodel.Element r = ((org.hl7.fhir.r4.elementmodel.Element) b).getNamedChild("resource"); 4607 if (r!=null) { 4608 XhtmlNode c = getHtmlForResource(r); 4609 if (c != null) 4610 root.getChildNodes().addAll(c.getChildNodes()); 4611 root.hr(); 4612 } 4613 } 4614 return root; 4615 } 4616 4617 private XhtmlNode getHtmlForResource(org.hl7.fhir.r4.elementmodel.Element element) { 4618 org.hl7.fhir.r4.elementmodel.Element text = element.getNamedChild("text"); 4619 if (text == null) 4620 return null; 4621 org.hl7.fhir.r4.elementmodel.Element div = text.getNamedChild("div"); 4622 if (div == null) 4623 return null; 4624 else 4625 return div.getXhtml(); 4626 } 4627 4628 public String getDefinitionsTarget() { 4629 return definitionsTarget; 4630 } 4631 4632 public void setDefinitionsTarget(String definitionsTarget) { 4633 this.definitionsTarget = definitionsTarget; 4634 } 4635 4636 public String getCorePath() { 4637 return corePath; 4638 } 4639 4640 public void setCorePath(String corePath) { 4641 this.corePath = corePath; 4642 } 4643 4644 public String getDestDir() { 4645 return destDir; 4646 } 4647 4648 public void setDestDir(String destDir) { 4649 this.destDir = destDir; 4650 } 4651 4652 public ProfileKnowledgeProvider getPkp() { 4653 return pkp; 4654 } 4655 4656 public NarrativeGenerator setPkp(ProfileKnowledgeProvider pkp) { 4657 this.pkp = pkp; 4658 return this; 4659 } 4660 4661 public boolean isPretty() { 4662 return pretty; 4663 } 4664 4665 public NarrativeGenerator setPretty(boolean pretty) { 4666 this.pretty = pretty; 4667 return this; 4668 } 4669 4670 public boolean isCanonicalUrlsAsLinks() { 4671 return canonicalUrlsAsLinks; 4672 } 4673 4674 @Override 4675 public void setCanonicalUrlsAsLinks(boolean canonicalUrlsAsLinks) { 4676 this.canonicalUrlsAsLinks = canonicalUrlsAsLinks; 4677 } 4678 4679 public String getSnomedEdition() { 4680 return snomedEdition; 4681 } 4682 4683 public NarrativeGenerator setSnomedEdition(String snomedEdition) { 4684 this.snomedEdition = snomedEdition; 4685 return this; 4686 } 4687 4688 public TerminologyServiceOptions getTerminologyServiceOptions() { 4689 return terminologyServiceOptions; 4690 } 4691 4692 public void setTerminologyServiceOptions(TerminologyServiceOptions terminologyServiceOptions) { 4693 this.terminologyServiceOptions = terminologyServiceOptions; 4694 } 4695 4696 4697}