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