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