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    case NULL: return null;
3519            default: return "?";
3520          }
3521  }
3522
3523  private List<TargetElementComponentWrapper> findMappingsForCode(String code, ConceptMap map) {
3524    List<TargetElementComponentWrapper> mappings = new ArrayList<TargetElementComponentWrapper>();
3525
3526    for (ConceptMapGroupComponent g : map.getGroup()) {
3527      for (SourceElementComponent c : g.getElement()) {
3528                if (c.getCode().equals(code))
3529          for (TargetElementComponent cc : c.getTarget())
3530            mappings.add(new TargetElementComponentWrapper(g, cc));
3531      }
3532          }
3533          return mappings;
3534  }
3535
3536  private boolean generateComposition(ResourceContext rcontext, XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRException, IOException {
3537          boolean hasExtensions = false;
3538    List<String> langs = new ArrayList<String>();
3539
3540    if (header) {
3541      XhtmlNode h = x.h2();
3542      h.addText(vs.present());
3543      addMarkdown(x, vs.getDescription());
3544      if (vs.hasCopyrightElement())
3545        generateCopyright(x, vs);
3546    }
3547    XhtmlNode p = x.para();
3548    p.tx("This value set includes codes from the following code systems:");
3549
3550    XhtmlNode ul = x.ul();
3551    XhtmlNode li;
3552    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
3553      hasExtensions = genInclude(rcontext, ul, inc, "Include", langs, maps) || hasExtensions;
3554    }
3555    for (ConceptSetComponent exc : vs.getCompose().getExclude()) {
3556      hasExtensions = genInclude(rcontext, ul, exc, "Exclude", langs, maps) || hasExtensions;
3557    }
3558
3559    // now, build observed languages
3560
3561    if (langs.size() > 0) {
3562      Collections.sort(langs);
3563      x.para().b().tx("Additional Language Displays");
3564      XhtmlNode t = x.table( "codes");
3565      XhtmlNode tr = t.tr();
3566      tr.td().b().tx("Code");
3567      for (String lang : langs)
3568        tr.td().b().addText(describeLang(lang));
3569      for (ConceptSetComponent c : vs.getCompose().getInclude()) {
3570        for (ConceptReferenceComponent cc : c.getConcept()) {
3571          addLanguageRow(cc, t, langs);
3572        }
3573      }
3574    }
3575
3576    return hasExtensions;
3577  }
3578
3579    private void addLanguageRow(ConceptReferenceComponent c, XhtmlNode t, List<String> langs) {
3580      XhtmlNode tr = t.tr();
3581      tr.td().addText(c.getCode());
3582      for (String lang : langs) {
3583        String d = null;
3584        for (ConceptReferenceDesignationComponent cd : c.getDesignation()) {
3585          String l = cd.getLanguage();
3586          if (lang.equals(l))
3587            d = cd.getValue();
3588        }
3589        tr.td().addText(d == null ? "" : d);
3590      }
3591    }
3592
3593  private void AddVsRef(ResourceContext rcontext, String value, XhtmlNode li) {
3594    Resource res = rcontext == null ? null : rcontext.resolve(value); 
3595    if (res != null && !(res instanceof MetadataResource)) {
3596      li.addText(value);
3597      return;      
3598    }      
3599    MetadataResource vs = (MetadataResource) res;
3600    if (vs == null)
3601                vs = context.fetchResource(ValueSet.class, value);
3602    if (vs == null)
3603                vs = context.fetchResource(StructureDefinition.class, value);
3604//    if (vs == null)
3605        //      vs = context.fetchResource(DataElement.class, value);
3606    if (vs == null)
3607                vs = context.fetchResource(Questionnaire.class, value);
3608    if (vs != null) {
3609      String ref = (String) vs.getUserData("path");
3610      
3611      ref = adjustForPath(ref);
3612      XhtmlNode a = li.ah(ref == null ? "??" : ref.replace("\\", "/"));
3613      a.addText(value);
3614    } else {
3615        CodeSystem cs = context.fetchCodeSystem(value);
3616        if (cs != null) {
3617        String ref = (String) cs.getUserData("path");
3618        ref = adjustForPath(ref);
3619        XhtmlNode a = li.ah(ref == null ? "??" : ref.replace("\\", "/"));
3620        a.addText(value);
3621            } else if (value.equals("http://snomed.info/sct") || value.equals("http://snomed.info/id")) {
3622              XhtmlNode a = li.ah(value);
3623              a.tx("SNOMED-CT");
3624            }
3625            else {
3626              if (value.startsWith("http://hl7.org") && !Utilities.existsInList(value, "http://hl7.org/fhir/sid/icd-10-us"))
3627                System.out.println("Unable to resolve value set "+value);
3628              li.addText(value);
3629    }
3630  }
3631        }
3632
3633  private String adjustForPath(String ref) {
3634    if (prefix == null)
3635      return ref;
3636    else
3637      return prefix+ref;
3638  }
3639
3640  private boolean genInclude(ResourceContext rcontext, XhtmlNode ul, ConceptSetComponent inc, String type, List<String> langs, List<UsedConceptMap> maps) throws FHIRException, IOException {
3641    boolean hasExtensions = false;
3642    XhtmlNode li;
3643    li = ul.li();
3644    CodeSystem e = context.fetchCodeSystem(inc.getSystem());
3645
3646    if (inc.hasSystem()) {
3647      if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) {
3648        li.addText(type+" all codes defined in ");
3649        addCsRef(inc, li, e);
3650      } else {
3651        if (inc.getConcept().size() > 0) {
3652          li.addText(type+" these codes as defined in ");
3653          addCsRef(inc, li, e);
3654
3655          XhtmlNode t = li.table("none");
3656          boolean hasComments = false;
3657          boolean hasDefinition = false;
3658          for (ConceptReferenceComponent c : inc.getConcept()) {
3659            hasComments = hasComments || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT);
3660            hasDefinition = hasDefinition || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION);
3661          }
3662          if (hasComments || hasDefinition)
3663            hasExtensions = true;
3664          addMapHeaders(addTableHeaderRowStandard(t, false, true, hasDefinition, hasComments, false, false, null), maps);
3665          for (ConceptReferenceComponent c : inc.getConcept()) {
3666            XhtmlNode tr = t.tr();
3667            XhtmlNode td = tr.td();
3668            ConceptDefinitionComponent cc = getConceptForCode(e, c.getCode(), inc);
3669            addCodeToTable(false, inc.getSystem(), c.getCode(), c.hasDisplay()? c.getDisplay() : cc != null ? cc.getDisplay() : "", td);
3670
3671            td = tr.td();
3672            if (!Utilities.noString(c.getDisplay()))
3673              td.addText(c.getDisplay());
3674            else if (cc != null && !Utilities.noString(cc.getDisplay()))
3675              td.addText(cc.getDisplay());
3676
3677            td = tr.td();
3678            if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION))
3679              smartAddText(td, ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_DEFINITION));
3680            else if (cc != null && !Utilities.noString(cc.getDefinition()))
3681              smartAddText(td, cc.getDefinition());
3682
3683            if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT)) {
3684              smartAddText(tr.td(), "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_VS_COMMENT));
3685            }
3686            for (ConceptReferenceDesignationComponent cd : c.getDesignation()) {
3687              if (cd.hasLanguage() && !langs.contains(cd.getLanguage()))
3688                langs.add(cd.getLanguage());
3689            }
3690          }
3691        }
3692        boolean first = true;
3693        for (ConceptSetFilterComponent f : inc.getFilter()) {
3694          if (first) {
3695            li.addText(type+" codes from ");
3696            first = false;
3697          } else
3698            li.tx(" and ");
3699          addCsRef(inc, li, e);
3700          li.tx(" where "+f.getProperty()+" "+describe(f.getOp())+" ");
3701          if (e != null && codeExistsInValueSet(e, f.getValue())) {
3702            String href = prefix+getCsRef(e);
3703            if (href.contains("#"))
3704              href = href + "-"+Utilities.nmtokenize(f.getValue());
3705            else
3706              href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(f.getValue());
3707            li.ah(href).addText(f.getValue());
3708          } else if ("concept".equals(f.getProperty()) && inc.hasSystem()) {
3709            li.addText(f.getValue());
3710            ValidationResult vr = context.validateCode(terminologyServiceOptions, inc.getSystem(), f.getValue(), null);
3711            if (vr.isOk()) {
3712              li.tx(" ("+vr.getDisplay()+")");
3713            }
3714          }
3715          else
3716            li.addText(f.getValue());
3717          String disp = ToolingExtensions.getDisplayHint(f);
3718          if (disp != null)
3719            li.tx(" ("+disp+")");
3720        }
3721      }
3722      if (inc.hasValueSet()) {
3723        li.tx(", where the codes are contained in ");
3724        boolean first = true;
3725        for (UriType vs : inc.getValueSet()) {
3726          if (first)
3727            first = false;
3728          else
3729            li.tx(", ");
3730          AddVsRef(rcontext, vs.asStringValue(), li);
3731        }
3732      }
3733    } else {
3734      li.tx("Import all the codes that are contained in ");
3735      boolean first = true;
3736      for (UriType vs : inc.getValueSet()) {
3737        if (first)
3738          first = false;
3739        else
3740          li.tx(", ");
3741        AddVsRef(rcontext, vs.asStringValue(), li);
3742      }
3743    }
3744    return hasExtensions;
3745  }
3746
3747  private String describe(FilterOperator op) {
3748    switch (op) {
3749    case EQUAL: return " = ";
3750    case ISA: return " is-a ";
3751    case ISNOTA: return " is-not-a ";
3752    case REGEX: return " matches (by regex) ";
3753                case NULL: return " ?? ";
3754                case IN: return " in ";
3755                case NOTIN: return " not in ";
3756    case DESCENDENTOF: return " descends from ";
3757    case EXISTS: return " exists ";
3758    case GENERALIZES: return " generalizes ";
3759    }
3760    return null;
3761  }
3762
3763  private ConceptDefinitionComponent getConceptForCode(CodeSystem e, String code, ConceptSetComponent inc) {
3764    // first, look in the code systems
3765    if (e == null)
3766    e = context.fetchCodeSystem(inc.getSystem());
3767    if (e != null) {
3768      ConceptDefinitionComponent v = getConceptForCode(e.getConcept(), code);
3769      if (v != null)
3770        return v;
3771    }
3772
3773    if (!context.hasCache()) {
3774      ValueSetExpansionComponent vse;
3775      try {
3776        ValueSetExpansionOutcome vso = context.expandVS(inc, false);   
3777        ValueSet valueset = vso.getValueset();
3778        if (valueset == null)
3779          throw new TerminologyServiceException("Error Expanding ValueSet: "+vso.getError());
3780        vse = valueset.getExpansion();        
3781
3782      } catch (TerminologyServiceException e1) {
3783        return null;
3784      }
3785      if (vse != null) {
3786        ConceptDefinitionComponent v = getConceptForCodeFromExpansion(vse.getContains(), code);
3787      if (v != null)
3788        return v;
3789    }
3790    }
3791
3792    return context.validateCode(terminologyServiceOptions, inc.getSystem(), code, null).asConceptDefinition();
3793  }
3794
3795
3796
3797  private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> list, String code) {
3798    for (ConceptDefinitionComponent c : list) {
3799    if (code.equals(c.getCode()))
3800      return c;
3801      ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code);
3802      if (v != null)
3803        return v;
3804    }
3805    return null;
3806  }
3807
3808  private ConceptDefinitionComponent getConceptForCodeFromExpansion(List<ValueSetExpansionContainsComponent> list, String code) {
3809    for (ValueSetExpansionContainsComponent c : list) {
3810      if (code.equals(c.getCode())) {
3811        ConceptDefinitionComponent res = new ConceptDefinitionComponent();
3812        res.setCode(c.getCode());
3813        res.setDisplay(c.getDisplay());
3814        return res;
3815      }
3816      ConceptDefinitionComponent v = getConceptForCodeFromExpansion(c.getContains(), code);
3817      if (v != null)
3818        return v;
3819    }
3820    return null;
3821  }
3822
3823  private void addRefToCode(XhtmlNode td, String target, String vslink, String code) {
3824    CodeSystem cs = context.fetchCodeSystem(target);
3825    String cslink = getCsRef(cs);
3826    XhtmlNode a = null;
3827    if (cslink != null) 
3828      a = td.ah(prefix+cslink+"#"+cs.getId()+"-"+code);
3829    else
3830      a = td.ah(prefix+vslink+"#"+code);
3831    a.addText(code);
3832  }
3833
3834  private  <T extends Resource> void addCsRef(ConceptSetComponent inc, XhtmlNode li, T cs) {
3835    String ref = null;
3836    boolean addHtml = true;
3837    if (cs != null) {
3838      ref = (String) cs.getUserData("external.url");
3839      if (Utilities.noString(ref))
3840        ref = (String) cs.getUserData("filename");
3841      else
3842        addHtml = false;
3843      if (Utilities.noString(ref))
3844        ref = (String) cs.getUserData("path");
3845    }
3846    String spec = getSpecialReference(inc.getSystem());
3847    if (spec != null) {
3848      XhtmlNode a = li.ah(spec);
3849      a.code(inc.getSystem());
3850    } else if (cs != null && ref != null) {
3851      if (!Utilities.noString(prefix) && ref.startsWith("http://hl7.org/fhir/"))
3852        ref = ref.substring(20)+"/index.html";
3853      else if (addHtml && !ref.contains(".html"))
3854        ref = ref + ".html";
3855      XhtmlNode a = li.ah(prefix+ref.replace("\\", "/"));
3856      a.code(inc.getSystem());
3857    } else {
3858      li.code(inc.getSystem());
3859    }
3860  }
3861
3862  private String getSpecialReference(String system) {
3863    if ("http://snomed.info/sct".equals(system))
3864      return "http://www.snomed.org/";
3865    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", 
3866         "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", 
3867         "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", 
3868         "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/")) 
3869      return system;
3870      
3871    return null;
3872  }
3873
3874  private String getCsRef(String system) {
3875    CodeSystem cs = context.fetchCodeSystem(system);
3876    return getCsRef(cs);
3877  }
3878
3879  private  <T extends Resource> String getCsRef(T cs) {
3880    String ref = (String) cs.getUserData("filename");
3881    if (ref == null)
3882      ref = (String) cs.getUserData("path");
3883    if (ref == null)
3884      return "??.html";
3885    if (!ref.contains(".html"))
3886      ref = ref + ".html";
3887    return ref.replace("\\", "/");
3888  }
3889
3890  private boolean codeExistsInValueSet(CodeSystem cs, String code) {
3891    for (ConceptDefinitionComponent c : cs.getConcept()) {
3892      if (inConcept(code, c))
3893        return true;
3894    }
3895    return false;
3896  }
3897
3898  private boolean inConcept(String code, ConceptDefinitionComponent c) {
3899    if (c.hasCodeElement() && c.getCode().equals(code))
3900      return true;
3901    for (ConceptDefinitionComponent g : c.getConcept()) {
3902      if (inConcept(code, g))
3903        return true;
3904    }
3905    return false;
3906  }
3907
3908  /**
3909   * This generate is optimised for the build tool in that it tracks the source extension.
3910   * But it can be used for any other use.
3911   *
3912   * @param vs
3913   * @param codeSystems
3914   * @throws DefinitionException
3915   * @throws Exception
3916   */
3917  public boolean generate(ResourceContext rcontext, OperationOutcome op) throws DefinitionException {
3918    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
3919    boolean hasSource = false;
3920    boolean success = true;
3921    for (OperationOutcomeIssueComponent i : op.getIssue()) {
3922        success = success && i.getSeverity() == IssueSeverity.INFORMATION;
3923        hasSource = hasSource || ExtensionHelper.hasExtension(i, ToolingExtensions.EXT_ISSUE_SOURCE);
3924    }
3925    if (success)
3926        x.para().tx("All OK");
3927    if (op.getIssue().size() > 0) {
3928                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
3929                XhtmlNode tr = tbl.tr();
3930                tr.td().b().tx("Severity");
3931                tr.td().b().tx("Location");
3932        tr.td().b().tx("Code");
3933        tr.td().b().tx("Details");
3934        tr.td().b().tx("Diagnostics");
3935                if (hasSource)
3936                        tr.td().b().tx("Source");
3937                for (OperationOutcomeIssueComponent i : op.getIssue()) {
3938                        tr = tbl.tr();
3939                        tr.td().addText(i.getSeverity().toString());
3940                        XhtmlNode td = tr.td();
3941                        boolean d = false;
3942                        for (StringType s : i.getLocation()) {
3943                                if (d)
3944                                        td.tx(", ");
3945                                else
3946                                        d = true;
3947                                td.addText(s.getValue());
3948                        }
3949          tr.td().addText(i.getCode().getDisplay());
3950          tr.td().addText(gen(i.getDetails()));
3951          smartAddText(tr.td(), i.getDiagnostics());
3952                        if (hasSource) {
3953                                Extension ext = ExtensionHelper.getExtension(i, ToolingExtensions.EXT_ISSUE_SOURCE);
3954            tr.td().addText(ext == null ? "" : gen(ext));
3955                        }
3956                }
3957        }
3958    inject(op, x, hasSource ? NarrativeStatus.EXTENSIONS :  NarrativeStatus.GENERATED);
3959    return true;
3960  }
3961
3962
3963  public String genType(Type type) throws DefinitionException {
3964    if (type instanceof Coding)
3965      return gen((Coding) type);
3966    if (type instanceof CodeableConcept)
3967      return displayCodeableConcept((CodeableConcept) type);
3968    if (type instanceof Quantity)
3969      return displayQuantity((Quantity) type);
3970    if (type instanceof Range)
3971      return displayRange((Range) type);
3972    return null;
3973  }
3974        private String gen(Extension extension) throws DefinitionException {
3975                if (extension.getValue() instanceof CodeType)
3976                        return ((CodeType) extension.getValue()).getValue();
3977                if (extension.getValue() instanceof Coding)
3978                        return gen((Coding) extension.getValue());
3979
3980          throw new DefinitionException("Unhandled type "+extension.getValue().getClass().getName());
3981  }
3982
3983        public String gen(CodeableConcept code) {
3984                if (code == null)
3985                return null;
3986                if (code.hasText())
3987                        return code.getText();
3988                if (code.hasCoding())
3989                        return gen(code.getCoding().get(0));
3990                return null;
3991        }
3992
3993        public String gen(Coding code) {
3994          if (code == null)
3995                return null;
3996          if (code.hasDisplayElement())
3997                return code.getDisplay();
3998          if (code.hasCodeElement())
3999                return code.getCode();
4000          return null;
4001  }
4002
4003  public boolean generate(ResourceContext rcontext, StructureDefinition sd, Set<String> outputTracker) throws EOperationOutcome, FHIRException, IOException {
4004    ProfileUtilities pu = new ProfileUtilities(context, null, pkp);
4005    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
4006    x.getChildNodes().add(pu.generateTable(definitionsTarget, sd, true, destDir, false, sd.getId(), false, corePath, "", false, false, outputTracker));
4007    inject(sd, x, NarrativeStatus.GENERATED);
4008    return true;
4009  }
4010  public boolean generate(ResourceContext rcontext, ImplementationGuide ig) throws EOperationOutcome, FHIRException, IOException {
4011    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
4012    x.h2().addText(ig.getName());
4013    x.para().tx("The official URL for this implementation guide is: ");
4014    x.pre().tx(ig.getUrl());
4015    addMarkdown(x, ig.getDescription());
4016    inject(ig, x, NarrativeStatus.GENERATED);
4017    return true;
4018  }
4019        public boolean generate(ResourceContext rcontext, OperationDefinition opd) throws EOperationOutcome, FHIRException, IOException {
4020    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
4021    x.h2().addText(opd.getName());
4022    x.para().addText(Utilities.capitalize(opd.getKind().toString())+": "+opd.getName());
4023    x.para().tx("The official URL for this operation definition is: ");
4024    x.pre().tx(opd.getUrl());
4025    addMarkdown(x, opd.getDescription());
4026
4027    if (opd.getSystem())
4028      x.para().tx("URL: [base]/$"+opd.getCode());
4029    for (CodeType c : opd.getResource()) {
4030      if (opd.getType())
4031        x.para().tx("URL: [base]/"+c.getValue()+"/$"+opd.getCode());
4032      if (opd.getInstance())
4033        x.para().tx("URL: [base]/"+c.getValue()+"/[id]/$"+opd.getCode());
4034    }
4035
4036    x.para().tx("Parameters");
4037    XhtmlNode tbl = x.table( "grid");
4038    XhtmlNode tr = tbl.tr();
4039    tr.td().b().tx("Use");
4040    tr.td().b().tx("Name");
4041    tr.td().b().tx("Cardinality");
4042    tr.td().b().tx("Type");
4043    tr.td().b().tx("Binding");
4044    tr.td().b().tx("Documentation");
4045    for (OperationDefinitionParameterComponent p : opd.getParameter()) {
4046      genOpParam(rcontext, tbl, "", p);
4047    }
4048    addMarkdown(x, opd.getComment());
4049    inject(opd, x, NarrativeStatus.GENERATED);
4050    return true;
4051        }
4052
4053        private void genOpParam(ResourceContext rcontext, XhtmlNode tbl, String path, OperationDefinitionParameterComponent p) throws EOperationOutcome, FHIRException, IOException {
4054                XhtmlNode tr;
4055      tr = tbl.tr();
4056      tr.td().addText(p.getUse().toString());
4057      tr.td().addText(path+p.getName());
4058      tr.td().addText(Integer.toString(p.getMin())+".."+p.getMax());
4059      XhtmlNode td = tr.td();
4060      StructureDefinition sd = context.fetchTypeDefinition(p.getType());
4061      if (sd == null)
4062        td.tx(p.hasType() ? p.getType() : "");
4063        else if (sd.getAbstract() && p.hasExtension(ToolingExtensions.EXT_ALLOWED_TYPE)) {
4064          boolean first = true;
4065          for (Extension ex : p.getExtensionsByUrl(ToolingExtensions.EXT_ALLOWED_TYPE)) {
4066            if (first) first = false; else td.tx(" | ");
4067            String s = ex.getValue().primitiveValue();
4068            StructureDefinition sdt = context.fetchTypeDefinition(s);
4069            if (sdt == null)
4070              td.tx(p.hasType() ? p.getType() : "");
4071            else
4072              td.ah(sdt.getUserString("path")).tx(s);         
4073          }
4074        } else
4075          td.ah(sd.getUserString("path")).tx(p.hasType() ? p.getType() : "");
4076      if (p.hasSearchType()) {
4077        td.br();
4078        td.tx("(");
4079        td.ah( corePath == null ? "search.html#"+p.getSearchType().toCode() : Utilities.pathURL(corePath, "search.html#"+p.getSearchType().toCode())).tx(p.getSearchType().toCode());       
4080        td.tx(")");
4081      }
4082      td = tr.td();
4083      if (p.hasBinding() && p.getBinding().hasValueSet()) {
4084        AddVsRef(rcontext, p.getBinding().getValueSet(), td);
4085        td.tx(" ("+p.getBinding().getStrength().getDisplay()+")");
4086      }
4087      addMarkdown(tr.td(), p.getDocumentation());
4088      if (!p.hasType()) {
4089                        for (OperationDefinitionParameterComponent pp : p.getPart()) {
4090                                genOpParam(rcontext, tbl, path+p.getName()+".", pp);
4091        }
4092      }
4093    }
4094
4095        private void addMarkdown(XhtmlNode x, String text) throws FHIRFormatError, IOException, DefinitionException {
4096          if (text != null) {
4097            // 1. custom FHIR extensions
4098            while (text.contains("[[[")) {
4099              String left = text.substring(0, text.indexOf("[[["));
4100              String link = text.substring(text.indexOf("[[[")+3, text.indexOf("]]]"));
4101              String right = text.substring(text.indexOf("]]]")+3);
4102              String url = link;
4103              String[] parts = link.split("\\#");
4104              StructureDefinition p = context.fetchResource(StructureDefinition.class, parts[0]);
4105              if (p == null)
4106                p = context.fetchTypeDefinition(parts[0]);
4107              if (p == null)
4108                p = context.fetchResource(StructureDefinition.class, link);
4109              if (p != null) {
4110                url = p.getUserString("path");
4111                if (url == null)
4112                  url = p.getUserString("filename");
4113              } else
4114                throw new DefinitionException("Unable to resolve markdown link "+link);
4115
4116              text = left+"["+link+"]("+url+")"+right;
4117            }
4118
4119            // 2. markdown
4120            String s = markdown.process(Utilities.escapeXml(text), "narrative generator");
4121            XhtmlParser p = new XhtmlParser();
4122            XhtmlNode m;
4123                try {
4124                        m = p.parse("<div>"+s+"</div>", "div");
4125                } catch (org.hl7.fhir.exceptions.FHIRFormatError e) {
4126                        throw new FHIRFormatError(e.getMessage(), e);
4127                }
4128            x.getChildNodes().addAll(m.getChildNodes());
4129          }
4130  }
4131
4132  public boolean generate(ResourceContext rcontext, CompartmentDefinition cpd) {
4133    StringBuilder in = new StringBuilder();
4134    StringBuilder out = new StringBuilder();
4135    for (CompartmentDefinitionResourceComponent cc: cpd.getResource()) {
4136      CommaSeparatedStringBuilder rules = new CommaSeparatedStringBuilder();
4137      if (!cc.hasParam()) {
4138        out.append(" <li><a href=\"").append(cc.getCode().toLowerCase()).append(".html\">").append(cc.getCode()).append("</a></li>\r\n");
4139      } else if (!rules.equals("{def}")) {
4140        for (StringType p : cc.getParam())
4141          rules.append(p.asStringValue());
4142        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");
4143      }
4144    }
4145    XhtmlNode x;
4146    try {
4147      x = new XhtmlParser().parseFragment("<div><p>\r\nThe following resources may be in this compartment:\r\n</p>\r\n" +
4148          "<table class=\"grid\">\r\n"+
4149          " <tr><td><b>Resource</b></td><td><b>Inclusion Criteria</b></td></tr>\r\n"+
4150          in.toString()+
4151          "</table>\r\n"+
4152          "<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" +
4153          "<p>\r\n\r\n</p>\r\n" +
4154          "<p>\r\nThe following resources are never in this compartment:\r\n</p>\r\n" +
4155          "<ul>\r\n"+
4156          out.toString()+
4157          "</ul></div>\r\n");
4158      inject(cpd, x, NarrativeStatus.GENERATED);
4159      return true;
4160    } catch (Exception e) {
4161      e.printStackTrace();
4162      return false;
4163    }
4164  }
4165
4166  public boolean generate(ResourceContext rcontext, CapabilityStatement conf) throws FHIRFormatError, DefinitionException, IOException {
4167    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
4168    x.h2().addText(conf.getName());
4169    addMarkdown(x, conf.getDescription());
4170    if (conf.getRest().size() > 0) {
4171      CapabilityStatementRestComponent rest = conf.getRest().get(0);
4172      XhtmlNode t = x.table(null);
4173      addTableRow(t, "Mode", rest.getMode().toString());
4174      addTableRow(t, "Description", rest.getDocumentation());
4175
4176      addTableRow(t, "Transaction", showOp(rest, SystemRestfulInteraction.TRANSACTION));
4177      addTableRow(t, "System History", showOp(rest, SystemRestfulInteraction.HISTORYSYSTEM));
4178      addTableRow(t, "System Search", showOp(rest, SystemRestfulInteraction.SEARCHSYSTEM));
4179
4180      boolean hasVRead = false;
4181      boolean hasPatch = false;
4182      boolean hasDelete = false;
4183      boolean hasHistory = false;
4184      boolean hasUpdates = false;
4185      for (CapabilityStatementRestResourceComponent r : rest.getResource()) {
4186        hasVRead = hasVRead || hasOp(r, TypeRestfulInteraction.VREAD);
4187        hasPatch = hasPatch || hasOp(r, TypeRestfulInteraction.PATCH);
4188        hasDelete = hasDelete || hasOp(r, TypeRestfulInteraction.DELETE);
4189        hasHistory = hasHistory || hasOp(r, TypeRestfulInteraction.HISTORYTYPE);
4190        hasUpdates = hasUpdates || hasOp(r, TypeRestfulInteraction.HISTORYINSTANCE);
4191      }
4192      
4193      t = x.table(null);
4194      XhtmlNode tr = t.tr();
4195      tr.th().b().tx("Resource Type");
4196      tr.th().b().tx("Profile");
4197      tr.th().b().attribute("title", "GET a resource (read interaction)").tx("Read");
4198      if (hasVRead)
4199        tr.th().b().attribute("title", "GET past versions of resources (vread interaction)").tx("V-Read");
4200      tr.th().b().attribute("title", "GET all set of resources of the type (search interaction)").tx("Search");
4201      tr.th().b().attribute("title", "PUT a new resource version (update interaction)").tx("Update");
4202      if (hasPatch)
4203        tr.th().b().attribute("title", "PATCH a new resource version (patch interaction)").tx("Patch");
4204      tr.th().b().attribute("title", "POST a new resource (create interaction)").tx("Create");
4205      if (hasDelete)
4206        tr.th().b().attribute("title", "DELETE a resource (delete interaction)").tx("Delete");
4207      if (hasUpdates)
4208        tr.th().b().attribute("title", "GET changes to a resource (history interaction on instance)").tx("Updates");
4209      if (hasHistory)
4210        tr.th().b().attribute("title", "GET changes for all resources of the type (history interaction on type)").tx("History");
4211
4212      for (CapabilityStatementRestResourceComponent r : rest.getResource()) {
4213        tr = t.tr();
4214        tr.td().addText(r.getType());
4215        if (r.hasProfile()) {
4216          tr.td().ah(prefix+r.getProfile()).addText(r.getProfile());
4217        }
4218        tr.td().addText(showOp(r, TypeRestfulInteraction.READ));
4219        if (hasVRead)
4220        tr.td().addText(showOp(r, TypeRestfulInteraction.VREAD));
4221        tr.td().addText(showOp(r, TypeRestfulInteraction.SEARCHTYPE));
4222        tr.td().addText(showOp(r, TypeRestfulInteraction.UPDATE));
4223        if (hasPatch)
4224          tr.td().addText(showOp(r, TypeRestfulInteraction.PATCH));
4225        tr.td().addText(showOp(r, TypeRestfulInteraction.CREATE));
4226        if (hasDelete)
4227        tr.td().addText(showOp(r, TypeRestfulInteraction.DELETE));
4228        if (hasUpdates)
4229          tr.td().addText(showOp(r, TypeRestfulInteraction.HISTORYINSTANCE));
4230        if (hasHistory)
4231        tr.td().addText(showOp(r, TypeRestfulInteraction.HISTORYTYPE));
4232      }
4233    }
4234
4235    inject(conf, x, NarrativeStatus.GENERATED);
4236    return true;
4237  }
4238
4239  private boolean hasOp(CapabilityStatementRestResourceComponent r, TypeRestfulInteraction on) {
4240    for (ResourceInteractionComponent op : r.getInteraction()) {
4241      if (op.getCode() == on)
4242        return true;
4243    }
4244    return false;
4245  }
4246  
4247  private String showOp(CapabilityStatementRestResourceComponent r, TypeRestfulInteraction on) {
4248    for (ResourceInteractionComponent op : r.getInteraction()) {
4249      if (op.getCode() == on)
4250        return "y";
4251    }
4252    return "";
4253  }
4254
4255  private String showOp(CapabilityStatementRestComponent r, SystemRestfulInteraction on) {
4256    for (SystemInteractionComponent op : r.getInteraction()) {
4257      if (op.getCode() == on)
4258        return "y";
4259    }
4260    return "";
4261  }
4262
4263  private void addTableRow(XhtmlNode t, String name, String value) {
4264    XhtmlNode tr = t.tr();
4265    tr.td().addText(name);
4266    tr.td().addText(value);
4267  }
4268
4269  public XhtmlNode generateDocumentNarrative(Bundle feed) {
4270    /*
4271     When the document is presented for human consumption, applications must present the collated narrative portions of the following resources in order:
4272     * The Composition resource
4273     * The Subject resource
4274     * Resources referenced in the section.content
4275     */
4276    XhtmlNode root = new XhtmlNode(NodeType.Element, "div");
4277    Composition comp = (Composition) feed.getEntry().get(0).getResource();
4278    root.getChildNodes().add(comp.getText().getDiv());
4279    Resource subject = ResourceUtilities.getById(feed, null, comp.getSubject().getReference());
4280    if (subject != null && subject instanceof DomainResource) {
4281      root.hr();
4282      root.getChildNodes().add(((DomainResource)subject).getText().getDiv());
4283    }
4284    List<SectionComponent> sections = comp.getSection();
4285    renderSections(feed, root, sections, 1);
4286    return root;
4287  }
4288
4289  private void renderSections(Bundle feed, XhtmlNode node, List<SectionComponent> sections, int level) {
4290    for (SectionComponent section : sections) {
4291      node.hr();
4292      if (section.hasTitleElement())
4293        node.addTag("h"+Integer.toString(level)).addText(section.getTitle());
4294//      else if (section.hasCode())
4295//        node.addTag("h"+Integer.toString(level)).addText(displayCodeableConcept(section.getCode()));
4296
4297//      if (section.hasText()) {
4298//        node.getChildNodes().add(section.getText().getDiv());
4299//      }
4300//
4301//      if (!section.getSection().isEmpty()) {
4302//        renderSections(feed, node.addTag("blockquote"), section.getSection(), level+1);
4303//      }
4304    }
4305  }
4306
4307
4308  public class ObservationNode {
4309    private String ref;
4310    private ResourceWrapper obs;
4311    private List<ObservationNode> contained = new ArrayList<NarrativeGenerator.ObservationNode>();
4312  }
4313
4314  public XhtmlNode generateDiagnosticReport(ResourceWrapper dr) {
4315    XhtmlNode root = new XhtmlNode(NodeType.Element, "div");
4316    XhtmlNode h2 = root.h2();
4317    displayCodeableConcept(h2, getProperty(dr, "code").value());
4318    h2.tx(" ");
4319    PropertyWrapper pw = getProperty(dr, "category");
4320    if (valued(pw)) {
4321      h2.tx("(");
4322      displayCodeableConcept(h2, pw.value());
4323      h2.tx(") ");
4324    }
4325    displayDate(h2, getProperty(dr, "issued").value());
4326
4327    XhtmlNode tbl = root.table( "grid");
4328    XhtmlNode tr = tbl.tr();
4329    XhtmlNode tdl = tr.td();
4330    XhtmlNode tdr = tr.td();
4331    populateSubjectSummary(tdl, getProperty(dr, "subject").value());
4332    tdr.b().tx("Report Details");
4333    tdr.br();
4334    pw = getProperty(dr, "perfomer");
4335    if (valued(pw)) {
4336      tdr.addText(pluralise("Performer", pw.getValues().size())+":");
4337      for (BaseWrapper v : pw.getValues()) {
4338        tdr.tx(" ");
4339        displayReference(tdr, v);
4340      }
4341      tdr.br();
4342    }
4343    pw = getProperty(dr, "identifier");
4344    if (valued(pw)) {
4345      tdr.addText(pluralise("Identifier", pw.getValues().size())+":");
4346      for (BaseWrapper v : pw.getValues()) {
4347        tdr.tx(" ");
4348        displayIdentifier(tdr, v);
4349      }
4350      tdr.br();
4351    }
4352    pw = getProperty(dr, "request");
4353    if (valued(pw)) {
4354      tdr.addText(pluralise("Request", pw.getValues().size())+":");
4355      for (BaseWrapper v : pw.getValues()) {
4356        tdr.tx(" ");
4357        displayReferenceId(tdr, v);
4358      }
4359      tdr.br();
4360    }
4361
4362    pw = getProperty(dr, "result");
4363    if (valued(pw)) {
4364      List<ObservationNode> observations = fetchObservations(pw.getValues());
4365      buildObservationsTable(root, observations);
4366    }
4367
4368    pw = getProperty(dr, "conclusion");
4369    if (valued(pw))
4370      displayText(root.para(), pw.value());
4371
4372    pw = getProperty(dr, "result");
4373    if (valued(pw)) {
4374      XhtmlNode p = root.para();
4375      p.b().tx("Coded Diagnoses :");
4376      for (BaseWrapper v : pw.getValues()) {
4377        tdr.tx(" ");
4378        displayCodeableConcept(tdr, v);
4379      }
4380    }
4381    return root;
4382  }
4383
4384  private void buildObservationsTable(XhtmlNode root, List<ObservationNode> observations) {
4385    XhtmlNode tbl = root.table( "none");
4386    for (ObservationNode o : observations) {
4387      addObservationToTable(tbl, o, 0);
4388    }
4389  }
4390
4391  private void addObservationToTable(XhtmlNode tbl, ObservationNode o, int i) {
4392    XhtmlNode tr = tbl.tr();
4393    if (o.obs == null) {
4394      XhtmlNode td = tr.td().colspan("6");
4395      td.i().tx("This Observation could not be resolved");
4396    } else {
4397      addObservationToTable(tr, o.obs, i);
4398      // todo: contained observations
4399    }
4400    for (ObservationNode c : o.contained) {
4401      addObservationToTable(tbl, c, i+1);
4402    }
4403  }
4404
4405  private void addObservationToTable(XhtmlNode tr, ResourceWrapper obs, int i) {
4406    // TODO Auto-generated method stub
4407
4408    // code (+bodysite)
4409    XhtmlNode td = tr.td();
4410    PropertyWrapper pw = getProperty(obs, "result");
4411    if (valued(pw)) {
4412      displayCodeableConcept(td, pw.value());
4413    }
4414    pw = getProperty(obs, "bodySite");
4415    if (valued(pw)) {
4416      td.tx(" (");
4417      displayCodeableConcept(td, pw.value());
4418      td.tx(")");
4419    }
4420
4421    // value / dataAbsentReason (in red)
4422    td = tr.td();
4423    pw = getProperty(obs, "value[x]");
4424    if (valued(pw)) {
4425      if (pw.getTypeCode().equals("CodeableConcept"))
4426        displayCodeableConcept(td, pw.value());
4427      else if (pw.getTypeCode().equals("string"))
4428        displayText(td, pw.value());
4429      else
4430        td.addText(pw.getTypeCode()+" not rendered yet");
4431    }
4432
4433    // units
4434    td = tr.td();
4435    td.tx("to do");
4436
4437    // reference range
4438    td = tr.td();
4439    td.tx("to do");
4440
4441    // flags (status other than F, interpretation, )
4442    td = tr.td();
4443    td.tx("to do");
4444
4445    // issued if different to DR
4446    td = tr.td();
4447    td.tx("to do");
4448  }
4449
4450  private boolean valued(PropertyWrapper pw) {
4451    return pw != null && pw.hasValues();
4452  }
4453
4454  private void displayText(XhtmlNode c, BaseWrapper v) {
4455    c.addText(v.toString());
4456  }
4457
4458  private String pluralise(String name, int size) {
4459    return size == 1 ? name : name+"s";
4460  }
4461
4462  private void displayIdentifier(XhtmlNode c, BaseWrapper v) {
4463    String hint = "";
4464    PropertyWrapper pw = v.getChildByName("type");
4465    if (valued(pw)) {
4466      hint = genCC(pw.value());
4467    } else {
4468      pw = v.getChildByName("system");
4469      if (valued(pw)) {
4470        hint = pw.value().toString();
4471      }
4472    }
4473    displayText(c.span(null, hint), v.getChildByName("value").value());
4474  }
4475
4476  private String genCoding(BaseWrapper value) {
4477    PropertyWrapper pw = value.getChildByName("display");
4478    if (valued(pw))
4479      return pw.value().toString();
4480    pw = value.getChildByName("code");
4481    if (valued(pw))
4482      return pw.value().toString();
4483    return "";
4484  }
4485
4486  private String genCC(BaseWrapper value) {
4487    PropertyWrapper pw = value.getChildByName("text");
4488    if (valued(pw))
4489      return pw.value().toString();
4490    pw = value.getChildByName("coding");
4491    if (valued(pw))
4492      return genCoding(pw.getValues().get(0));
4493    return "";
4494  }
4495
4496  private void displayReference(XhtmlNode c, BaseWrapper v) {
4497    c.tx("to do");
4498  }
4499
4500
4501  private void displayDate(XhtmlNode c, BaseWrapper baseWrapper) {
4502    c.tx("to do");
4503  }
4504
4505  private void displayCodeableConcept(XhtmlNode c, BaseWrapper property) {
4506    c.tx("to do");
4507  }
4508
4509  private void displayReferenceId(XhtmlNode c, BaseWrapper v) {
4510    c.tx("to do");
4511  }
4512
4513  private PropertyWrapper getProperty(ResourceWrapper res, String name) {
4514    for (PropertyWrapper t : res.children()) {
4515      if (t.getName().equals(name))
4516        return t;
4517    }
4518    return null;
4519  }
4520
4521  private void populateSubjectSummary(XhtmlNode container, BaseWrapper subject) {
4522    ResourceWrapper r = fetchResource(subject);
4523    if (r == null)
4524      container.tx("Unable to get Patient Details");
4525    else if (r.getName().equals("Patient"))
4526      generatePatientSummary(container, r);
4527    else
4528      container.tx("Not done yet");
4529  }
4530
4531  private void generatePatientSummary(XhtmlNode c, ResourceWrapper r) {
4532    c.tx("to do");
4533  }
4534
4535  private ResourceWrapper fetchResource(BaseWrapper subject) {
4536    if (resolver == null)
4537      return null;
4538    String url = subject.getChildByName("reference").value().toString();
4539    ResourceWithReference rr = resolver.resolve(url);
4540    return rr == null ? null : rr.resource;
4541  }
4542
4543  private List<ObservationNode> fetchObservations(List<BaseWrapper> list) {
4544    return new ArrayList<NarrativeGenerator.ObservationNode>();
4545  }
4546
4547  public XhtmlNode renderBundle(Bundle b) throws FHIRException {
4548    if (b.getType() == BundleType.DOCUMENT) {
4549      if (!b.hasEntry() || !(b.getEntryFirstRep().hasResource() && b.getEntryFirstRep().getResource() instanceof Composition))
4550        throw new FHIRException("Invalid document - first entry is not a Composition");
4551      Composition dr = (Composition) b.getEntryFirstRep().getResource();
4552      return dr.getText().getDiv();
4553    } else  {
4554      XhtmlNode root = new XhtmlNode(NodeType.Element, "div");
4555      root.para().addText("Bundle "+b.getId()+" of type "+b.getType().toCode());
4556      int i = 0;
4557      for (BundleEntryComponent be : b.getEntry()) {
4558        i++;
4559        if (be.hasResource() && be.getResource().hasId())
4560          root.an(be.getResource().getResourceType().name().toLowerCase() + "_" + be.getResource().getId());
4561        root.hr();
4562        root.para().addText("Entry "+Integer.toString(i)+(be.hasFullUrl() ? " - Full URL = " + be.getFullUrl() : ""));
4563        if (be.hasRequest())
4564          renderRequest(root, be.getRequest());
4565        if (be.hasSearch())
4566          renderSearch(root, be.getSearch());
4567        if (be.hasResponse())
4568          renderResponse(root, be.getResponse());
4569        if (be.hasResource()) {
4570          root.para().addText("Resource "+be.getResource().fhirType()+":");
4571          if (be.hasResource() && be.getResource() instanceof DomainResource) {
4572            DomainResource dr = (DomainResource) be.getResource();
4573            if ( dr.getText().hasDiv())
4574              root.blockquote().getChildNodes().addAll(dr.getText().getDiv().getChildNodes());
4575          }
4576        }
4577      }
4578      return root;
4579    }
4580  }
4581
4582  private void renderSearch(XhtmlNode root, BundleEntrySearchComponent search) {
4583    StringBuilder b = new StringBuilder();
4584    b.append("Search: ");
4585    if (search.hasMode())
4586      b.append("mode = "+search.getMode().toCode());
4587    if (search.hasScore()) {
4588      if (search.hasMode())
4589        b.append(",");
4590      b.append("score = "+search.getScore());
4591    }
4592    root.para().addText(b.toString());    
4593  }
4594
4595  private void renderResponse(XhtmlNode root, BundleEntryResponseComponent response) {
4596    root.para().addText("Request:");
4597    StringBuilder b = new StringBuilder();
4598    b.append(response.getStatus()+"\r\n");
4599    if (response.hasLocation())
4600      b.append("Location: "+response.getLocation()+"\r\n");
4601    if (response.hasEtag())
4602      b.append("E-Tag: "+response.getEtag()+"\r\n");
4603    if (response.hasLastModified())
4604      b.append("LastModified: "+response.getEtag()+"\r\n");
4605    root.pre().addText(b.toString());    
4606  }
4607
4608  private void renderRequest(XhtmlNode root, BundleEntryRequestComponent request) {
4609    root.para().addText("Response:");
4610    StringBuilder b = new StringBuilder();
4611    b.append(request.getMethod()+" "+request.getUrl()+"\r\n");
4612    if (request.hasIfNoneMatch())
4613      b.append("If-None-Match: "+request.getIfNoneMatch()+"\r\n");
4614    if (request.hasIfModifiedSince())
4615      b.append("If-Modified-Since: "+request.getIfModifiedSince()+"\r\n");
4616    if (request.hasIfMatch())
4617      b.append("If-Match: "+request.getIfMatch()+"\r\n");
4618    if (request.hasIfNoneExist())
4619      b.append("If-None-Exist: "+request.getIfNoneExist()+"\r\n");
4620    root.pre().addText(b.toString());    
4621  }
4622
4623  public XhtmlNode renderBundle(org.hl7.fhir.r4.elementmodel.Element element) throws FHIRException {
4624    XhtmlNode root = new XhtmlNode(NodeType.Element, "div");
4625    for (Base b : element.listChildrenByName("entry")) {
4626      org.hl7.fhir.r4.elementmodel.Element r = ((org.hl7.fhir.r4.elementmodel.Element) b).getNamedChild("resource");
4627      if (r!=null) {
4628        XhtmlNode c = getHtmlForResource(r);
4629        if (c != null)
4630          root.getChildNodes().addAll(c.getChildNodes());
4631        root.hr();
4632      }
4633    }
4634    return root;
4635  }
4636
4637  private XhtmlNode getHtmlForResource(org.hl7.fhir.r4.elementmodel.Element element) {
4638    org.hl7.fhir.r4.elementmodel.Element text = element.getNamedChild("text");
4639    if (text == null)
4640      return null;
4641    org.hl7.fhir.r4.elementmodel.Element div = text.getNamedChild("div");
4642    if (div == null)
4643      return null;
4644    else
4645      return div.getXhtml();
4646  }
4647
4648  public String getDefinitionsTarget() {
4649    return definitionsTarget;
4650  }
4651
4652  public void setDefinitionsTarget(String definitionsTarget) {
4653    this.definitionsTarget = definitionsTarget;
4654  }
4655
4656  public String getCorePath() {
4657    return corePath;
4658  }
4659
4660  public void setCorePath(String corePath) {
4661    this.corePath = corePath;
4662  }
4663
4664  public String getDestDir() {
4665    return destDir;
4666  }
4667
4668  public void setDestDir(String destDir) {
4669    this.destDir = destDir;
4670  }
4671
4672  public ProfileKnowledgeProvider getPkp() {
4673    return pkp;
4674  }
4675
4676  public NarrativeGenerator setPkp(ProfileKnowledgeProvider pkp) {
4677    this.pkp = pkp;
4678    return this;
4679  }
4680
4681  public boolean isPretty() {
4682    return pretty;
4683  }
4684
4685  public NarrativeGenerator setPretty(boolean pretty) {
4686    this.pretty = pretty;
4687    return this;
4688  }
4689
4690  public boolean isCanonicalUrlsAsLinks() {
4691    return canonicalUrlsAsLinks;
4692  }
4693
4694  @Override
4695  public void setCanonicalUrlsAsLinks(boolean canonicalUrlsAsLinks) {
4696    this.canonicalUrlsAsLinks = canonicalUrlsAsLinks;
4697  }
4698
4699  public String getSnomedEdition() {
4700    return snomedEdition;
4701  }
4702
4703  public NarrativeGenerator setSnomedEdition(String snomedEdition) {
4704    this.snomedEdition = snomedEdition;
4705    return this;
4706  }
4707
4708  public TerminologyServiceOptions getTerminologyServiceOptions() {
4709    return terminologyServiceOptions;
4710  }
4711
4712  public void setTerminologyServiceOptions(TerminologyServiceOptions terminologyServiceOptions) {
4713    this.terminologyServiceOptions = terminologyServiceOptions;
4714  }
4715
4716  
4717}