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