001package org.hl7.fhir.dstu2.utils;
002
003/*-
004 * #%L
005 * org.hl7.fhir.dstu2
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 java.io.IOException;
025import java.io.UnsupportedEncodingException;
026
027/*
028Copyright (c) 2011+, HL7, Inc
029  All rights reserved.
030
031  Redistribution and use in source and binary forms, with or without modification,
032  are permitted provided that the following conditions are met:
033
034   * Redistributions of source code must retain the above copyright notice, this
035     list of conditions and the following disclaimer.
036   * Redistributions in binary form must reproduce the above copyright notice,
037     this list of conditions and the following disclaimer in the documentation
038     and/or other materials provided with the distribution.
039   * Neither the name of HL7 nor the names of its contributors may be used to
040     endorse or promote products derived from this software without specific
041     prior written permission.
042
043  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
044  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
045  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
046  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
047  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
048  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
049  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
050  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
051  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
052  POSSIBILITY OF SUCH DAMAGE.
053
054*/
055
056import java.util.ArrayList;
057import java.util.Collections;
058import java.util.HashMap;
059import java.util.HashSet;
060import java.util.List;
061import java.util.Map;
062
063import org.apache.commons.codec.binary.Base64;
064import org.apache.commons.lang3.NotImplementedException;
065import org.hl7.fhir.dstu2.formats.FormatUtilities;
066import org.hl7.fhir.dstu2.formats.IParser.OutputStyle;
067import org.hl7.fhir.dstu2.model.Address;
068import org.hl7.fhir.dstu2.model.Annotation;
069import org.hl7.fhir.dstu2.model.Attachment;
070import org.hl7.fhir.dstu2.model.Base;
071import org.hl7.fhir.dstu2.model.Base64BinaryType;
072import org.hl7.fhir.dstu2.model.BooleanType;
073import org.hl7.fhir.dstu2.model.Bundle;
074import org.hl7.fhir.dstu2.model.CodeType;
075import org.hl7.fhir.dstu2.model.CodeableConcept;
076import org.hl7.fhir.dstu2.model.Coding;
077import org.hl7.fhir.dstu2.model.Composition;
078import org.hl7.fhir.dstu2.model.Composition.SectionComponent;
079import org.hl7.fhir.dstu2.model.ConceptMap;
080import org.hl7.fhir.dstu2.model.ConceptMap.ConceptMapContactComponent;
081import org.hl7.fhir.dstu2.model.ConceptMap.OtherElementComponent;
082import org.hl7.fhir.dstu2.model.ConceptMap.SourceElementComponent;
083import org.hl7.fhir.dstu2.model.ConceptMap.TargetElementComponent;
084import org.hl7.fhir.dstu2.model.Conformance;
085import org.hl7.fhir.dstu2.model.Conformance.ConformanceRestComponent;
086import org.hl7.fhir.dstu2.model.Conformance.ConformanceRestResourceComponent;
087import org.hl7.fhir.dstu2.model.Conformance.ResourceInteractionComponent;
088import org.hl7.fhir.dstu2.model.Conformance.SystemInteractionComponent;
089import org.hl7.fhir.dstu2.model.Conformance.SystemRestfulInteraction;
090import org.hl7.fhir.dstu2.model.Conformance.TypeRestfulInteraction;
091import org.hl7.fhir.dstu2.model.ContactPoint;
092import org.hl7.fhir.dstu2.model.ContactPoint.ContactPointSystem;
093import org.hl7.fhir.dstu2.model.DateTimeType;
094import org.hl7.fhir.dstu2.model.DomainResource;
095import org.hl7.fhir.dstu2.model.ElementDefinition;
096import org.hl7.fhir.dstu2.model.ElementDefinition.TypeRefComponent;
097import org.hl7.fhir.dstu2.model.Enumeration;
098import org.hl7.fhir.dstu2.model.Extension;
099import org.hl7.fhir.dstu2.model.ExtensionHelper;
100import org.hl7.fhir.dstu2.model.HumanName;
101import org.hl7.fhir.dstu2.model.HumanName.NameUse;
102import org.hl7.fhir.dstu2.model.IdType;
103import org.hl7.fhir.dstu2.model.Identifier;
104import org.hl7.fhir.dstu2.model.InstantType;
105import org.hl7.fhir.dstu2.model.Meta;
106import org.hl7.fhir.dstu2.model.Narrative;
107import org.hl7.fhir.dstu2.model.Narrative.NarrativeStatus;
108import org.hl7.fhir.dstu2.model.OperationDefinition;
109import org.hl7.fhir.dstu2.model.OperationDefinition.OperationDefinitionParameterComponent;
110import org.hl7.fhir.dstu2.model.OperationOutcome;
111import org.hl7.fhir.dstu2.model.OperationOutcome.IssueSeverity;
112import org.hl7.fhir.dstu2.model.OperationOutcome.OperationOutcomeIssueComponent;
113import org.hl7.fhir.dstu2.model.Period;
114import org.hl7.fhir.dstu2.model.PrimitiveType;
115import org.hl7.fhir.dstu2.model.Property;
116import org.hl7.fhir.dstu2.model.Quantity;
117import org.hl7.fhir.dstu2.model.Range;
118import org.hl7.fhir.dstu2.model.Ratio;
119import org.hl7.fhir.dstu2.model.Reference;
120import org.hl7.fhir.dstu2.model.Resource;
121import org.hl7.fhir.dstu2.model.SampledData;
122import org.hl7.fhir.dstu2.model.StringType;
123import org.hl7.fhir.dstu2.model.StructureDefinition;
124import org.hl7.fhir.dstu2.model.Timing;
125import org.hl7.fhir.dstu2.model.Timing.EventTiming;
126import org.hl7.fhir.dstu2.model.Timing.TimingRepeatComponent;
127import org.hl7.fhir.dstu2.model.Timing.UnitsOfTime;
128import org.hl7.fhir.dstu2.model.UriType;
129import org.hl7.fhir.dstu2.model.ValueSet;
130import org.hl7.fhir.dstu2.model.ValueSet.ConceptDefinitionComponent;
131import org.hl7.fhir.dstu2.model.ValueSet.ConceptDefinitionDesignationComponent;
132import org.hl7.fhir.dstu2.model.ValueSet.ConceptReferenceComponent;
133import org.hl7.fhir.dstu2.model.ValueSet.ConceptSetComponent;
134import org.hl7.fhir.dstu2.model.ValueSet.ConceptSetFilterComponent;
135import org.hl7.fhir.dstu2.model.ValueSet.FilterOperator;
136import org.hl7.fhir.dstu2.model.ValueSet.ValueSetExpansionContainsComponent;
137import org.hl7.fhir.dstu2.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
138import org.hl7.fhir.dstu2.utils.IWorkerContext.ValidationResult;
139import org.hl7.fhir.exceptions.DefinitionException;
140import org.hl7.fhir.exceptions.FHIRException;
141import org.hl7.fhir.exceptions.FHIRFormatError;
142import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
143import org.hl7.fhir.utilities.MarkDownProcessor;
144import org.hl7.fhir.utilities.MarkDownProcessor.Dialect;
145import org.hl7.fhir.utilities.Utilities;
146import org.hl7.fhir.utilities.xhtml.NodeType;
147import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
148import org.hl7.fhir.utilities.xhtml.XhtmlNode;
149import org.hl7.fhir.utilities.xhtml.XhtmlParser;
150import org.hl7.fhir.utilities.xml.XMLUtil;
151import org.hl7.fhir.utilities.xml.XmlGenerator;
152import org.w3c.dom.Element;
153
154public class NarrativeGenerator implements INarrativeGenerator {
155
156  private interface PropertyWrapper {
157    public String getName();
158    public boolean hasValues();
159    public List<BaseWrapper> getValues();
160    public String getTypeCode();
161    public String getDefinition();
162    public int getMinCardinality();
163    public int getMaxCardinality();
164    public StructureDefinition getStructure();
165  }
166
167  private interface ResourceWrapper {
168    public List<ResourceWrapper> getContained();
169    public String getId();
170    public XhtmlNode getNarrative() throws FHIRFormatError, IOException, FHIRException;
171    public String getName();
172    public List<PropertyWrapper> children();
173  }
174
175  private interface BaseWrapper {
176    public Base getBase() throws UnsupportedEncodingException, IOException, FHIRException;
177    public List<PropertyWrapper> children();
178    public PropertyWrapper getChildByName(String tail);
179  }
180
181  private class BaseWrapperElement implements BaseWrapper {
182    private Element element;
183    private String type;
184    private StructureDefinition structure;
185    private ElementDefinition definition;
186    private List<ElementDefinition> children;
187    private List<PropertyWrapper> list;
188
189    public BaseWrapperElement(Element element, String type, StructureDefinition structure, ElementDefinition definition) {
190      this.element = element;
191      this.type = type;
192      this.structure = structure;
193      this.definition = definition;
194    }
195
196    @Override
197    public Base getBase() throws UnsupportedEncodingException, IOException, FHIRException {
198      if (type == null || type.equals("Resource") || type.equals("BackboneElement") || type.equals("Element"))
199        return null;
200
201      String xml = new XmlGenerator().generate(element);
202      return context.newXmlParser().setOutputStyle(OutputStyle.PRETTY).parseType(xml, type);
203    }
204
205    @Override
206    public List<PropertyWrapper> children() {
207      if (list == null) {
208        children = ProfileUtilities.getChildList(structure, definition);
209        list = new ArrayList<NarrativeGenerator.PropertyWrapper>();
210        for (ElementDefinition child : children) {
211          List<Element> elements = new ArrayList<Element>();
212          XMLUtil.getNamedChildrenWithWildcard(element, tail(child.getPath()), elements);
213          list.add(new PropertyWrapperElement(structure, child, elements));
214        }
215      }
216      return list;
217    }
218
219    @Override
220    public PropertyWrapper getChildByName(String name) {
221      for (PropertyWrapper p : children())
222        if (p.getName().equals(name))
223          return p;
224      return null;
225    }
226
227  }
228
229  private class PropertyWrapperElement implements PropertyWrapper {
230
231    private StructureDefinition structure;
232    private ElementDefinition definition;
233    private List<Element> values;
234    private List<BaseWrapper> list;
235
236    public PropertyWrapperElement(StructureDefinition structure, ElementDefinition definition, List<Element> values) {
237      this.structure = structure;
238      this.definition = definition;
239      this.values = values;
240    }
241
242    @Override
243    public String getName() {
244      return tail(definition.getPath());
245    }
246
247    @Override
248    public boolean hasValues() {
249      return values.size() > 0;
250    }
251
252    @Override
253    public List<BaseWrapper> getValues() {
254      if (list == null) {
255        list = new ArrayList<NarrativeGenerator.BaseWrapper>();
256        for (Element e : values)
257          list.add(new BaseWrapperElement(e, determineType(e), structure, definition));
258      }
259      return list;
260    }
261    private String determineType(Element e) {
262      if (definition.getType().isEmpty())
263        return null;
264      if (definition.getType().size() == 1) {
265        if (definition.getType().get(0).getCode().equals("Element") || definition.getType().get(0).getCode().equals("BackboneElement"))
266          return null;
267        return definition.getType().get(0).getCode();
268      }
269      String t = e.getNodeName().substring(tail(definition.getPath()).length()-3);
270      boolean allReference = true;
271      for (TypeRefComponent tr : definition.getType()) {
272        if (!tr.getCode().equals("Reference"))
273          allReference = false;
274      }
275      if (allReference)
276        return "Reference";
277
278      if (ProfileUtilities.isPrimitive(t))
279        return Utilities.uncapitalize(t);
280      else
281        return t;
282    }
283
284    @Override
285    public String getTypeCode() {
286      throw new Error("todo");
287    }
288
289    @Override
290    public String getDefinition() {
291      throw new Error("todo");
292    }
293
294    @Override
295    public int getMinCardinality() {
296      throw new Error("todo");
297//      return definition.getMin();
298    }
299
300    @Override
301    public int getMaxCardinality() {
302      throw new Error("todo");
303    }
304
305    @Override
306    public StructureDefinition getStructure() {
307      return structure;
308    }
309
310  }
311
312  private class ResurceWrapperElement implements ResourceWrapper {
313
314    private Element wrapped;
315    private StructureDefinition definition;
316    private List<ResourceWrapper> list;
317    private List<PropertyWrapper> list2;
318
319    public ResurceWrapperElement(Element wrapped, StructureDefinition definition) {
320      this.wrapped = wrapped;
321      this.definition = definition;
322    }
323
324    @Override
325    public List<ResourceWrapper> getContained() {
326      if (list == null) {
327        List<Element> children = new ArrayList<Element>();
328        XMLUtil.getNamedChildren(wrapped, "contained", children);
329        list = new ArrayList<NarrativeGenerator.ResourceWrapper>();
330        for (Element e : children) {
331          Element c = XMLUtil.getFirstChild(e);
332          list.add(new ResurceWrapperElement(c, context.fetchTypeDefinition(c.getNodeName())));
333        }
334      }
335      return list;
336    }
337
338    @Override
339    public String getId() {
340      return XMLUtil.getNamedChildValue(wrapped, "id");
341    }
342
343    @Override
344    public XhtmlNode getNarrative() throws FHIRFormatError, IOException, FHIRException {
345      Element txt = XMLUtil.getNamedChild(wrapped, "text");
346      if (txt == null)
347        return null;
348      Element div = XMLUtil.getNamedChild(txt, "div");
349      if (div == null)
350        return null;
351      return new XhtmlParser().parse(new XmlGenerator().generate(div), "div");
352    }
353
354    @Override
355    public String getName() {
356      return wrapped.getNodeName();
357    }
358
359    @Override
360    public List<PropertyWrapper> children() {
361      if (list2 == null) {
362        List<ElementDefinition> children = ProfileUtilities.getChildList(definition, definition.getSnapshot().getElement().get(0));
363        list2 = new ArrayList<NarrativeGenerator.PropertyWrapper>();
364        for (ElementDefinition child : children) {
365          List<Element> elements = new ArrayList<Element>();
366          XMLUtil.getNamedChildrenWithWildcard(wrapped, tail(child.getPath()), elements);
367          list2.add(new PropertyWrapperElement(definition, child, elements));
368        }
369      }
370      return list2;
371    }
372  }
373
374  private class PropertyWrapperDirect implements PropertyWrapper {
375    private Property wrapped;
376    private List<BaseWrapper> list;
377
378    private PropertyWrapperDirect(Property wrapped) {
379      super();
380      if (wrapped == null)
381        throw new Error("wrapped == null");
382      this.wrapped = wrapped;
383    }
384
385    @Override
386    public String getName() {
387      return wrapped.getName();
388    }
389
390    @Override
391    public boolean hasValues() {
392      return wrapped.hasValues();
393    }
394
395    @Override
396    public List<BaseWrapper> getValues() {
397      if (list == null) {
398        list = new ArrayList<NarrativeGenerator.BaseWrapper>();
399        for (Base b : wrapped.getValues())
400          list.add(b == null ? null : new BaseWrapperDirect(b));
401      }
402      return list;
403    }
404
405    @Override
406    public String getTypeCode() {
407      return wrapped.getTypeCode();
408    }
409
410    @Override
411    public String getDefinition() {
412      return wrapped.getDefinition();
413    }
414
415    @Override
416    public int getMinCardinality() {
417      return wrapped.getMinCardinality();
418    }
419
420    @Override
421    public int getMaxCardinality() {
422      return wrapped.getMinCardinality();
423    }
424
425    @Override
426    public StructureDefinition getStructure() {
427      return wrapped.getStructure();
428    }
429  }
430
431  private class BaseWrapperDirect implements BaseWrapper {
432    private Base wrapped;
433    private List<PropertyWrapper> list;
434
435    private BaseWrapperDirect(Base wrapped) {
436      super();
437      if (wrapped == null)
438        throw new Error("wrapped == null");
439      this.wrapped = wrapped;
440    }
441
442    @Override
443    public Base getBase() {
444      return wrapped;
445    }
446
447    @Override
448    public List<PropertyWrapper> children() {
449      if (list == null) {
450        list = new ArrayList<NarrativeGenerator.PropertyWrapper>();
451        for (Property p : wrapped.children())
452          list.add(new PropertyWrapperDirect(p));
453      }
454      return list;
455
456    }
457
458    @Override
459    public PropertyWrapper getChildByName(String name) {
460      Property p = wrapped.getChildByName(name);
461      if (p == null)
462        return null;
463      else
464        return new PropertyWrapperDirect(p);
465    }
466
467  }
468
469  private class ResourceWrapperDirect implements ResourceWrapper {
470    private Resource wrapped;
471
472    private ResourceWrapperDirect(Resource wrapped) {
473      super();
474      if (wrapped == null)
475        throw new Error("wrapped == null");
476      this.wrapped = wrapped;
477    }
478
479    @Override
480    public List<ResourceWrapper> getContained() {
481      List<ResourceWrapper> list = new ArrayList<NarrativeGenerator.ResourceWrapper>();
482      if (wrapped instanceof DomainResource) {
483        DomainResource dr = (DomainResource) wrapped;
484        for (Resource c : dr.getContained()) {
485          list.add(new ResourceWrapperDirect(c));
486        }
487      }
488      return list;
489    }
490
491    @Override
492    public String getId() {
493      return wrapped.getId();
494    }
495
496    @Override
497    public XhtmlNode getNarrative() {
498      if (wrapped instanceof DomainResource) {
499        DomainResource dr = (DomainResource) wrapped;
500        if (dr.hasText() && dr.getText().hasDiv())
501          return dr.getText().getDiv();
502      }
503      return null;
504    }
505
506    @Override
507    public String getName() {
508      return wrapped.getResourceType().toString();
509    }
510
511    @Override
512    public List<PropertyWrapper> children() {
513      List<PropertyWrapper> list = new ArrayList<PropertyWrapper>();
514      for (Property c : wrapped.children())
515        list.add(new PropertyWrapperDirect(c));
516      return list;
517    }
518  }
519
520  public class ResourceWithReference {
521
522    private String reference;
523    private ResourceWrapper resource;
524
525    public ResourceWithReference(String reference, ResourceWrapper resource) {
526      this.reference = reference;
527      this.resource = resource;
528    }
529
530    public String getReference() {
531      return reference;
532    }
533
534    public ResourceWrapper getResource() {
535      return resource;
536    }
537  }
538
539  private String prefix;
540  private IWorkerContext context;
541  private String basePath;
542  private String tooCostlyNote;
543  private boolean pretty;
544
545
546  public NarrativeGenerator(String prefix, String basePath, IWorkerContext context) {
547    super();
548    this.prefix = prefix;
549    this.context = context;
550    this.basePath = basePath;
551  }
552
553
554  public String getTooCostlyNote() {
555    return tooCostlyNote;
556  }
557
558
559  public NarrativeGenerator setTooCostlyNote(String tooCostlyNote) {
560    this.tooCostlyNote = tooCostlyNote;
561    return this;
562  }
563
564
565  public void generate(DomainResource r) throws EOperationOutcome, FHIRException, IOException {
566    if (r instanceof ConceptMap) {
567      generate((ConceptMap) r); // Maintainer = Grahame
568    } else if (r instanceof ValueSet) {
569      generate((ValueSet) r, true); // Maintainer = Grahame
570    } else if (r instanceof OperationOutcome) {
571      generate((OperationOutcome) r); // Maintainer = Grahame
572    } else if (r instanceof Conformance) {
573      generate((Conformance) r);   // Maintainer = Grahame
574    } else if (r instanceof OperationDefinition) {
575      generate((OperationDefinition) r);   // Maintainer = Grahame
576    } else {
577      StructureDefinition p = null;
578      if (r.hasMeta())
579        for (UriType pu : r.getMeta().getProfile())
580          if (p == null)
581            p = context.fetchResource(StructureDefinition.class, pu.getValue());
582      if (p == null)
583        p = context.fetchResource(StructureDefinition.class, r.getResourceType().toString());
584      if (p == null)
585        p = context.fetchTypeDefinition(r.getResourceType().toString().toLowerCase());
586      if (p != null)
587        generateByProfile(r, p, true);
588    }
589  }
590
591  // dom based version, for build program
592  public String generate(Element doc) throws IOException {
593    String rt = "http://hl7.org/fhir/StructureDefinition/"+doc.getNodeName();
594    StructureDefinition p = context.fetchResource(StructureDefinition.class, rt);
595    return generateByProfile(doc, p, true);
596  }
597
598  private void generateByProfile(DomainResource r, StructureDefinition profile, boolean showCodeDetails) {
599    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
600    x.addTag("p").addTag("b").addText("Generated Narrative"+(showCodeDetails ? " with Details" : ""));
601    try {
602      generateByProfile(r, profile, r, profile.getSnapshot().getElement(), profile.getSnapshot().getElement().get(0), getChildrenForPath(profile.getSnapshot().getElement(), r.getResourceType().toString()), x, r.getResourceType().toString(), showCodeDetails);
603    } catch (Exception e) {
604      e.printStackTrace();
605      x.addTag("p").addTag("b").setAttribute("style", "color: maroon").addText("Exception generating Narrative: "+e.getMessage());
606    }
607    inject(r, x,  NarrativeStatus.GENERATED);
608  }
609
610  private String generateByProfile(Element er, StructureDefinition profile, boolean showCodeDetails) throws IOException {
611    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
612    x.addTag("p").addTag("b").addText("Generated Narrative"+(showCodeDetails ? " with Details" : ""));
613    try {
614      generateByProfile(er, profile, er, profile.getSnapshot().getElement(), profile.getSnapshot().getElement().get(0), getChildrenForPath(profile.getSnapshot().getElement(), er.getLocalName()), x, er.getLocalName(), showCodeDetails);
615    } catch (Exception e) {
616      e.printStackTrace();
617      x.addTag("p").addTag("b").setAttribute("style", "color: maroon").addText("Exception generating Narrative: "+e.getMessage());
618    }
619    inject(er, x,  NarrativeStatus.GENERATED);
620    return new XhtmlComposer(true, pretty).compose(x);
621  }
622
623  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 {
624
625    ResurceWrapperElement resw = new ResurceWrapperElement(eres, profile);
626    BaseWrapperElement base = new BaseWrapperElement(ee, null, profile, profile.getSnapshot().getElement().get(0));
627    generateByProfile(resw, profile, base, allElements, defn, children, x, path, showCodeDetails);
628  }
629
630  private void generateByProfile(Resource res, StructureDefinition profile, Base e, List<ElementDefinition> allElements, ElementDefinition defn, List<ElementDefinition> children,  XhtmlNode x, String path, boolean showCodeDetails) throws FHIRException, UnsupportedEncodingException, IOException {
631    generateByProfile(new ResourceWrapperDirect(res), profile, new BaseWrapperDirect(e), allElements, defn, children, x, path, showCodeDetails);
632  }
633
634  private void generateByProfile(ResourceWrapper res, StructureDefinition profile, BaseWrapper e, List<ElementDefinition> allElements, ElementDefinition defn, List<ElementDefinition> children,  XhtmlNode x, String path, boolean showCodeDetails) throws FHIRException, UnsupportedEncodingException, IOException {
635    if (children.isEmpty()) {
636      renderLeaf(res, e, defn, x, false, showCodeDetails, readDisplayHints(defn));
637    } else {
638      for (PropertyWrapper p : splitExtensions(profile, e.children())) {
639        if (p.hasValues()) {
640          ElementDefinition child = getElementDefinition(children, path+"."+p.getName(), p);
641          if (child != null) {
642            Map<String, String> displayHints = readDisplayHints(child);
643            if (!exemptFromRendering(child)) {
644              List<ElementDefinition> grandChildren = getChildrenForPath(allElements, path+"."+p.getName());
645            filterGrandChildren(grandChildren, path+"."+p.getName(), p);
646              if (p.getValues().size() > 0 && child != null) {
647                if (isPrimitive(child)) {
648                  XhtmlNode para = x.addTag("p");
649                  String name = p.getName();
650                  if (name.endsWith("[x]"))
651                    name = name.substring(0, name.length() - 3);
652                  if (showCodeDetails || !isDefaultValue(displayHints, p.getValues())) {
653                    para.addTag("b").addText(name);
654                    para.addText(": ");
655                    if (renderAsList(child) && p.getValues().size() > 1) {
656                      XhtmlNode list = x.addTag("ul");
657                      for (BaseWrapper v : p.getValues())
658                        renderLeaf(res, v, child, list.addTag("li"), false, showCodeDetails, displayHints);
659                    } else {
660                      boolean first = true;
661                      for (BaseWrapper v : p.getValues()) {
662                        if (first)
663                          first = false;
664                        else
665                          para.addText(", ");
666                        renderLeaf(res, v, child, para, false, showCodeDetails, displayHints);
667                      }
668                    }
669                  }
670                } else if (canDoTable(path, p, grandChildren)) {
671                  x.addTag("h3").addText(Utilities.capitalize(Utilities.camelCase(Utilities.pluralizeMe(p.getName()))));
672                  XhtmlNode tbl = x.addTag("table").setAttribute("class", "grid");
673                  XhtmlNode tr = tbl.addTag("tr");
674                  tr.addTag("td").addText("-"); // work around problem with empty table rows
675                  addColumnHeadings(tr, grandChildren);
676                  for (BaseWrapper v : p.getValues()) {
677                    if (v != null) {
678                      tr = tbl.addTag("tr");
679                      tr.addTag("td").addText("*"); // work around problem with empty table rows
680                      addColumnValues(res, tr, grandChildren, v, showCodeDetails, displayHints);
681                    }
682                  }
683                } else {
684                  for (BaseWrapper v : p.getValues()) {
685                    if (v != null) {
686                      XhtmlNode bq = x.addTag("blockquote");
687                      bq.addTag("p").addTag("b").addText(p.getName());
688                      generateByProfile(res, profile, v, allElements, child, grandChildren, bq, path+"."+p.getName(), showCodeDetails);
689                    }
690                  }
691                }
692              }
693            }
694          }
695        }
696      }
697    }
698  }
699
700  private void filterGrandChildren(List<ElementDefinition> grandChildren,  String string, PropertyWrapper prop) {
701        List<ElementDefinition> toRemove = new ArrayList<ElementDefinition>();
702        toRemove.addAll(grandChildren);
703        for (BaseWrapper b : prop.getValues()) {
704        List<ElementDefinition> list = new ArrayList<ElementDefinition>();
705                for (ElementDefinition ed : toRemove) {
706                        PropertyWrapper p = b.getChildByName(tail(ed.getPath()));
707                        if (p != null && p.hasValues())
708                                list.add(ed);
709                }
710                toRemove.removeAll(list);
711        }
712        grandChildren.removeAll(toRemove);
713  }
714
715  private List<PropertyWrapper> splitExtensions(StructureDefinition profile, List<PropertyWrapper> children) throws UnsupportedEncodingException, IOException, FHIRException {
716    List<PropertyWrapper> results = new ArrayList<PropertyWrapper>();
717    Map<String, PropertyWrapper> map = new HashMap<String, PropertyWrapper>();
718    for (PropertyWrapper p : children)
719      if (p.getName().equals("extension") || p.getName().equals("modifierExtension")) {
720        // we're going to split these up, and create a property for each url
721        if (p.hasValues()) {
722          for (BaseWrapper v : p.getValues()) {
723            Extension ex  = (Extension) v.getBase();
724            String url = ex.getUrl();
725            StructureDefinition ed = context.fetchResource(StructureDefinition.class, url);
726            if (p.getName().equals("modifierExtension") && ed == null)
727              throw new DefinitionException("Unknown modifier extension "+url);
728            PropertyWrapper pe = map.get(p.getName()+"["+url+"]");
729            if (pe == null) {
730              if (ed == null) {
731                if (url.startsWith("http://hl7.org/fhir"))
732                  throw new DefinitionException("unknown extension "+url);
733                System.out.println("unknown extension "+url);
734                pe = new PropertyWrapperDirect(new Property(p.getName()+"["+url+"]", p.getTypeCode(), p.getDefinition(), p.getMinCardinality(), p.getMaxCardinality(), ex));
735              } else {
736                ElementDefinition def = ed.getSnapshot().getElement().get(0);
737                pe = new PropertyWrapperDirect(new Property(p.getName()+"["+url+"]", "Extension", def.getDefinition(), def.getMin(), def.getMax().equals("*") ? Integer.MAX_VALUE : Integer.parseInt(def.getMax()), ex));
738                ((PropertyWrapperDirect) pe).wrapped.setStructure(ed);
739              }
740              results.add(pe);
741            } else
742              pe.getValues().add(v);
743          }
744        }
745      } else
746        results.add(p);
747    return results;
748  }
749
750  @SuppressWarnings("rawtypes")
751  private boolean isDefaultValue(Map<String, String> displayHints, List<BaseWrapper> list) throws UnsupportedEncodingException, IOException, FHIRException {
752    if (list.size() != 1)
753      return false;
754    if (list.get(0).getBase() instanceof PrimitiveType)
755      return isDefault(displayHints, (PrimitiveType) list.get(0).getBase());
756    else
757      return false;
758  }
759
760  private boolean isDefault(Map<String, String> displayHints, PrimitiveType primitiveType) {
761    String v = primitiveType.asStringValue();
762    if (!Utilities.noString(v) && displayHints.containsKey("default") && v.equals(displayHints.get("default")))
763        return true;
764    return false;
765  }
766
767  private boolean exemptFromRendering(ElementDefinition child) {
768    if (child == null)
769      return false;
770    if ("Composition.subject".equals(child.getPath()))
771      return true;
772    if ("Composition.section".equals(child.getPath()))
773      return true;
774    return false;
775  }
776
777  private boolean renderAsList(ElementDefinition child) {
778    if (child.getType().size() == 1) {
779      String t = child.getType().get(0).getCode();
780      if (t.equals("Address") || t.equals("Reference"))
781        return true;
782    }
783    return false;
784  }
785
786  private void addColumnHeadings(XhtmlNode tr, List<ElementDefinition> grandChildren) {
787    for (ElementDefinition e : grandChildren)
788      tr.addTag("td").addTag("b").addText(Utilities.capitalize(tail(e.getPath())));
789  }
790
791  private void addColumnValues(ResourceWrapper res, XhtmlNode tr, List<ElementDefinition> grandChildren, BaseWrapper v, boolean showCodeDetails, Map<String, String> displayHints) throws FHIRException, UnsupportedEncodingException, IOException {
792    for (ElementDefinition e : grandChildren) {
793      PropertyWrapper p = v.getChildByName(e.getPath().substring(e.getPath().lastIndexOf(".")+1));
794      if (p == null || p.getValues().size() == 0 || p.getValues().get(0) == null)
795        tr.addTag("td").addText(" ");
796      else
797        renderLeaf(res, p.getValues().get(0), e, tr.addTag("td"), false, showCodeDetails, displayHints);
798    }
799  }
800
801  private String tail(String path) {
802    return path.substring(path.lastIndexOf(".")+1);
803  }
804
805  private boolean canDoTable(String path, PropertyWrapper p, List<ElementDefinition> grandChildren) {
806    for (ElementDefinition e : grandChildren) {
807      List<PropertyWrapper> values = getValues(path, p, e);
808      if (values.size() > 1 || !isPrimitive(e) || !canCollapse(e))
809        return false;
810    }
811    return true;
812  }
813
814  private List<PropertyWrapper> getValues(String path, PropertyWrapper p, ElementDefinition e) {
815    List<PropertyWrapper> res = new ArrayList<PropertyWrapper>();
816    for (BaseWrapper v : p.getValues()) {
817      for (PropertyWrapper g : v.children()) {
818        if ((path+"."+p.getName()+"."+g.getName()).equals(e.getPath()))
819          res.add(p);
820      }
821    }
822    return res;
823  }
824
825  private boolean canCollapse(ElementDefinition e) {
826    // we can collapse any data type
827    return !e.getType().isEmpty();
828  }
829
830  private boolean isPrimitive(ElementDefinition e) {
831    //we can tell if e is a primitive because it has types
832    if (e.getType().isEmpty())
833      return false;
834    if (e.getType().size() == 1 && isBase(e.getType().get(0).getCode()))
835      return false;
836    return true;
837//    return !e.getType().isEmpty()
838  }
839
840  private boolean isBase(String code) {
841    return code.equals("Element") || code.equals("BackboneElement");
842  }
843
844  private ElementDefinition getElementDefinition(List<ElementDefinition> elements, String path, PropertyWrapper p) {
845    for (ElementDefinition element : elements)
846      if (element.getPath().equals(path))
847        return element;
848    if (path.endsWith("\"]") && p.getStructure() != null)
849      return p.getStructure().getSnapshot().getElement().get(0);
850    return null;
851  }
852
853  private void renderLeaf(ResourceWrapper res, BaseWrapper ew, ElementDefinition defn, XhtmlNode x, boolean title, boolean showCodeDetails, Map<String, String> displayHints) throws FHIRException, UnsupportedEncodingException, IOException {
854    if (ew == null)
855      return;
856
857    Base e = ew.getBase();
858
859    if (e instanceof StringType)
860      x.addText(((StringType) e).getValue());
861    else if (e instanceof CodeType)
862      x.addText(((CodeType) e).getValue());
863    else if (e instanceof IdType)
864      x.addText(((IdType) e).getValue());
865    else if (e instanceof Extension)
866      x.addText("Extensions: Todo");
867    else if (e instanceof InstantType)
868      x.addText(((InstantType) e).toHumanDisplay());
869    else if (e instanceof DateTimeType)
870      x.addText(((DateTimeType) e).toHumanDisplay());
871    else if (e instanceof Base64BinaryType)
872      x.addText(new Base64().encodeAsString(((Base64BinaryType) e).getValue()));
873    else if (e instanceof org.hl7.fhir.dstu2.model.DateType)
874      x.addText(((org.hl7.fhir.dstu2.model.DateType) e).toHumanDisplay());
875    else if (e instanceof Enumeration) {
876      Object ev = ((Enumeration<?>) e).getValue();
877                        x.addText(ev == null ? "" : ev.toString()); // todo: look up a display name if there is one
878    } else if (e instanceof BooleanType)
879      x.addText(((BooleanType) e).getValue().toString());
880    else if (e instanceof CodeableConcept) {
881      renderCodeableConcept((CodeableConcept) e, x, showCodeDetails);
882    } else if (e instanceof Coding) {
883      renderCoding((Coding) e, x, showCodeDetails);
884    } else if (e instanceof Annotation) {
885      renderAnnotation((Annotation) e, x);
886    } else if (e instanceof Identifier) {
887      renderIdentifier((Identifier) e, x);
888    } else if (e instanceof org.hl7.fhir.dstu2.model.IntegerType) {
889      x.addText(Integer.toString(((org.hl7.fhir.dstu2.model.IntegerType) e).getValue()));
890    } else if (e instanceof org.hl7.fhir.dstu2.model.DecimalType) {
891      x.addText(((org.hl7.fhir.dstu2.model.DecimalType) e).getValue().toString());
892    } else if (e instanceof HumanName) {
893      renderHumanName((HumanName) e, x);
894    } else if (e instanceof SampledData) {
895      renderSampledData((SampledData) e, x);
896    } else if (e instanceof Address) {
897      renderAddress((Address) e, x);
898    } else if (e instanceof ContactPoint) {
899      renderContactPoint((ContactPoint) e, x);
900    } else if (e instanceof UriType) {
901      renderUri((UriType) e, x);
902    } else if (e instanceof Timing) {
903      renderTiming((Timing) e, x);
904    } else if (e instanceof Range) {
905      renderRange((Range) e, x);
906    } else if (e instanceof Quantity) {
907      renderQuantity((Quantity) e, x, showCodeDetails);
908    } else if (e instanceof Ratio) {
909      renderQuantity(((Ratio) e).getNumerator(), x, showCodeDetails);
910      x.addText("/");
911      renderQuantity(((Ratio) e).getDenominator(), x, showCodeDetails);
912    } else if (e instanceof Period) {
913      Period p = (Period) e;
914      x.addText(!p.hasStart() ? "??" : p.getStartElement().toHumanDisplay());
915      x.addText(" --> ");
916      x.addText(!p.hasEnd() ? "(ongoing)" : p.getEndElement().toHumanDisplay());
917    } else if (e instanceof Reference) {
918      Reference r = (Reference) e;
919      XhtmlNode c = x;
920      ResourceWithReference tr = null;
921      if (r.hasReferenceElement()) {
922        tr = resolveReference(res, r.getReference());
923        if (!r.getReference().startsWith("#")) {
924          if (tr != null && tr.getReference() != null)
925            c = x.addTag("a").attribute("href", tr.getReference());
926          else
927            c = x.addTag("a").attribute("href", r.getReference());
928        }
929      }
930      // what to display: if text is provided, then that. if the reference was resolved, then show the generated narrative
931      if (r.hasDisplayElement()) {
932        c.addText(r.getDisplay());
933        if (tr != null) {
934          c.addText(". Generated Summary: ");
935          generateResourceSummary(c, tr.getResource(), true, r.getReference().startsWith("#"));
936        }
937      } else if (tr != null) {
938        generateResourceSummary(c, tr.getResource(), r.getReference().startsWith("#"), r.getReference().startsWith("#"));
939      } else {
940        c.addText(r.getReference());
941      }
942    } else if (e instanceof Resource) {
943      return;
944    } else if (e instanceof ElementDefinition) {
945      x.addText("todo-bundle");
946    } else if (e != null && !(e instanceof Attachment) && !(e instanceof Narrative) && !(e instanceof Meta))
947      throw new NotImplementedException("type "+e.getClass().getName()+" not handled yet");
948  }
949
950  private boolean displayLeaf(ResourceWrapper res, BaseWrapper ew, ElementDefinition defn, XhtmlNode x, String name, boolean showCodeDetails) throws FHIRException, UnsupportedEncodingException, IOException {
951    if (ew == null)
952      return false;
953    Base e = ew.getBase();
954    Map<String, String> displayHints = readDisplayHints(defn);
955
956    if (name.endsWith("[x]"))
957      name = name.substring(0, name.length() - 3);
958
959    if (!showCodeDetails && e instanceof PrimitiveType && isDefault(displayHints, ((PrimitiveType) e)))
960        return false;
961
962    if (e instanceof StringType) {
963      x.addText(name+": "+((StringType) e).getValue());
964      return true;
965    } else if (e instanceof CodeType) {
966      x.addText(name+": "+((CodeType) e).getValue());
967      return true;
968    } else if (e instanceof IdType) {
969      x.addText(name+": "+((IdType) e).getValue());
970      return true;
971    } else if (e instanceof DateTimeType) {
972      x.addText(name+": "+((DateTimeType) e).toHumanDisplay());
973      return true;
974    } else if (e instanceof InstantType) {
975      x.addText(name+": "+((InstantType) e).toHumanDisplay());
976      return true;
977    } else if (e instanceof Extension) {
978      x.addText("Extensions: todo");
979      return true;
980    } else if (e instanceof org.hl7.fhir.dstu2.model.DateType) {
981      x.addText(name+": "+((org.hl7.fhir.dstu2.model.DateType) e).toHumanDisplay());
982      return true;
983    } else if (e instanceof Enumeration) {
984      x.addText(((Enumeration<?>) e).getValue().toString()); // todo: look up a display name if there is one
985      return true;
986    } else if (e instanceof BooleanType) {
987      if (((BooleanType) e).getValue()) {
988        x.addText(name);
989          return true;
990      }
991    } else if (e instanceof CodeableConcept) {
992      renderCodeableConcept((CodeableConcept) e, x, showCodeDetails);
993      return true;
994    } else if (e instanceof Coding) {
995      renderCoding((Coding) e, x, showCodeDetails);
996      return true;
997    } else if (e instanceof Annotation) {
998      renderAnnotation((Annotation) e, x, showCodeDetails);
999      return true;
1000    } else if (e instanceof org.hl7.fhir.dstu2.model.IntegerType) {
1001      x.addText(Integer.toString(((org.hl7.fhir.dstu2.model.IntegerType) e).getValue()));
1002      return true;
1003    } else if (e instanceof org.hl7.fhir.dstu2.model.DecimalType) {
1004      x.addText(((org.hl7.fhir.dstu2.model.DecimalType) e).getValue().toString());
1005      return true;
1006    } else if (e instanceof Identifier) {
1007      renderIdentifier((Identifier) e, x);
1008      return true;
1009    } else if (e instanceof HumanName) {
1010      renderHumanName((HumanName) e, x);
1011      return true;
1012    } else if (e instanceof SampledData) {
1013      renderSampledData((SampledData) e, x);
1014      return true;
1015    } else if (e instanceof Address) {
1016      renderAddress((Address) e, x);
1017      return true;
1018    } else if (e instanceof ContactPoint) {
1019      renderContactPoint((ContactPoint) e, x);
1020      return true;
1021    } else if (e instanceof Timing) {
1022      renderTiming((Timing) e, x);
1023      return true;
1024    } else if (e instanceof Quantity) {
1025      renderQuantity((Quantity) e, x, showCodeDetails);
1026      return true;
1027    } else if (e instanceof Ratio) {
1028      renderQuantity(((Ratio) e).getNumerator(), x, showCodeDetails);
1029      x.addText("/");
1030      renderQuantity(((Ratio) e).getDenominator(), x, showCodeDetails);
1031      return true;
1032    } else if (e instanceof Period) {
1033      Period p = (Period) e;
1034      x.addText(name+": ");
1035      x.addText(!p.hasStart() ? "??" : p.getStartElement().toHumanDisplay());
1036      x.addText(" --> ");
1037      x.addText(!p.hasEnd() ? "(ongoing)" : p.getEndElement().toHumanDisplay());
1038      return true;
1039    } else if (e instanceof Reference) {
1040      Reference r = (Reference) e;
1041      if (r.hasDisplayElement())
1042        x.addText(r.getDisplay());
1043      else if (r.hasReferenceElement()) {
1044        ResourceWithReference tr = resolveReference(res, r.getReference());
1045        x.addText(tr == null ? r.getReference() : "????"); // getDisplayForReference(tr.getReference()));
1046      } else
1047        x.addText("??");
1048      return true;
1049    } else if (e instanceof Narrative) {
1050      return false;
1051    } else if (e instanceof Resource) {
1052      return false;
1053    } else if (!(e instanceof Attachment))
1054      throw new NotImplementedException("type "+e.getClass().getName()+" not handled yet");
1055    return false;
1056  }
1057
1058
1059  private Map<String, String> readDisplayHints(ElementDefinition defn) throws DefinitionException {
1060    Map<String, String> hints = new HashMap<String, String>();
1061    if (defn != null) {
1062      String displayHint = ToolingExtensions.getDisplayHint(defn);
1063      if (!Utilities.noString(displayHint)) {
1064        String[] list = displayHint.split(";");
1065        for (String item : list) {
1066          String[] parts = item.split(":");
1067          if (parts.length != 2)
1068            throw new DefinitionException("error reading display hint: '"+displayHint+"'");
1069          hints.put(parts[0].trim(), parts[1].trim());
1070        }
1071      }
1072    }
1073    return hints;
1074  }
1075
1076  public static String displayPeriod(Period p) {
1077    String s = !p.hasStart() ? "??" : p.getStartElement().toHumanDisplay();
1078    s = s + " --> ";
1079    return s + (!p.hasEnd() ? "(ongoing)" : p.getEndElement().toHumanDisplay());
1080  }
1081
1082  private void generateResourceSummary(XhtmlNode x, ResourceWrapper res, boolean textAlready, boolean showCodeDetails) throws FHIRException, UnsupportedEncodingException, IOException {
1083    if (!textAlready) {
1084      XhtmlNode div = res.getNarrative();
1085      if (div != null) {
1086        if (div.allChildrenAreText())
1087          x.getChildNodes().addAll(div.getChildNodes());
1088        if (div.getChildNodes().size() == 1 && div.getChildNodes().get(0).allChildrenAreText())
1089          x.getChildNodes().addAll(div.getChildNodes().get(0).getChildNodes());
1090      }
1091      x.addText("Generated Summary: ");
1092    }
1093    String path = res.getName();
1094    StructureDefinition profile = context.fetchResource(StructureDefinition.class, path);
1095    if (profile == null)
1096      x.addText("unknown resource " +path);
1097    else {
1098      boolean firstElement = true;
1099      boolean last = false;
1100      for (PropertyWrapper p : res.children()) {
1101        ElementDefinition child = getElementDefinition(profile.getSnapshot().getElement(), path+"."+p.getName(), p);
1102        if (p.getValues().size() > 0 && p.getValues().get(0) != null && child != null && isPrimitive(child) && includeInSummary(child)) {
1103          if (firstElement)
1104            firstElement = false;
1105          else if (last)
1106            x.addText("; ");
1107          boolean first = true;
1108          last = false;
1109          for (BaseWrapper v : p.getValues()) {
1110            if (first)
1111              first = false;
1112            else if (last)
1113              x.addText(", ");
1114            last = displayLeaf(res, v, child, x, p.getName(), showCodeDetails) || last;
1115          }
1116        }
1117      }
1118    }
1119  }
1120
1121
1122  private boolean includeInSummary(ElementDefinition child) {
1123    if (child.getIsModifier())
1124      return true;
1125    if (child.getMustSupport())
1126      return true;
1127    if (child.getType().size() == 1) {
1128      String t = child.getType().get(0).getCode();
1129      if (t.equals("Address") || t.equals("Contact") || t.equals("Reference") || t.equals("Uri"))
1130        return false;
1131    }
1132    return true;
1133  }
1134
1135  private ResourceWithReference resolveReference(ResourceWrapper res, String url) {
1136    if (url == null)
1137      return null;
1138    if (url.startsWith("#")) {
1139      for (ResourceWrapper r : res.getContained()) {
1140        if (r.getId().equals(url.substring(1)))
1141          return new ResourceWithReference(null, r);
1142      }
1143      return null;
1144    }
1145
1146    Resource ae = context.fetchResource(null, url);
1147    if (ae == null)
1148      return null;
1149    else
1150      return new ResourceWithReference(url, new ResourceWrapperDirect(ae));
1151  }
1152
1153  private void renderCodeableConcept(CodeableConcept cc, XhtmlNode x, boolean showCodeDetails) {
1154    String s = cc.getText();
1155    if (Utilities.noString(s)) {
1156      for (Coding c : cc.getCoding()) {
1157        if (c.hasDisplayElement()) {
1158          s = c.getDisplay();
1159          break;
1160        }
1161      }
1162    }
1163    if (Utilities.noString(s)) {
1164      // still? ok, let's try looking it up
1165      for (Coding c : cc.getCoding()) {
1166        if (c.hasCodeElement() && c.hasSystemElement()) {
1167          s = lookupCode(c.getSystem(), c.getCode());
1168          if (!Utilities.noString(s))
1169            break;
1170        }
1171      }
1172    }
1173
1174    if (Utilities.noString(s)) {
1175      if (cc.getCoding().isEmpty())
1176        s = "";
1177      else
1178        s = cc.getCoding().get(0).getCode();
1179    }
1180
1181    if (showCodeDetails) {
1182      x.addText(s+" ");
1183      XhtmlNode sp = x.addTag("span");
1184      sp.setAttribute("style", "background: LightGoldenRodYellow ");
1185      sp.addText("(Details ");
1186      boolean first = true;
1187      for (Coding c : cc.getCoding()) {
1188        if (first) {
1189          sp.addText(": ");
1190          first = false;
1191        } else
1192          sp.addText("; ");
1193        sp.addText("{"+describeSystem(c.getSystem())+" code '"+c.getCode()+"' = '"+lookupCode(c.getSystem(), c.getCode())+(c.hasDisplay() ? "', given as '"+c.getDisplay()+"'}" : ""));
1194      }
1195      sp.addText(")");
1196    } else {
1197
1198    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1199    for (Coding c : cc.getCoding()) {
1200      if (c.hasCodeElement() && c.hasSystemElement()) {
1201        b.append("{"+c.getSystem()+" "+c.getCode()+"}");
1202      }
1203    }
1204
1205    x.addTag("span").setAttribute("title", "Codes: "+b.toString()).addText(s);
1206    }
1207  }
1208
1209  private void renderAnnotation(Annotation a, XhtmlNode x, boolean showCodeDetails) throws FHIRException {
1210    StringBuilder s = new StringBuilder();
1211    if (a.hasAuthor()) {
1212      s.append("Author: ");
1213
1214      if (a.hasAuthorReference())
1215        s.append(a.getAuthorReference().getReference());
1216      else if (a.hasAuthorStringType())
1217        s.append(a.getAuthorStringType().getValue());
1218    }
1219
1220
1221    if (a.hasTimeElement()) {
1222      if (s.length() > 0)
1223        s.append("; ");
1224
1225      s.append("Made: ").append(a.getTimeElement().toHumanDisplay());
1226    }
1227
1228    if (a.hasText()) {
1229      if (s.length() > 0)
1230        s.append("; ");
1231
1232      s.append("Annotation: ").append(a.getText());
1233    }
1234
1235    x.addText(s.toString());
1236  }
1237
1238  private void renderCoding(Coding c, XhtmlNode x, boolean showCodeDetails) {
1239    String s = "";
1240    if (c.hasDisplayElement())
1241      s = c.getDisplay();
1242    if (Utilities.noString(s))
1243      s = lookupCode(c.getSystem(), c.getCode());
1244
1245    if (Utilities.noString(s))
1246      s = c.getCode();
1247
1248    if (showCodeDetails) {
1249      x.addText(s+" (Details: "+describeSystem(c.getSystem())+" code "+c.getCode()+" = '"+lookupCode(c.getSystem(), c.getCode())+"', stated as '"+c.getDisplay()+"')");
1250    } else
1251      x.addTag("span").setAttribute("title", "{"+c.getSystem()+" "+c.getCode()+"}").addText(s);
1252  }
1253
1254  private String describeSystem(String system) {
1255    if (system == null)
1256      return "[not stated]";
1257    if (system.equals("http://loinc.org"))
1258      return "LOINC";
1259    if (system.startsWith("http://snomed.info"))
1260      return "SNOMED CT";
1261    if (system.equals("http://www.nlm.nih.gov/research/umls/rxnorm"))
1262      return "RxNorm";
1263    if (system.equals("http://hl7.org/fhir/sid/icd-9"))
1264      return "ICD-9";
1265
1266    return system;
1267  }
1268
1269  private String lookupCode(String system, String code) {
1270    ValidationResult t = context.validateCode(system, code, null);
1271
1272    if (t != null && t.getDisplay() != null)
1273        return t.getDisplay();
1274    else
1275      return code;
1276
1277  }
1278
1279  private ConceptDefinitionComponent findCode(String code, List<ConceptDefinitionComponent> list) {
1280    for (ConceptDefinitionComponent t : list) {
1281      if (code.equals(t.getCode()))
1282        return t;
1283      ConceptDefinitionComponent c = findCode(code, t.getConcept());
1284      if (c != null)
1285        return c;
1286    }
1287    return null;
1288  }
1289
1290  private String displayCodeableConcept(CodeableConcept cc) {
1291    String s = cc.getText();
1292    if (Utilities.noString(s)) {
1293      for (Coding c : cc.getCoding()) {
1294        if (c.hasDisplayElement()) {
1295          s = c.getDisplay();
1296          break;
1297        }
1298      }
1299    }
1300    if (Utilities.noString(s)) {
1301      // still? ok, let's try looking it up
1302      for (Coding c : cc.getCoding()) {
1303        if (c.hasCode() && c.hasSystem()) {
1304          s = lookupCode(c.getSystem(), c.getCode());
1305          if (!Utilities.noString(s))
1306            break;
1307        }
1308      }
1309    }
1310
1311    if (Utilities.noString(s)) {
1312      if (cc.getCoding().isEmpty())
1313        s = "";
1314      else
1315        s = cc.getCoding().get(0).getCode();
1316    }
1317    return s;
1318  }
1319
1320  private void renderIdentifier(Identifier ii, XhtmlNode x) {
1321    x.addText(displayIdentifier(ii));
1322  }
1323
1324  private void renderTiming(Timing s, XhtmlNode x) throws FHIRException {
1325    x.addText(displayTiming(s));
1326  }
1327
1328  private void renderQuantity(Quantity q, XhtmlNode x, boolean showCodeDetails) {
1329    if (q.hasComparator())
1330      x.addText(q.getComparator().toCode());
1331    x.addText(q.getValue().toString());
1332    if (q.hasUnit())
1333      x.addText(" "+q.getUnit());
1334    else if (q.hasCode())
1335      x.addText(" "+q.getCode());
1336    if (showCodeDetails && q.hasCode()) {
1337      XhtmlNode sp = x.addTag("span");
1338      sp.setAttribute("style", "background: LightGoldenRodYellow ");
1339      sp.addText(" (Details: "+describeSystem(q.getSystem())+" code "+q.getCode()+" = '"+lookupCode(q.getSystem(), q.getCode())+"')");
1340    }
1341  }
1342
1343  private void renderRange(Range q, XhtmlNode x) {
1344    if (q.hasLow())
1345      x.addText(q.getLow().getValue().toString());
1346    else
1347      x.addText("?");
1348    x.addText("-");
1349    if (q.hasHigh())
1350      x.addText(q.getHigh().getValue().toString());
1351    else
1352      x.addText("?");
1353    if (q.getLow().hasUnit())
1354      x.addText(" "+q.getLow().getUnit());
1355  }
1356
1357  private void renderHumanName(HumanName name, XhtmlNode x) {
1358    x.addText(displayHumanName(name));
1359  }
1360
1361  private void renderAnnotation(Annotation annot, XhtmlNode x) {
1362    x.addText(annot.getText());
1363  }
1364
1365  private void renderAddress(Address address, XhtmlNode x) {
1366    x.addText(displayAddress(address));
1367  }
1368
1369  private void renderContactPoint(ContactPoint contact, XhtmlNode x) {
1370    x.addText(displayContactPoint(contact));
1371  }
1372
1373  private void renderUri(UriType uri, XhtmlNode x) {
1374    x.addTag("a").setAttribute("href", uri.getValue()).addText(uri.getValue());
1375  }
1376
1377  private void renderSampledData(SampledData sampledData, XhtmlNode x) {
1378    x.addText(displaySampledData(sampledData));
1379  }
1380
1381  private String displaySampledData(SampledData s) {
1382    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1383    if (s.hasOrigin())
1384      b.append("Origin: "+displayQuantity(s.getOrigin()));
1385
1386    if (s.hasPeriod())
1387      b.append("Period: "+s.getPeriod().toString());
1388
1389    if (s.hasFactor())
1390      b.append("Factor: "+s.getFactor().toString());
1391
1392    if (s.hasLowerLimit())
1393      b.append("Lower: "+s.getLowerLimit().toString());
1394
1395    if (s.hasUpperLimit())
1396      b.append("Upper: "+s.getUpperLimit().toString());
1397
1398    if (s.hasDimensions())
1399      b.append("Dimensions: "+s.getDimensions());
1400
1401    if (s.hasData())
1402      b.append("Data: "+s.getData());
1403
1404    return b.toString();
1405  }
1406
1407  private String displayQuantity(Quantity q) {
1408    StringBuilder s = new StringBuilder();
1409
1410    s.append("(system = '").append(describeSystem(q.getSystem()))
1411        .append("' code ").append(q.getCode())
1412        .append(" = '").append(lookupCode(q.getSystem(), q.getCode())).append("')");
1413
1414    return s.toString();
1415  }
1416
1417  private String displayTiming(Timing s) throws FHIRException {
1418    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1419    if (s.hasCode())
1420        b.append("Code: "+displayCodeableConcept(s.getCode()));
1421
1422    if (s.getEvent().size() > 0) {
1423      CommaSeparatedStringBuilder c = new CommaSeparatedStringBuilder();
1424      for (DateTimeType p : s.getEvent()) {
1425        c.append(p.toHumanDisplay());
1426      }
1427      b.append("Events: "+ c.toString());
1428    }
1429
1430    if (s.hasRepeat()) {
1431      TimingRepeatComponent rep = s.getRepeat();
1432      if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasStart())
1433        b.append("Starting "+rep.getBoundsPeriod().getStartElement().toHumanDisplay());
1434      if (rep.hasCount())
1435        b.append("Count "+Integer.toString(rep.getCount())+" times");
1436      if (rep.hasDuration())
1437        b.append("Duration "+rep.getDuration().toPlainString()+displayTimeUnits(rep.getPeriodUnits()));
1438
1439      if (rep.hasWhen()) {
1440        String st = "";
1441        if (rep.hasPeriod()) {
1442          st = rep.getPeriod().toPlainString();
1443          if (rep.hasPeriodMax())
1444            st = st + "-"+rep.getPeriodMax().toPlainString();
1445          st = st + displayTimeUnits(rep.getPeriodUnits());
1446        }
1447        b.append("Do "+st+displayEventCode(rep.getWhen()));
1448      } else {
1449        String st = "";
1450        if (!rep.hasFrequency() || (!rep.hasFrequencyMax() && rep.getFrequency() == 1) )
1451          st = "Once";
1452        else {
1453          st = Integer.toString(rep.getFrequency());
1454          if (rep.hasFrequencyMax())
1455            st = st + "-"+Integer.toString(rep.getFrequency());
1456        }
1457        if (rep.hasPeriod()) {
1458        st = st + " per "+rep.getPeriod().toPlainString();
1459        if (rep.hasPeriodMax())
1460          st = st + "-"+rep.getPeriodMax().toPlainString();
1461                st = st + " "+displayTimeUnits(rep.getPeriodUnits());
1462        }
1463        b.append("Do "+st);
1464      }
1465      if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasEnd())
1466        b.append("Until "+rep.getBoundsPeriod().getEndElement().toHumanDisplay());
1467    }
1468    return b.toString();
1469  }
1470
1471  private Object displayEventCode(EventTiming when) {
1472    switch (when) {
1473    case C: return "at meals";
1474    case CD: return "at lunch";
1475    case CM: return "at breakfast";
1476    case CV: return "at dinner";
1477    case AC: return "before meals";
1478    case ACD: return "before lunch";
1479    case ACM: return "before breakfast";
1480    case ACV: return "before dinner";
1481    case HS: return "before sleeping";
1482    case PC: return "after meals";
1483    case PCD: return "after lunch";
1484    case PCM: return "after breakfast";
1485    case PCV: return "after dinner";
1486    case WAKE: return "after waking";
1487    default: return "??";
1488    }
1489  }
1490
1491  private String displayTimeUnits(UnitsOfTime units) {
1492        if (units == null)
1493                return "??";
1494    switch (units) {
1495    case A: return "years";
1496    case D: return "days";
1497    case H: return "hours";
1498    case MIN: return "minutes";
1499    case MO: return "months";
1500    case S: return "seconds";
1501    case WK: return "weeks";
1502    default: return "??";
1503    }
1504  }
1505
1506  public static String displayHumanName(HumanName name) {
1507    StringBuilder s = new StringBuilder();
1508    if (name.hasText())
1509      s.append(name.getText());
1510    else {
1511      for (StringType p : name.getGiven()) {
1512        s.append(p.getValue());
1513        s.append(" ");
1514      }
1515      for (StringType p : name.getFamily()) {
1516        s.append(p.getValue());
1517        s.append(" ");
1518      }
1519    }
1520    if (name.hasUse() && name.getUse() != NameUse.USUAL)
1521      s.append("("+name.getUse().toString()+")");
1522    return s.toString();
1523  }
1524
1525  private String displayAddress(Address address) {
1526    StringBuilder s = new StringBuilder();
1527    if (address.hasText())
1528      s.append(address.getText());
1529    else {
1530      for (StringType p : address.getLine()) {
1531        s.append(p.getValue());
1532        s.append(" ");
1533      }
1534      if (address.hasCity()) {
1535        s.append(address.getCity());
1536        s.append(" ");
1537      }
1538      if (address.hasState()) {
1539        s.append(address.getState());
1540        s.append(" ");
1541      }
1542
1543      if (address.hasPostalCode()) {
1544        s.append(address.getPostalCode());
1545        s.append(" ");
1546      }
1547
1548      if (address.hasCountry()) {
1549        s.append(address.getCountry());
1550        s.append(" ");
1551      }
1552    }
1553    if (address.hasUse())
1554      s.append("("+address.getUse().toString()+")");
1555    return s.toString();
1556  }
1557
1558  public static String displayContactPoint(ContactPoint contact) {
1559    StringBuilder s = new StringBuilder();
1560    s.append(describeSystem(contact.getSystem()));
1561    if (Utilities.noString(contact.getValue()))
1562      s.append("-unknown-");
1563    else
1564      s.append(contact.getValue());
1565    if (contact.hasUse())
1566      s.append("("+contact.getUse().toString()+")");
1567    return s.toString();
1568  }
1569
1570  private static String describeSystem(ContactPointSystem system) {
1571    if (system == null)
1572      return "";
1573    switch (system) {
1574    case PHONE: return "ph: ";
1575    case FAX: return "fax: ";
1576    default:
1577      return "";
1578    }
1579  }
1580
1581  private String displayIdentifier(Identifier ii) {
1582    String s = Utilities.noString(ii.getValue()) ? "??" : ii.getValue();
1583
1584    if (ii.hasType()) {
1585        if (ii.getType().hasText())
1586                s = ii.getType().getText()+" = "+s;
1587        else if (ii.getType().hasCoding() && ii.getType().getCoding().get(0).hasDisplay())
1588                s = ii.getType().getCoding().get(0).getDisplay()+" = "+s;
1589        else if (ii.getType().hasCoding() && ii.getType().getCoding().get(0).hasCode())
1590                s = lookupCode(ii.getType().getCoding().get(0).getSystem(), ii.getType().getCoding().get(0).getCode())+" = "+s;
1591    }
1592
1593    if (ii.hasUse())
1594      s = s + " ("+ii.getUse().toString()+")";
1595    return s;
1596  }
1597
1598  private List<ElementDefinition> getChildrenForPath(List<ElementDefinition> elements, String path) throws DefinitionException {
1599    // do we need to do a name reference substitution?
1600    for (ElementDefinition e : elements) {
1601      if (e.getPath().equals(path) && e.hasNameReference()) {
1602        String name = e.getNameReference();
1603        ElementDefinition t = null;
1604        // now, resolve the name
1605        for (ElementDefinition e1 : elements) {
1606                if (name.equals(e1.getName()))
1607                        t = e1;
1608        }
1609        if (t == null)
1610                throw new DefinitionException("Unable to resolve name reference "+name+" trying to resolve "+path);
1611        path = t.getPath();
1612        break;
1613      }
1614    }
1615
1616    List<ElementDefinition> results = new ArrayList<ElementDefinition>();
1617    for (ElementDefinition e : elements) {
1618      if (e.getPath().startsWith(path+".") && !e.getPath().substring(path.length()+1).contains("."))
1619        results.add(e);
1620    }
1621    return results;
1622  }
1623
1624
1625  public void generate(ConceptMap cm) {
1626    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
1627    x.addTag("h2").addText(cm.getName()+" ("+cm.getUrl()+")");
1628
1629    XhtmlNode p = x.addTag("p");
1630    p.addText("Mapping from ");
1631    AddVsRef(((Reference) cm.getSource()).getReference(), p);
1632    p.addText(" to ");
1633    AddVsRef(((Reference) cm.getTarget()).getReference(), p);
1634
1635    p = x.addTag("p");
1636    if (cm.getExperimental())
1637      p.addText(Utilities.capitalize(cm.getStatus().toString())+" (not intended for production usage). ");
1638    else
1639      p.addText(Utilities.capitalize(cm.getStatus().toString())+". ");
1640    p.addText("Published on "+cm.getDateElement().toHumanDisplay()+" by "+cm.getPublisher());
1641    if (!cm.getContact().isEmpty()) {
1642      p.addText(" (");
1643      boolean firsti = true;
1644      for (ConceptMapContactComponent ci : cm.getContact()) {
1645        if (firsti)
1646          firsti = false;
1647        else
1648          p.addText(", ");
1649        if (ci.hasName())
1650          p.addText(ci.getName()+": ");
1651        boolean first = true;
1652        for (ContactPoint c : ci.getTelecom()) {
1653          if (first)
1654            first = false;
1655          else
1656            p.addText(", ");
1657          addTelecom(p, c);
1658        }
1659        p.addText("; ");
1660      }
1661      p.addText(")");
1662    }
1663    p.addText(". ");
1664    p.addText(cm.getCopyright());
1665    if (!Utilities.noString(cm.getDescription()))
1666      x.addTag("p").addText(cm.getDescription());
1667
1668    x.addTag("br");
1669
1670    if (!cm.getElement().isEmpty()) {
1671      SourceElementComponent cc = cm.getElement().get(0);
1672      String src = cc.getCodeSystem();
1673      boolean comments = false;
1674      boolean ok = cc.getTarget().size() == 1;
1675      Map<String, HashSet<String>> sources = new HashMap<String, HashSet<String>>();
1676      sources.put("code", new HashSet<String>());
1677      Map<String, HashSet<String>> targets = new HashMap<String, HashSet<String>>();
1678      targets.put("code", new HashSet<String>());
1679      if (ok) {
1680        String dst = cc.getTarget().get(0).getCodeSystem();
1681        for (SourceElementComponent ccl : cm.getElement()) {
1682          ok = ok && src.equals(ccl.getCodeSystem()) && ccl.getTarget().size() == 1 && dst.equals(ccl.getTarget().get(0).getCodeSystem()) && ccl.getTarget().get(0).getDependsOn().isEmpty() && ccl.getTarget().get(0).getProduct().isEmpty();
1683          if (ccl.hasCodeSystem())
1684            sources.get("code").add(ccl.getCodeSystem());
1685          for (TargetElementComponent ccm : ccl.getTarget()) {
1686            comments = comments || !Utilities.noString(ccm.getComments());
1687            for (OtherElementComponent d : ccm.getDependsOn()) {
1688            if (!sources.containsKey(d.getElement()))
1689              sources.put(d.getElement(), new HashSet<String>());
1690            sources.get(d.getElement()).add(d.getCodeSystem());
1691          }
1692            if (ccm.hasCodeSystem())
1693              targets.get("code").add(ccm.getCodeSystem());
1694            for (OtherElementComponent d : ccm.getProduct()) {
1695              if (!targets.containsKey(d.getElement()))
1696                targets.put(d.getElement(), new HashSet<String>());
1697              targets.get(d.getElement()).add(d.getCodeSystem());
1698            }
1699
1700          }
1701        }
1702      }
1703
1704      String display;
1705      if (ok) {
1706        // simple
1707        XhtmlNode tbl = x.addTag("table").setAttribute("class", "grid");
1708        XhtmlNode tr = tbl.addTag("tr");
1709        tr.addTag("td").addTag("b").addText("Source Code");
1710        tr.addTag("td").addTag("b").addText("Equivalence");
1711        tr.addTag("td").addTag("b").addText("Destination Code");
1712        if (comments)
1713          tr.addTag("td").addTag("b").addText("Comments");
1714        for (SourceElementComponent ccl : cm.getElement()) {
1715          tr = tbl.addTag("tr");
1716          XhtmlNode td = tr.addTag("td");
1717          td.addText(ccl.getCode());
1718          display = getDisplayForConcept(ccl.getCodeSystem(), ccl.getCode());
1719          if (display != null)
1720            td.addText(" ("+display+")");
1721          TargetElementComponent ccm = ccl.getTarget().get(0);
1722          tr.addTag("td").addText(!ccm.hasEquivalence() ? "" : ccm.getEquivalence().toCode());
1723          td = tr.addTag("td");
1724          td.addText(ccm.getCode());
1725          display = getDisplayForConcept(ccm.getCodeSystem(), ccm.getCode());
1726          if (display != null)
1727            td.addText(" ("+display+")");
1728          if (comments)
1729            tr.addTag("td").addText(ccm.getComments());
1730        }
1731      } else {
1732        XhtmlNode tbl = x.addTag("table").setAttribute("class", "grid");
1733        XhtmlNode tr = tbl.addTag("tr");
1734        XhtmlNode td;
1735        tr.addTag("td").setAttribute("colspan", Integer.toString(sources.size())).addTag("b").addText("Source Concept");
1736        tr.addTag("td").addTag("b").addText("Equivalence");
1737        tr.addTag("td").setAttribute("colspan", Integer.toString(targets.size())).addTag("b").addText("Destination Concept");
1738        if (comments)
1739          tr.addTag("td").addTag("b").addText("Comments");
1740        tr = tbl.addTag("tr");
1741        if (sources.get("code").size() == 1)
1742          tr.addTag("td").addTag("b").addText("Code "+sources.get("code").toString()+"");
1743        else
1744          tr.addTag("td").addTag("b").addText("Code");
1745        for (String s : sources.keySet()) {
1746          if (!s.equals("code")) {
1747            if (sources.get(s).size() == 1)
1748              tr.addTag("td").addTag("b").addText(getDescForConcept(s) +" "+sources.get(s).toString());
1749            else
1750              tr.addTag("td").addTag("b").addText(getDescForConcept(s));
1751          }
1752        }
1753        tr.addTag("td");
1754        if (targets.get("code").size() == 1)
1755          tr.addTag("td").addTag("b").addText("Code "+targets.get("code").toString());
1756        else
1757          tr.addTag("td").addTag("b").addText("Code");
1758        for (String s : targets.keySet()) {
1759          if (!s.equals("code")) {
1760            if (targets.get(s).size() == 1)
1761              tr.addTag("td").addTag("b").addText(getDescForConcept(s) +" "+targets.get(s).toString()+"");
1762            else
1763              tr.addTag("td").addTag("b").addText(getDescForConcept(s));
1764          }
1765        }
1766        if (comments)
1767          tr.addTag("td");
1768
1769        for (SourceElementComponent ccl : cm.getElement()) {
1770          tr = tbl.addTag("tr");
1771          td = tr.addTag("td");
1772          if (sources.get("code").size() == 1)
1773            td.addText(ccl.getCode());
1774          else
1775            td.addText(ccl.getCodeSystem()+" / "+ccl.getCode());
1776          display = getDisplayForConcept(ccl.getCodeSystem(), ccl.getCode());
1777          if (display != null)
1778            td.addText(" ("+display+")");
1779
1780          TargetElementComponent ccm = ccl.getTarget().get(0);
1781          for (String s : sources.keySet()) {
1782            if (!s.equals("code")) {
1783              td = tr.addTag("td");
1784              td.addText(getCode(ccm.getDependsOn(), s, sources.get(s).size() != 1));
1785              display = getDisplay(ccm.getDependsOn(), s);
1786              if (display != null)
1787                td.addText(" ("+display+")");
1788            }
1789          }
1790          tr.addTag("td").addText(ccm.getEquivalence().toString());
1791          td = tr.addTag("td");
1792          if (targets.get("code").size() == 1)
1793            td.addText(ccm.getCode());
1794          else
1795            td.addText(ccm.getCodeSystem()+" / "+ccm.getCode());
1796          display = getDisplayForConcept(ccm.getCodeSystem(), ccm.getCode());
1797          if (display != null)
1798            td.addText(" ("+display+")");
1799
1800          for (String s : targets.keySet()) {
1801            if (!s.equals("code")) {
1802              td = tr.addTag("td");
1803              td.addText(getCode(ccm.getProduct(), s, targets.get(s).size() != 1));
1804              display = getDisplay(ccm.getProduct(), s);
1805              if (display != null)
1806                td.addText(" ("+display+")");
1807            }
1808          }
1809          if (comments)
1810            tr.addTag("td").addText(ccm.getComments());
1811        }
1812      }
1813    }
1814
1815    inject(cm, x, NarrativeStatus.GENERATED);
1816  }
1817
1818
1819
1820  private void inject(DomainResource r, XhtmlNode x, NarrativeStatus status) {
1821    if (!r.hasText() || !r.getText().hasDiv() || r.getText().getDiv().getChildNodes().isEmpty()) {
1822      r.setText(new Narrative());
1823      r.getText().setDiv(x);
1824      r.getText().setStatus(status);
1825    } else {
1826      XhtmlNode n = r.getText().getDiv();
1827      n.addTag("hr");
1828      n.getChildNodes().addAll(x.getChildNodes());
1829    }
1830  }
1831
1832  public Element getNarrative(Element er) {
1833    Element txt = XMLUtil.getNamedChild(er, "text");
1834    if (txt == null)
1835      return null;
1836    return XMLUtil.getNamedChild(txt, "div");
1837  }
1838
1839
1840  private void inject(Element er, XhtmlNode x, NarrativeStatus status) {
1841    Element txt = XMLUtil.getNamedChild(er, "text");
1842    if (txt == null) {
1843      txt = er.getOwnerDocument().createElementNS(FormatUtilities.FHIR_NS, "text");
1844      Element n = XMLUtil.getFirstChild(er);
1845      while (n != null && (n.getNodeName().equals("id") || n.getNodeName().equals("meta") || n.getNodeName().equals("implicitRules") || n.getNodeName().equals("language")))
1846        n = XMLUtil.getNextSibling(n);
1847      if (n == null)
1848        er.appendChild(txt);
1849      else
1850        er.insertBefore(txt, n);
1851    }
1852    Element st = XMLUtil.getNamedChild(txt, "status");
1853    if (st == null) {
1854      st = er.getOwnerDocument().createElementNS(FormatUtilities.FHIR_NS, "status");
1855      Element n = XMLUtil.getFirstChild(txt);
1856      if (n == null)
1857        txt.appendChild(st);
1858      else
1859        txt.insertBefore(st, n);
1860    }
1861    st.setAttribute("value", status.toCode());
1862    Element div = XMLUtil.getNamedChild(txt, "div");
1863    if (div == null) {
1864      div = er.getOwnerDocument().createElementNS(FormatUtilities.XHTML_NS, "div");
1865      div.setAttribute("xmlns", FormatUtilities.XHTML_NS);
1866      txt.appendChild(div);
1867    }
1868    if (div.hasChildNodes())
1869      div.appendChild(er.getOwnerDocument().createElementNS(FormatUtilities.XHTML_NS, "hr"));
1870    new XhtmlComposer(true, pretty).compose(div, x);
1871  }
1872
1873  private String getDisplay(List<OtherElementComponent> list, String s) {
1874    for (OtherElementComponent c : list) {
1875      if (s.equals(c.getElement()))
1876        return getDisplayForConcept(c.getCodeSystem(), c.getCode());
1877    }
1878    return null;
1879  }
1880
1881  private String getDisplayForConcept(String system, String code) {
1882    if (code == null)
1883      return null;
1884    ValidationResult cl = context.validateCode(system, code, null);
1885    return cl == null ? null : cl.getDisplay();
1886  }
1887
1888
1889
1890  private String getDescForConcept(String s) {
1891    if (s.startsWith("http://hl7.org/fhir/v2/element/"))
1892        return "v2 "+s.substring("http://hl7.org/fhir/v2/element/".length());
1893    return s;
1894  }
1895
1896  private String getCode(List<OtherElementComponent> list, String s, boolean withSystem) {
1897    for (OtherElementComponent c : list) {
1898      if (s.equals(c.getElement()))
1899        if (withSystem)
1900          return c.getCodeSystem()+" / "+c.getCode();
1901        else
1902          return c.getCode();
1903    }
1904    return null;
1905  }
1906
1907  private void addTelecom(XhtmlNode p, ContactPoint c) {
1908    if (c.getSystem() == ContactPointSystem.PHONE) {
1909      p.addText("Phone: "+c.getValue());
1910    } else if (c.getSystem() == ContactPointSystem.FAX) {
1911      p.addText("Fax: "+c.getValue());
1912    } else if (c.getSystem() == ContactPointSystem.EMAIL) {
1913      p.addTag("a").setAttribute("href",  "mailto:"+c.getValue()).addText(c.getValue());
1914    } else if (c.getSystem() == ContactPointSystem.OTHER) {
1915      if (c.getValue().length() > 30)
1916        p.addTag("a").setAttribute("href", c.getValue()).addText(c.getValue().substring(0, 30)+"...");
1917      else
1918        p.addTag("a").setAttribute("href", c.getValue()).addText(c.getValue());
1919    }
1920  }
1921
1922  /**
1923   * This generate is optimised for the FHIR build process itself in as much as it
1924   * generates hyperlinks in the narrative that are only going to be correct for
1925   * the purposes of the build. This is to be reviewed in the future.
1926   *
1927   * @param vs
1928   * @param codeSystems
1929   * @throws Exception
1930   */
1931  public void generate(ValueSet vs, boolean header) {
1932    generate(vs, null, header);
1933  }
1934
1935  public void generate(ValueSet vs, ValueSet src, boolean header) {
1936    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
1937    if (vs.hasExpansion()) {
1938      // for now, we just accept an expansion if there is one
1939      generateExpansion(x, vs, src, header);
1940//      if (!vs.hasCodeSystem() && !vs.hasCompose())
1941//        generateExpansion(x, vs, src, header);
1942//      else
1943//        throw new DefinitionException("Error: should not encounter value set expansion at this point");
1944    }
1945
1946    boolean hasExtensions = false;
1947    if (vs.hasCodeSystem())
1948      hasExtensions = generateDefinition(x, vs, header);
1949    if (vs.hasCompose())
1950      hasExtensions = generateComposition(x, vs, header) || hasExtensions;
1951    inject(vs, x, hasExtensions ? NarrativeStatus.EXTENSIONS :  NarrativeStatus.GENERATED);
1952  }
1953
1954  private Integer countMembership(ValueSet vs) {
1955    int count = 0;
1956    if (vs.hasExpansion())
1957      count = count + conceptCount(vs.getExpansion().getContains());
1958    else {
1959      if (vs.hasCodeSystem())
1960        count = count + countConcepts(vs.getCodeSystem().getConcept());
1961      if (vs.hasCompose()) {
1962        if (vs.getCompose().hasExclude()) {
1963          try {
1964            ValueSetExpansionOutcome vse = context.expandVS(vs, true);
1965            count = 0;
1966            count += conceptCount(vse.getValueset().getExpansion().getContains());
1967            return count;
1968          } catch (Exception e) {
1969            return null;
1970          }
1971        }
1972        for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
1973          if (inc.hasFilter())
1974            return null;
1975          if (!inc.hasConcept())
1976            return null;
1977          count = count + inc.getConcept().size();
1978        }
1979      }
1980    }
1981    return count;
1982  }
1983
1984  private int conceptCount(List<ValueSetExpansionContainsComponent> list) {
1985    int count = 0;
1986    for (ValueSetExpansionContainsComponent c : list) {
1987      if (!c.getAbstract())
1988        count++;
1989      count = count + conceptCount(c.getContains());
1990    }
1991    return count;
1992  }
1993
1994  private int countConcepts(List<ConceptDefinitionComponent> list) {
1995    int count = list.size();
1996    for (ConceptDefinitionComponent c : list)
1997      if (c.hasConcept())
1998        count = count + countConcepts(c.getConcept());
1999    return count;
2000  }
2001
2002  private boolean generateExpansion(XhtmlNode x, ValueSet vs, ValueSet src, boolean header) {
2003    boolean hasExtensions = false;
2004    Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>();
2005    for (ConceptMap a : context.findMapsForSource(vs.getUrl())) {
2006        String url = "";
2007        ValueSet vsr = context.fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference());
2008        if (vsr != null)
2009            url = (String) vsr.getUserData("filename");
2010        mymaps.put(a, url);
2011      }
2012
2013    if (header) {
2014      XhtmlNode h = x.addTag("h3");
2015      h.addText("Value Set Contents");
2016      if (IsNotFixedExpansion(vs))
2017        x.addTag("p").addText(vs.getDescription());
2018      if (vs.hasCopyright())
2019        generateCopyright(x, vs);
2020    }
2021    if (ToolingExtensions.hasExtension(vs.getExpansion(), "http://hl7.org/fhir/StructureDefinition/valueset-toocostly"))
2022      x.addTag("p").setAttribute("style", "border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(tooCostlyNote);
2023    else {
2024      Integer count = countMembership(vs);
2025      if (count == null)
2026        x.addTag("p").addText("This value set does not contain a fixed number of concepts");
2027      else
2028        x.addTag("p").addText("This value set contains "+count.toString()+" concepts");
2029    }
2030
2031    boolean doSystem = checkDoSystem(vs, src);
2032    if (doSystem && allFromOneSystem(vs)) {
2033      doSystem = false;
2034      XhtmlNode p = x.addTag("p");
2035      p.addText("All codes from system ");
2036      p.addTag("code").addText(vs.getExpansion().getContains().get(0).getSystem());
2037    }
2038    XhtmlNode t = x.addTag("table").setAttribute("class", "codes");
2039    XhtmlNode tr = t.addTag("tr");
2040    tr.addTag("td").addTag("b").addText("Code");
2041    if (doSystem)
2042      tr.addTag("td").addTag("b").addText("System");
2043    tr.addTag("td").addTag("b").addText("Display");
2044
2045    addMapHeaders(tr, mymaps);
2046    for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) {
2047      addExpansionRowToTable(t, c, 0, doSystem, mymaps);
2048    }
2049    return hasExtensions;
2050  }
2051
2052  private boolean allFromOneSystem(ValueSet vs) {
2053    if (vs.getExpansion().getContains().isEmpty())
2054      return false;
2055    String system = vs.getExpansion().getContains().get(0).getSystem();
2056    for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) {
2057      if (!checkSystemMatches(system, cc))
2058        return false;
2059    }
2060    return true;
2061  }
2062
2063
2064  private boolean checkSystemMatches(String system, ValueSetExpansionContainsComponent cc) {
2065    if (!system.equals(cc.getSystem()))
2066      return false;
2067    for (ValueSetExpansionContainsComponent cc1 : cc.getContains()) {
2068      if (!checkSystemMatches(system, cc1))
2069        return false;
2070    }
2071     return true;
2072  }
2073
2074
2075  private boolean checkDoSystem(ValueSet vs, ValueSet src) {
2076    if (src != null)
2077      vs = src;
2078    if (!vs.hasCodeSystem())
2079      return true;
2080    if (vs.hasCompose())
2081      return true;
2082    return false;
2083  }
2084
2085  private boolean IsNotFixedExpansion(ValueSet vs) {
2086    if (vs.hasCompose())
2087      return false;
2088
2089    if (vs.getCompose().hasImport())
2090      return true;
2091
2092    // it's not fixed if it has any includes that are not version fixed
2093    for (ConceptSetComponent cc : vs.getCompose().getInclude())
2094      if (!cc.hasVersion())
2095        return true;
2096    return false;
2097  }
2098
2099  private boolean generateDefinition(XhtmlNode x, ValueSet vs, boolean header) {
2100    boolean hasExtensions = false;
2101    Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>();
2102    for (ConceptMap a : context.findMapsForSource(vs.getUrl())) {
2103        String url = "";
2104        ValueSet vsr = context.fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference());
2105        if (vsr != null)
2106            url = (String) vsr.getUserData("filename");
2107        mymaps.put(a, url);
2108    }
2109    // also, look in the contained resources for a concept map
2110    for (Resource r : vs.getContained()) {
2111      if (r instanceof ConceptMap) {
2112        ConceptMap cm = (ConceptMap) r;
2113        if (((Reference) cm.getSource()).getReference().equals(vs.getUrl())) {
2114          String url = "";
2115          ValueSet vsr = context.fetchResource(ValueSet.class, ((Reference) cm.getTarget()).getReference());
2116          if (vsr != null)
2117              url = (String) vsr.getUserData("filename");
2118        mymaps.put(cm, url);
2119        }
2120      }
2121    }
2122    List<String> langs = new ArrayList<String>();
2123
2124    if (header) {
2125      XhtmlNode h = x.addTag("h2");
2126      h.addText(vs.getName());
2127      XhtmlNode p = x.addTag("p");
2128      smartAddText(p, vs.getDescription());
2129      if (vs.hasCopyright())
2130        generateCopyright(x, vs);
2131    }
2132    XhtmlNode p = x.addTag("p");
2133    p.addText("This value set has an inline code system "+vs.getCodeSystem().getSystem()+", which defines the following codes:");
2134    XhtmlNode t = x.addTag("table").setAttribute("class", "codes");
2135    boolean commentS = false;
2136    boolean deprecated = false;
2137    boolean display = false;
2138    boolean hierarchy = false;
2139    for (ConceptDefinitionComponent c : vs.getCodeSystem().getConcept()) {
2140      commentS = commentS || conceptsHaveComments(c);
2141      deprecated = deprecated || conceptsHaveDeprecated(c);
2142      display = display || conceptsHaveDisplay(c);
2143      hierarchy = hierarchy || c.hasConcept();
2144      scanLangs(c, langs);
2145    }
2146    addMapHeaders(addTableHeaderRowStandard(t, hierarchy, display, true, commentS, deprecated), mymaps);
2147    for (ConceptDefinitionComponent c : vs.getCodeSystem().getConcept()) {
2148      hasExtensions = addDefineRowToTable(t, c, 0, hierarchy, display, commentS, deprecated, mymaps, vs.getCodeSystem().getSystem()) || hasExtensions;
2149    }
2150    if (langs.size() > 0) {
2151      Collections.sort(langs);
2152      x.addTag("p").addTag("b").addText("Additional Language Displays");
2153      t = x.addTag("table").setAttribute("class", "codes");
2154      XhtmlNode tr = t.addTag("tr");
2155      tr.addTag("td").addTag("b").addText("Code");
2156      for (String lang : langs)
2157        tr.addTag("td").addTag("b").addText(lang);
2158      for (ConceptDefinitionComponent c : vs.getCodeSystem().getConcept()) {
2159        addLanguageRow(c, t, langs);
2160      }
2161    }
2162    return hasExtensions;
2163  }
2164
2165  private void addLanguageRow(ConceptDefinitionComponent c, XhtmlNode t, List<String> langs) {
2166    XhtmlNode tr = t.addTag("tr");
2167    tr.addTag("td").addText(c.getCode());
2168    for (String lang : langs) {
2169      ConceptDefinitionDesignationComponent d = null;
2170      for (ConceptDefinitionDesignationComponent designation : c.getDesignation()) {
2171        if (lang.equals(designation.getLanguage()))
2172          d = designation;
2173      }
2174      tr.addTag("td").addText(d == null ? "" : d.getValue());
2175    }
2176  }
2177
2178  private void scanLangs(ConceptDefinitionComponent c, List<String> langs) {
2179    for (ConceptDefinitionDesignationComponent designation : c.getDesignation()) {
2180      String lang = designation.getLanguage();
2181      if (langs != null && !langs.contains(lang))
2182        langs.add(lang);
2183    }
2184    for (ConceptDefinitionComponent g : c.getConcept())
2185      scanLangs(g, langs);
2186  }
2187
2188  private void addMapHeaders(XhtmlNode tr, Map<ConceptMap, String> mymaps) {
2189          for (ConceptMap m : mymaps.keySet()) {
2190                XhtmlNode td = tr.addTag("td");
2191                XhtmlNode b = td.addTag("b");
2192                XhtmlNode a = b.addTag("a");
2193                a.setAttribute("href", prefix+mymaps.get(m));
2194                a.addText(m.hasDescription() ? m.getDescription() : m.getName());
2195          }
2196  }
2197
2198        private void smartAddText(XhtmlNode p, String text) {
2199          if (text == null)
2200            return;
2201
2202    String[] lines = text.split("\\r\\n");
2203    for (int i = 0; i < lines.length; i++) {
2204      if (i > 0)
2205        p.addTag("br");
2206      p.addText(lines[i]);
2207    }
2208  }
2209
2210  private boolean conceptsHaveComments(ConceptDefinitionComponent c) {
2211    if (ToolingExtensions.hasComment(c))
2212      return true;
2213    for (ConceptDefinitionComponent g : c.getConcept())
2214      if (conceptsHaveComments(g))
2215        return true;
2216    return false;
2217  }
2218
2219  private boolean conceptsHaveDisplay(ConceptDefinitionComponent c) {
2220    if (c.hasDisplay())
2221      return true;
2222    for (ConceptDefinitionComponent g : c.getConcept())
2223      if (conceptsHaveDisplay(g))
2224        return true;
2225    return false;
2226  }
2227
2228  private boolean conceptsHaveDeprecated(ConceptDefinitionComponent c) {
2229    if (ToolingExtensions.hasDeprecated(c))
2230      return true;
2231    for (ConceptDefinitionComponent g : c.getConcept())
2232      if (conceptsHaveDeprecated(g))
2233        return true;
2234    return false;
2235  }
2236
2237  private void generateCopyright(XhtmlNode x, ValueSet vs) {
2238    XhtmlNode p = x.addTag("p");
2239    p.addTag("b").addText("Copyright Statement:");
2240    smartAddText(p, " " + vs.getCopyright());
2241  }
2242
2243
2244  private XhtmlNode addTableHeaderRowStandard(XhtmlNode t, boolean hasHierarchy, boolean hasDisplay, boolean definitions, boolean comments, boolean deprecated) {
2245    XhtmlNode tr = t.addTag("tr");
2246    if (hasHierarchy)
2247      tr.addTag("td").addTag("b").addText("Lvl");
2248    tr.addTag("td").addTag("b").addText("Code");
2249    if (hasDisplay)
2250      tr.addTag("td").addTag("b").addText("Display");
2251    if (definitions)
2252      tr.addTag("td").addTag("b").addText("Definition");
2253    if (deprecated)
2254      tr.addTag("td").addTag("b").addText("Deprecated");
2255    if (comments)
2256      tr.addTag("td").addTag("b").addText("Comments");
2257    return tr;
2258  }
2259
2260  private void addExpansionRowToTable(XhtmlNode t, ValueSetExpansionContainsComponent c, int i, boolean doSystem, Map<ConceptMap, String> mymaps) {
2261    XhtmlNode tr = t.addTag("tr");
2262    XhtmlNode td = tr.addTag("td");
2263
2264    String tgt = makeAnchor(c.getSystem(), c.getCode());
2265    td.addTag("a").setAttribute("name", tgt).addText(" ");
2266
2267    String s = Utilities.padLeft("", '.', i*2);
2268
2269    td.addText(s);
2270    Resource e = context.fetchCodeSystem(c.getSystem());
2271    if (e == null)
2272      td.addText(c.getCode());
2273    else {
2274      XhtmlNode a = td.addTag("a");
2275      a.addText(c.getCode());
2276      a.setAttribute("href", prefix+getCsRef(e)+"#"+Utilities.nmtokenize(c.getCode()));
2277    }
2278    if (doSystem) {
2279      td = tr.addTag("td");
2280      td.addText(c.getSystem());
2281    }
2282    td = tr.addTag("td");
2283    if (c.hasDisplayElement())
2284      td.addText(c.getDisplay());
2285
2286    for (ConceptMap m : mymaps.keySet()) {
2287      td = tr.addTag("td");
2288      List<TargetElementComponent> mappings = findMappingsForCode(c.getCode(), m);
2289      boolean first = true;
2290      for (TargetElementComponent mapping : mappings) {
2291        if (!first)
2292            td.addTag("br");
2293        first = false;
2294        XhtmlNode span = td.addTag("span");
2295        span.setAttribute("title", mapping.getEquivalence().toString());
2296        span.addText(getCharForEquivalence(mapping));
2297        XhtmlNode a = td.addTag("a");
2298        a.setAttribute("href", prefix+mymaps.get(m)+"#"+mapping.getCode());
2299        a.addText(mapping.getCode());
2300        if (!Utilities.noString(mapping.getComments()))
2301          td.addTag("i").addText("("+mapping.getComments()+")");
2302      }
2303    }
2304    for (ValueSetExpansionContainsComponent cc : c.getContains()) {
2305      addExpansionRowToTable(t, cc, i+1, doSystem, mymaps);
2306    }
2307  }
2308
2309  private boolean addDefineRowToTable(XhtmlNode t, ConceptDefinitionComponent c, int i, boolean hasHierarchy, boolean hasDisplay, boolean comment, boolean deprecated, Map<ConceptMap, String> maps, String system) {
2310    boolean hasExtensions = false;
2311    XhtmlNode tr = t.addTag("tr");
2312    XhtmlNode td = tr.addTag("td");
2313    if (hasHierarchy) {
2314      td.addText(Integer.toString(i+1));
2315      td = tr.addTag("td");
2316      String s = Utilities.padLeft("", '\u00A0', i*2);
2317      td.addText(s);
2318    }
2319    td.addText(c.getCode());
2320    XhtmlNode a;
2321    if (c.hasCodeElement()) {
2322      a = td.addTag("a");
2323      a.setAttribute("name", Utilities.nmtokenize(c.getCode()));
2324      a.addText(" ");
2325    }
2326
2327    if (hasDisplay) {
2328      td = tr.addTag("td");
2329      if (c.hasDisplayElement())
2330        td.addText(c.getDisplay());
2331    }
2332    td = tr.addTag("td");
2333    if (c != null)
2334      smartAddText(td, c.getDefinition());
2335    if (deprecated) {
2336      td = tr.addTag("td");
2337      Boolean b = ToolingExtensions.getDeprecated(c);
2338      if (b !=  null && b) {
2339        smartAddText(td, "Deprecated");
2340        hasExtensions = true;
2341        if (ToolingExtensions.hasExtension(c, ToolingExtensions.EXT_REPLACED_BY)) {
2342          Coding cc = (Coding) ToolingExtensions.getExtension(c, ToolingExtensions.EXT_REPLACED_BY).getValue();
2343          td.addText(" (replaced by ");
2344          String url = getCodingReference(cc, system);
2345          if (url != null) {
2346            td.addTag("a").setAttribute("href", url).addText(cc.getCode());
2347            td.addText(": "+cc.getDisplay()+")");
2348          } else
2349            td.addText(cc.getCode()+" '"+cc.getDisplay()+"' in "+cc.getSystem()+")");
2350        }
2351      }
2352    }
2353    if (comment) {
2354      td = tr.addTag("td");
2355      String s = ToolingExtensions.getComment(c);
2356      if (s != null) {
2357        smartAddText(td, s);
2358        hasExtensions = true;
2359      }
2360    }
2361    for (ConceptMap m : maps.keySet()) {
2362      td = tr.addTag("td");
2363      List<TargetElementComponent> mappings = findMappingsForCode(c.getCode(), m);
2364      boolean first = true;
2365      for (TargetElementComponent mapping : mappings) {
2366        if (!first)
2367                  td.addTag("br");
2368        first = false;
2369        XhtmlNode span = td.addTag("span");
2370        span.setAttribute("title", mapping.hasEquivalence() ?  mapping.getEquivalence().toCode() : "");
2371        span.addText(getCharForEquivalence(mapping));
2372        a = td.addTag("a");
2373        a.setAttribute("href", prefix+maps.get(m)+"#"+makeAnchor(mapping.getCodeSystem(), mapping.getCode()));
2374        a.addText(mapping.getCode());
2375        if (!Utilities.noString(mapping.getComments()))
2376          td.addTag("i").addText("("+mapping.getComments()+")");
2377      }
2378    }
2379    for (CodeType e : ToolingExtensions.getSubsumes(c)) {
2380      hasExtensions = true;
2381      tr = t.addTag("tr");
2382      td = tr.addTag("td");
2383      String s = Utilities.padLeft("", '.', i*2);
2384      td.addText(s);
2385      a = td.addTag("a");
2386      a.setAttribute("href", "#"+Utilities.nmtokenize(e.getValue()));
2387      a.addText(c.getCode());
2388    }
2389    for (ConceptDefinitionComponent cc : c.getConcept()) {
2390      hasExtensions = addDefineRowToTable(t, cc, i+1, hasHierarchy, hasDisplay, comment, deprecated, maps, system) || hasExtensions;
2391    }
2392    return hasExtensions;
2393  }
2394
2395
2396  private String makeAnchor(String codeSystem, String code) {
2397    String s = codeSystem+'-'+code;
2398    StringBuilder b = new StringBuilder();
2399    for (char c : s.toCharArray()) {
2400      if (Character.isAlphabetic(c) || Character.isDigit(c) || c == '.')
2401        b.append(c);
2402      else
2403        b.append('-');
2404    }
2405    return b.toString();
2406  }
2407
2408  private String getCodingReference(Coding cc, String system) {
2409    if (cc.getSystem().equals(system))
2410      return "#"+cc.getCode();
2411    if (cc.getSystem().equals("http://snomed.info/sct"))
2412      return "http://snomed.info/sct/"+cc.getCode();
2413    if (cc.getSystem().equals("http://loinc.org"))
2414      return "http://s.details.loinc.org/LOINC/"+cc.getCode()+".html";
2415    return null;
2416  }
2417
2418  private String getCharForEquivalence(TargetElementComponent mapping) {
2419    if (!mapping.hasEquivalence())
2420      return "";
2421          switch (mapping.getEquivalence()) {
2422          case EQUAL : return "=";
2423          case EQUIVALENT : return "~";
2424          case WIDER : return "<";
2425          case NARROWER : return ">";
2426          case INEXACT : return "><";
2427          case UNMATCHED : return "-";
2428          case DISJOINT : return "!=";
2429    default: return "?";
2430          }
2431  }
2432
2433        private List<TargetElementComponent> findMappingsForCode(String code, ConceptMap map) {
2434          List<TargetElementComponent> mappings = new ArrayList<TargetElementComponent>();
2435
2436        for (SourceElementComponent c : map.getElement()) {
2437                if (c.getCode().equals(code))
2438                        mappings.addAll(c.getTarget());
2439          }
2440          return mappings;
2441  }
2442
2443        private boolean generateComposition(XhtmlNode x, ValueSet vs, boolean header) {
2444          boolean hasExtensions = false;
2445    if (!vs.hasCodeSystem()) {
2446      if (header) {
2447        XhtmlNode h = x.addTag("h2");
2448        h.addText(vs.getName());
2449        XhtmlNode p = x.addTag("p");
2450        smartAddText(p, vs.getDescription());
2451        if (vs.hasCopyrightElement())
2452          generateCopyright(x, vs);
2453      }
2454      XhtmlNode p = x.addTag("p");
2455      p.addText("This value set includes codes from the following code systems:");
2456    } else {
2457      XhtmlNode p = x.addTag("p");
2458      p.addText("In addition, this value set includes codes from other code systems:");
2459    }
2460
2461    XhtmlNode ul = x.addTag("ul");
2462    XhtmlNode li;
2463    for (UriType imp : vs.getCompose().getImport()) {
2464      li = ul.addTag("li");
2465      li.addText("Import all the codes that are contained in ");
2466      AddVsRef(imp.getValue(), li);
2467    }
2468    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
2469      hasExtensions = genInclude(ul, inc, "Include") || hasExtensions;
2470    }
2471    for (ConceptSetComponent exc : vs.getCompose().getExclude()) {
2472      hasExtensions = genInclude(ul, exc, "Exclude") || hasExtensions;
2473    }
2474    return hasExtensions;
2475  }
2476
2477  private void AddVsRef(String value, XhtmlNode li) {
2478
2479    ValueSet vs = context.fetchResource(ValueSet.class, value);
2480    if (vs == null)
2481      vs = context.fetchCodeSystem(value);
2482    if (vs != null) {
2483      String ref = (String) vs.getUserData("path");
2484      ref = adjustForPath(ref);
2485      XhtmlNode a = li.addTag("a");
2486      a.setAttribute("href", ref == null ? "??" : ref.replace("\\", "/"));
2487      a.addText(value);
2488    } else if (value.equals("http://snomed.info/sct") || value.equals("http://snomed.info/id")) {
2489      XhtmlNode a = li.addTag("a");
2490      a.setAttribute("href", value);
2491      a.addText("SNOMED-CT");
2492    }
2493    else
2494      li.addText(value);
2495  }
2496
2497  private String adjustForPath(String ref) {
2498    if (prefix == null)
2499      return ref;
2500    else
2501      return prefix+ref;
2502  }
2503
2504  private boolean genInclude(XhtmlNode ul, ConceptSetComponent inc, String type) {
2505    boolean hasExtensions = false;
2506    XhtmlNode li;
2507    li = ul.addTag("li");
2508    ValueSet e = context.fetchCodeSystem(inc.getSystem());
2509
2510    if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) {
2511      li.addText(type+" all codes defined in ");
2512      addCsRef(inc, li, e);
2513    } else {
2514      if (inc.getConcept().size() > 0) {
2515        li.addText(type+" these codes as defined in ");
2516        addCsRef(inc, li, e);
2517
2518        XhtmlNode t = li.addTag("table");
2519        boolean hasComments = false;
2520        boolean hasDefinition = false;
2521        for (ConceptReferenceComponent c : inc.getConcept()) {
2522          hasComments = hasComments || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_COMMENT);
2523          hasDefinition = hasDefinition || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION);
2524        }
2525        if (hasComments || hasDefinition)
2526          hasExtensions = true;
2527        addTableHeaderRowStandard(t, false, true, hasDefinition, hasComments, false);
2528        for (ConceptReferenceComponent c : inc.getConcept()) {
2529          XhtmlNode tr = t.addTag("tr");
2530          tr.addTag("td").addText(c.getCode());
2531          ConceptDefinitionComponent cc = getConceptForCode(e, c.getCode(), inc.getSystem());
2532
2533          XhtmlNode td = tr.addTag("td");
2534          if (!Utilities.noString(c.getDisplay()))
2535            td.addText(c.getDisplay());
2536          else if (cc != null && !Utilities.noString(cc.getDisplay()))
2537            td.addText(cc.getDisplay());
2538
2539          td = tr.addTag("td");
2540          if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION))
2541            smartAddText(td, ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_DEFINITION));
2542          else if (cc != null && !Utilities.noString(cc.getDefinition()))
2543            smartAddText(td, cc.getDefinition());
2544
2545          if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_COMMENT)) {
2546            smartAddText(tr.addTag("td"), "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_COMMENT));
2547          }
2548        }
2549      }
2550      boolean first = true;
2551      for (ConceptSetFilterComponent f : inc.getFilter()) {
2552        if (first) {
2553          li.addText(type+" codes from ");
2554          first = false;
2555        } else
2556          li.addText(" and ");
2557        addCsRef(inc, li, e);
2558        li.addText(" where "+f.getProperty()+" "+describe(f.getOp())+" ");
2559        if (e != null && codeExistsInValueSet(e, f.getValue())) {
2560          XhtmlNode a = li.addTag("a");
2561          a.addText(f.getValue());
2562          a.setAttribute("href", prefix+getCsRef(e)+"#"+Utilities.nmtokenize(f.getValue()));
2563        } else
2564          li.addText(f.getValue());
2565        String disp = ToolingExtensions.getDisplayHint(f);
2566        if (disp != null)
2567          li.addText(" ("+disp+")");
2568      }
2569    }
2570    return hasExtensions;
2571  }
2572
2573  private String describe(FilterOperator opSimple) {
2574    switch (opSimple) {
2575    case EQUAL: return " = ";
2576    case ISA: return " is-a ";
2577    case ISNOTA: return " is-not-a ";
2578    case REGEX: return " matches (by regex) ";
2579                case NULL: return " ?? ";
2580                case IN: return " in ";
2581                case NOTIN: return " not in ";
2582    }
2583    return null;
2584  }
2585
2586  private <T extends Resource> ConceptDefinitionComponent getConceptForCode(T e, String code, String system) {
2587    if (e == null) {
2588      return context.validateCode(system, code, null).asConceptDefinition();
2589    }
2590    ValueSet vs = (ValueSet) e;
2591    if (!vs.hasCodeSystem())
2592      return null;
2593    for (ConceptDefinitionComponent c : vs.getCodeSystem().getConcept()) {
2594      ConceptDefinitionComponent v = getConceptForCode(c, code);
2595      if (v != null)
2596        return v;
2597    }
2598    return null;
2599  }
2600
2601
2602
2603  private ConceptDefinitionComponent getConceptForCode(ConceptDefinitionComponent c, String code) {
2604    if (code.equals(c.getCode()))
2605      return c;
2606    for (ConceptDefinitionComponent cc : c.getConcept()) {
2607      ConceptDefinitionComponent v = getConceptForCode(cc, code);
2608      if (v != null)
2609        return v;
2610    }
2611    return null;
2612  }
2613
2614  private  <T extends Resource> void addCsRef(ConceptSetComponent inc, XhtmlNode li, T cs) {
2615    String ref = null;
2616    if (cs != null) {
2617      ref = (String) cs.getUserData("filename");
2618      if (Utilities.noString(ref))
2619        ref = (String) cs.getUserData("path");
2620    }
2621    if (cs != null && ref != null) {
2622      if (!Utilities.noString(prefix) && ref.startsWith("http://hl7.org/fhir/"))
2623        ref = ref.substring(20)+"/index.html";
2624      else if (!ref.endsWith(".html"))
2625          ref = ref + ".html";
2626      XhtmlNode a = li.addTag("a");
2627      a.setAttribute("href", prefix+ref.replace("\\", "/"));
2628      a.addText(inc.getSystem().toString());
2629    } else
2630      li.addText(inc.getSystem().toString());
2631  }
2632
2633  private  <T extends Resource> String getCsRef(T cs) {
2634    String ref = (String) cs.getUserData("filename");
2635    if (ref == null)
2636      return "??";
2637    if (!ref.endsWith(".html"))
2638      ref = ref + ".html";
2639    return ref.replace("\\", "/");
2640  }
2641
2642  private  <T extends Resource> boolean codeExistsInValueSet(T cs, String code) {
2643    ValueSet vs = (ValueSet) cs;
2644    for (ConceptDefinitionComponent c : vs.getCodeSystem().getConcept()) {
2645      if (inConcept(code, c))
2646        return true;
2647    }
2648    return false;
2649  }
2650
2651  private boolean inConcept(String code, ConceptDefinitionComponent c) {
2652    if (c.hasCodeElement() && c.getCode().equals(code))
2653      return true;
2654    for (ConceptDefinitionComponent g : c.getConcept()) {
2655      if (inConcept(code, g))
2656        return true;
2657    }
2658    return false;
2659  }
2660
2661  /**
2662   * This generate is optimised for the build tool in that it tracks the source extension.
2663   * But it can be used for any other use.
2664   *
2665   * @param vs
2666   * @param codeSystems
2667   * @throws DefinitionException 
2668   * @throws Exception
2669   */
2670  public void generate(OperationOutcome op) throws DefinitionException {
2671    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
2672    boolean hasSource = false;
2673    boolean success = true;
2674    for (OperationOutcomeIssueComponent i : op.getIssue()) {
2675        success = success && i.getSeverity() == IssueSeverity.INFORMATION;
2676        hasSource = hasSource || ExtensionHelper.hasExtension(i, ToolingExtensions.EXT_ISSUE_SOURCE);
2677    }
2678    if (success)
2679        x.addTag("p").addText("All OK");
2680    if (op.getIssue().size() > 0) {
2681                XhtmlNode tbl = x.addTag("table");
2682                tbl.setAttribute("class", "grid"); // on the basis that we'll most likely be rendered using the standard fhir css, but it doesn't really matter
2683                XhtmlNode tr = tbl.addTag("tr");
2684                tr.addTag("td").addTag("b").addText("Severity");
2685                tr.addTag("td").addTag("b").addText("Location");
2686        tr.addTag("td").addTag("b").addText("Code");
2687        tr.addTag("td").addTag("b").addText("Details");
2688        tr.addTag("td").addTag("b").addText("Diagnostics");
2689                if (hasSource)
2690                        tr.addTag("td").addTag("b").addText("Source");
2691                for (OperationOutcomeIssueComponent i : op.getIssue()) {
2692                        tr = tbl.addTag("tr");
2693                        tr.addTag("td").addText(i.getSeverity().toString());
2694                        XhtmlNode td = tr.addTag("td");
2695                        boolean d = false;
2696                        for (StringType s : i.getLocation()) {
2697                                if (d)
2698                                        td.addText(", ");
2699                                else
2700                                        d = true;
2701                                td.addText(s.getValue());
2702                        }
2703          tr.addTag("td").addText(i.getCode().getDisplay());
2704          tr.addTag("td").addText(gen(i.getDetails()));
2705          smartAddText(tr.addTag("td"), i.getDiagnostics());
2706                        if (hasSource) {
2707                                Extension ext = ExtensionHelper.getExtension(i, ToolingExtensions.EXT_ISSUE_SOURCE);
2708            tr.addTag("td").addText(ext == null ? "" : gen(ext));
2709                        }
2710                }
2711        }
2712    inject(op, x, hasSource ? NarrativeStatus.EXTENSIONS :  NarrativeStatus.GENERATED);
2713  }
2714
2715
2716        private String gen(Extension extension) throws DefinitionException {
2717                if (extension.getValue() instanceof CodeType)
2718                        return ((CodeType) extension.getValue()).getValue();
2719                if (extension.getValue() instanceof Coding)
2720                        return gen((Coding) extension.getValue());
2721
2722          throw new DefinitionException("Unhandled type "+extension.getValue().getClass().getName());
2723  }
2724
2725        private String gen(CodeableConcept code) {
2726                if (code == null)
2727                return null;
2728                if (code.hasText())
2729                        return code.getText();
2730                if (code.hasCoding())
2731                        return gen(code.getCoding().get(0));
2732                return null;
2733        }
2734
2735        private String gen(Coding code) {
2736          if (code == null)
2737                return null;
2738          if (code.hasDisplayElement())
2739                return code.getDisplay();
2740          if (code.hasCodeElement())
2741                return code.getCode();
2742          return null;
2743  }
2744
2745        public void generate(OperationDefinition opd) throws EOperationOutcome, FHIRException, IOException {
2746    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
2747    x.addTag("h2").addText(opd.getName());
2748    x.addTag("p").addText(Utilities.capitalize(opd.getKind().toString())+": "+opd.getName());
2749    addMarkdown(x, opd.getDescription());
2750
2751    if (opd.getSystem())
2752      x.addTag("p").addText("URL: [base]/$"+opd.getCode());
2753    for (CodeType c : opd.getType()) {
2754      x.addTag("p").addText("URL: [base]/"+c.getValue()+"/$"+opd.getCode());
2755      if (opd.getInstance())
2756        x.addTag("p").addText("URL: [base]/"+c.getValue()+"/[id]/$"+opd.getCode());
2757    }
2758
2759    x.addTag("p").addText("Parameters");
2760    XhtmlNode tbl = x.addTag("table").setAttribute("class", "grid");
2761    XhtmlNode tr = tbl.addTag("tr");
2762    tr.addTag("td").addTag("b").addText("Use");
2763    tr.addTag("td").addTag("b").addText("Name");
2764    tr.addTag("td").addTag("b").addText("Cardinality");
2765    tr.addTag("td").addTag("b").addText("Type");
2766    tr.addTag("td").addTag("b").addText("Binding");
2767    tr.addTag("td").addTag("b").addText("Documentation");
2768    for (OperationDefinitionParameterComponent p : opd.getParameter()) {
2769      genOpParam(tbl, "", p);
2770    }
2771    addMarkdown(x, opd.getNotes());
2772    inject(opd, x, NarrativeStatus.GENERATED);
2773        }
2774
2775        private void genOpParam(XhtmlNode tbl, String path, OperationDefinitionParameterComponent p) throws EOperationOutcome, FHIRException, IOException {
2776                XhtmlNode tr;
2777      tr = tbl.addTag("tr");
2778      tr.addTag("td").addText(p.getUse().toString());
2779                tr.addTag("td").addText(path+p.getName());
2780      tr.addTag("td").addText(Integer.toString(p.getMin())+".."+p.getMax());
2781      tr.addTag("td").addText(p.hasType() ? p.getType() : "");
2782      XhtmlNode td = tr.addTag("td");
2783      if (p.hasBinding() && p.getBinding().hasValueSet()) {
2784        if (p.getBinding().getValueSet() instanceof Reference)
2785          AddVsRef(p.getBinding().getValueSetReference().getReference(), td);
2786        else
2787          td.addTag("a").setAttribute("href", p.getBinding().getValueSetUriType().getValue()).addText("External Reference");
2788        td.addText(" ("+p.getBinding().getStrength().getDisplay()+")");
2789      }
2790      addMarkdown(tr.addTag("td"), p.getDocumentation());
2791      if (!p.hasType()) {
2792                        for (OperationDefinitionParameterComponent pp : p.getPart()) {
2793                                genOpParam(tbl, path+p.getName()+".", pp);
2794        }
2795      }
2796    }
2797
2798        private void addMarkdown(XhtmlNode x, String text) throws FHIRFormatError, IOException, DefinitionException {
2799          if (text != null) {
2800            // 1. custom FHIR extensions
2801            while (text.contains("[[[")) {
2802              String left = text.substring(0, text.indexOf("[[["));
2803              String link = text.substring(text.indexOf("[[[")+3, text.indexOf("]]]"));
2804              String right = text.substring(text.indexOf("]]]")+3);
2805              String url = link;
2806              String[] parts = link.split("\\#");
2807              StructureDefinition p = context.fetchResource(StructureDefinition.class, parts[0]);
2808              if (p == null)
2809                p = context.fetchTypeDefinition(parts[0]);
2810              if (p == null)
2811                p = context.fetchResource(StructureDefinition.class, link);
2812              if (p != null) {
2813                url = p.getUserString("path");
2814                if (url == null)
2815                  url = p.getUserString("filename");
2816              } else
2817                throw new DefinitionException("Unable to resolve markdown link "+link);
2818
2819              text = left+"["+link+"]("+url+")"+right;
2820            }
2821
2822            // 2. markdown
2823            String s = new MarkDownProcessor(Dialect.DARING_FIREBALL).process(Utilities.escapeXml(text), "NarrativeGenerator");
2824            XhtmlParser p = new XhtmlParser();
2825            XhtmlNode m = p.parse("<div>"+s+"</div>", "div");
2826            x.getChildNodes().addAll(m.getChildNodes());
2827          }
2828  }
2829
2830  public void generate(Conformance conf) {
2831    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
2832    x.addTag("h2").addText(conf.getName());
2833    smartAddText(x.addTag("p"), conf.getDescription());
2834    ConformanceRestComponent rest = conf.getRest().get(0);
2835    XhtmlNode t = x.addTag("table");
2836    addTableRow(t, "Mode", rest.getMode().toString());
2837    addTableRow(t, "Description", rest.getDocumentation());
2838
2839    addTableRow(t, "Transaction", showOp(rest, SystemRestfulInteraction.TRANSACTION));
2840    addTableRow(t, "System History", showOp(rest, SystemRestfulInteraction.HISTORYSYSTEM));
2841    addTableRow(t, "System Search", showOp(rest, SystemRestfulInteraction.SEARCHSYSTEM));
2842
2843    t = x.addTag("table");
2844    XhtmlNode tr = t.addTag("tr");
2845    tr.addTag("th").addTag("b").addText("Resource Type");
2846    tr.addTag("th").addTag("b").addText("Profile");
2847    tr.addTag("th").addTag("b").addText("Read");
2848    tr.addTag("th").addTag("b").addText("V-Read");
2849    tr.addTag("th").addTag("b").addText("Search");
2850    tr.addTag("th").addTag("b").addText("Update");
2851    tr.addTag("th").addTag("b").addText("Updates");
2852    tr.addTag("th").addTag("b").addText("Create");
2853    tr.addTag("th").addTag("b").addText("Delete");
2854    tr.addTag("th").addTag("b").addText("History");
2855
2856    for (ConformanceRestResourceComponent r : rest.getResource()) {
2857      tr = t.addTag("tr");
2858      tr.addTag("td").addText(r.getType());
2859      if (r.hasProfile()) {
2860        XhtmlNode a = tr.addTag("td").addTag("a");
2861        a.addText(r.getProfile().getReference());
2862        a.setAttribute("href", prefix+r.getProfile().getReference());
2863      }
2864      tr.addTag("td").addText(showOp(r, TypeRestfulInteraction.READ));
2865      tr.addTag("td").addText(showOp(r, TypeRestfulInteraction.VREAD));
2866      tr.addTag("td").addText(showOp(r, TypeRestfulInteraction.SEARCHTYPE));
2867      tr.addTag("td").addText(showOp(r, TypeRestfulInteraction.UPDATE));
2868      tr.addTag("td").addText(showOp(r, TypeRestfulInteraction.HISTORYINSTANCE));
2869      tr.addTag("td").addText(showOp(r, TypeRestfulInteraction.CREATE));
2870      tr.addTag("td").addText(showOp(r, TypeRestfulInteraction.DELETE));
2871      tr.addTag("td").addText(showOp(r, TypeRestfulInteraction.HISTORYTYPE));
2872    }
2873
2874    inject(conf, x, NarrativeStatus.GENERATED);
2875  }
2876
2877  private String showOp(ConformanceRestResourceComponent r, TypeRestfulInteraction on) {
2878    for (ResourceInteractionComponent op : r.getInteraction()) {
2879      if (op.getCode() == on)
2880        return "y";
2881    }
2882    return "";
2883  }
2884
2885  private String showOp(ConformanceRestComponent r, SystemRestfulInteraction on) {
2886    for (SystemInteractionComponent op : r.getInteraction()) {
2887      if (op.getCode() == on)
2888        return "y";
2889    }
2890    return "";
2891  }
2892
2893  private void addTableRow(XhtmlNode t, String name, String value) {
2894    XhtmlNode tr = t.addTag("tr");
2895    tr.addTag("td").addText(name);
2896    tr.addTag("td").addText(value);
2897  }
2898
2899  public XhtmlNode generateDocumentNarrative(Bundle feed) {
2900    /*
2901     When the document is presented for human consumption, applications must present the collated narrative portions of the following resources in order:
2902     * The Composition resource
2903     * The Subject resource
2904     * Resources referenced in the section.content
2905     */
2906    XhtmlNode root = new XhtmlNode(NodeType.Element, "div");
2907    Composition comp = (Composition) feed.getEntry().get(0).getResource();
2908    root.getChildNodes().add(comp.getText().getDiv());
2909    Resource subject = ResourceUtilities.getById(feed, null, comp.getSubject().getReference());
2910    if (subject != null && subject instanceof DomainResource) {
2911      root.addTag("hr");
2912      root.getChildNodes().add(((DomainResource)subject).getText().getDiv());
2913    }
2914    List<SectionComponent> sections = comp.getSection();
2915    renderSections(feed, root, sections, 1);
2916    return root;
2917  }
2918
2919  private void renderSections(Bundle feed, XhtmlNode node, List<SectionComponent> sections, int level) {
2920    for (SectionComponent section : sections) {
2921      node.addTag("hr");
2922      if (section.hasTitleElement())
2923        node.addTag("h"+Integer.toString(level)).addText(section.getTitle());
2924//      else if (section.hasCode())
2925//        node.addTag("h"+Integer.toString(level)).addText(displayCodeableConcept(section.getCode()));
2926
2927//      if (section.hasText()) {
2928//        node.getChildNodes().add(section.getText().getDiv());
2929//      }
2930//
2931//      if (!section.getSection().isEmpty()) {
2932//        renderSections(feed, node.addTag("blockquote"), section.getSection(), level+1);
2933//      }
2934    }
2935  }
2936
2937
2938  public boolean isPretty() {
2939    return pretty;
2940  }
2941
2942
2943  public void setPretty(boolean pretty) {
2944    this.pretty = pretty;
2945  }
2946
2947}