001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.io.UnsupportedEncodingException;
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Map;
010import java.util.Set;
011
012import org.apache.commons.codec.binary.Base64;
013import org.apache.commons.lang3.NotImplementedException;
014import org.hl7.fhir.exceptions.DefinitionException;
015import org.hl7.fhir.exceptions.FHIRException;
016import org.hl7.fhir.exceptions.FHIRFormatError;
017import org.hl7.fhir.r5.formats.FormatUtilities;
018import org.hl7.fhir.r5.model.Address;
019import org.hl7.fhir.r5.model.Annotation;
020import org.hl7.fhir.r5.model.Attachment;
021import org.hl7.fhir.r5.model.Base;
022import org.hl7.fhir.r5.model.Base64BinaryType;
023import org.hl7.fhir.r5.model.BooleanType;
024import org.hl7.fhir.r5.model.CodeType;
025import org.hl7.fhir.r5.model.CodeableConcept;
026import org.hl7.fhir.r5.model.CodeableReference;
027import org.hl7.fhir.r5.model.Coding;
028import org.hl7.fhir.r5.model.ContactDetail;
029import org.hl7.fhir.r5.model.ContactPoint;
030import org.hl7.fhir.r5.model.DataRequirement;
031import org.hl7.fhir.r5.model.DateTimeType;
032import org.hl7.fhir.r5.model.DomainResource;
033import org.hl7.fhir.r5.model.Dosage;
034import org.hl7.fhir.r5.model.ElementDefinition;
035import org.hl7.fhir.r5.model.Enumeration;
036import org.hl7.fhir.r5.model.Expression;
037import org.hl7.fhir.r5.model.Extension;
038import org.hl7.fhir.r5.model.HumanName;
039import org.hl7.fhir.r5.model.IdType;
040import org.hl7.fhir.r5.model.Identifier;
041import org.hl7.fhir.r5.model.InstantType;
042import org.hl7.fhir.r5.model.Meta;
043import org.hl7.fhir.r5.model.Money;
044import org.hl7.fhir.r5.model.Narrative;
045import org.hl7.fhir.r5.model.Narrative.NarrativeStatus;
046import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
047import org.hl7.fhir.r5.model.Period;
048import org.hl7.fhir.r5.model.PrimitiveType;
049import org.hl7.fhir.r5.model.Property;
050import org.hl7.fhir.r5.model.Quantity;
051import org.hl7.fhir.r5.model.Range;
052import org.hl7.fhir.r5.model.Ratio;
053import org.hl7.fhir.r5.model.Reference;
054import org.hl7.fhir.r5.model.RelatedArtifact;
055import org.hl7.fhir.r5.model.Resource;
056import org.hl7.fhir.r5.model.SampledData;
057import org.hl7.fhir.r5.model.Signature;
058import org.hl7.fhir.r5.model.StringType;
059import org.hl7.fhir.r5.model.StructureDefinition;
060import org.hl7.fhir.r5.model.Timing;
061import org.hl7.fhir.r5.model.UriType;
062import org.hl7.fhir.r5.model.UsageContext;
063import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper;
064import org.hl7.fhir.r5.renderers.utils.BaseWrappers.PropertyWrapper;
065import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper;
066import org.hl7.fhir.r5.renderers.utils.DOMWrappers.BaseWrapperElement;
067import org.hl7.fhir.r5.renderers.utils.DOMWrappers.ResourceWrapperElement;
068import org.hl7.fhir.r5.renderers.utils.DirectWrappers;
069import org.hl7.fhir.r5.renderers.utils.DirectWrappers.BaseWrapperDirect;
070import org.hl7.fhir.r5.renderers.utils.DirectWrappers.PropertyWrapperDirect;
071import org.hl7.fhir.r5.renderers.utils.DirectWrappers.ResourceWrapperDirect;
072import org.hl7.fhir.r5.renderers.utils.RenderingContext;
073import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext;
074import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceWithReference;
075import org.hl7.fhir.r5.utils.EOperationOutcome;
076import org.hl7.fhir.r5.utils.ToolingExtensions;
077import org.hl7.fhir.r5.utils.XVerExtensionManager;
078import org.hl7.fhir.r5.utils.XVerExtensionManager.XVerExtensionStatus;
079import org.hl7.fhir.utilities.Utilities;
080import org.hl7.fhir.utilities.xhtml.NodeType;
081import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
082import org.hl7.fhir.utilities.xhtml.XhtmlNode;
083import org.hl7.fhir.utilities.xml.XMLUtil;
084import org.w3c.dom.Element;
085
086public class ProfileDrivenRenderer extends ResourceRenderer {
087
088  private Set<String> containedIds = new HashSet<>();
089  private boolean hasExtensions;
090  
091  public ProfileDrivenRenderer(RenderingContext context, ResourceContext rcontext) {
092    super(context, rcontext);
093  }
094
095  public ProfileDrivenRenderer(RenderingContext context) {
096    super(context);
097  }
098
099  @Override
100  public boolean render(XhtmlNode x, Resource r) throws FHIRFormatError, DefinitionException, IOException {
101    return render(x, new DirectWrappers.ResourceWrapperDirect(context, r));
102  }
103
104  @Override
105  public boolean render(XhtmlNode x, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
106    if (context.isAddGeneratedNarrativeHeader()) {
107      x.para().b().tx("Generated Narrative");
108    }
109    if (context.isTechnicalMode()) {
110      renderResourceHeader(r, x);
111    }
112    try {
113      StructureDefinition sd = r.getDefinition();
114      if (sd == null) {
115        throw new FHIRException("Cannot find definition for "+r.fhirType());
116      } else {
117        ElementDefinition ed = sd.getSnapshot().getElement().get(0);
118        containedIds.clear();
119        hasExtensions = false;
120        generateByProfile(r, sd, r.root(), sd.getSnapshot().getElement(), ed, context.getProfileUtilities().getChildList(sd, ed), x, r.fhirType(), context.isTechnicalMode(), 0);
121      }
122    } catch (Exception e) {
123      System.out.println("Error Generating Narrative for "+r.fhirType()+"/"+r.getId()+": "+e.getMessage());
124      e.printStackTrace();
125      x.para().b().style("color: maroon").tx("Exception generating Narrative: "+e.getMessage());
126    }
127    return hasExtensions;
128  }
129
130
131  @Override
132  public String display(Resource r) throws UnsupportedEncodingException, IOException {
133    return "todo";
134  }
135  
136  @Override
137  public String display(ResourceWrapper r) throws UnsupportedEncodingException, IOException {
138    return "Not done yet";
139  }
140
141//
142//  public void inject(Element er, XhtmlNode x, NarrativeStatus status, boolean pretty) {
143//    if (!x.hasAttribute("xmlns"))
144//      x.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
145//    Element le = XMLUtil.getNamedChild(er, "language");
146//    String l = le == null ? null : le.getAttribute("value");
147//    if (!Utilities.noString(l)) {
148//      // use both - see https://www.w3.org/TR/i18n-html-tech-lang/#langvalues
149//      x.setAttribute("lang", l);
150//      x.setAttribute("xml:lang", l);
151//    }
152//    Element txt = XMLUtil.getNamedChild(er, "text");
153//    if (txt == null) {
154//      txt = er.getOwnerDocument().createElementNS(FormatUtilities.FHIR_NS, "text");
155//      Element n = XMLUtil.getFirstChild(er);
156//      while (n != null && (n.getNodeName().equals("id") || n.getNodeName().equals("meta") || n.getNodeName().equals("implicitRules") || n.getNodeName().equals("language")))
157//        n = XMLUtil.getNextSibling(n);
158//      if (n == null)
159//        er.appendChild(txt);
160//      else
161//        er.insertBefore(txt, n);
162//    }
163//    Element st = XMLUtil.getNamedChild(txt, "status");
164//    if (st == null) {
165//      st = er.getOwnerDocument().createElementNS(FormatUtilities.FHIR_NS, "status");
166//      Element n = XMLUtil.getFirstChild(txt);
167//      if (n == null)
168//        txt.appendChild(st);
169//      else
170//        txt.insertBefore(st, n);
171//    }
172//    st.setAttribute("value", status.toCode());
173//    Element div = XMLUtil.getNamedChild(txt, "div");
174//    if (div == null) {
175//      div = er.getOwnerDocument().createElementNS(FormatUtilities.XHTML_NS, "div");
176//      div.setAttribute("xmlns", FormatUtilities.XHTML_NS);
177//      txt.appendChild(div);
178//    }
179//    if (div.hasChildNodes())
180//      div.appendChild(er.getOwnerDocument().createElementNS(FormatUtilities.XHTML_NS, "hr"));
181//    new XhtmlComposer(XhtmlComposer.XML, pretty).compose(div, x);
182//  }
183//
184//  public void inject(org.hl7.fhir.r5.elementmodel.Element er, XhtmlNode x, NarrativeStatus status, boolean pretty) throws IOException, FHIRException {
185//    if (!x.hasAttribute("xmlns"))
186//      x.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
187//    String l = er.getChildValue("language");
188//    if (!Utilities.noString(l)) {
189//      // use both - see https://www.w3.org/TR/i18n-html-tech-lang/#langvalues
190//      x.setAttribute("lang", l);
191//      x.setAttribute("xml:lang", l);
192//    }
193//    org.hl7.fhir.r5.elementmodel.Element txt = er.getNamedChild("text");
194//    if (txt == null) {
195//      txt = new org.hl7.fhir.r5.elementmodel.Element("text", er.getProperty().getChild(null, "text"));
196//      int i = 0;
197//      while (i < er.getChildren().size() && (er.getChildren().get(i).getName().equals("id") || er.getChildren().get(i).getName().equals("meta") || er.getChildren().get(i).getName().equals("implicitRules") || er.getChildren().get(i).getName().equals("language")))
198//        i++;
199//      if (i >= er.getChildren().size())
200//        er.getChildren().add(txt);
201//      else
202//        er.getChildren().add(i, txt);
203//    }
204//    org.hl7.fhir.r5.elementmodel.Element st = txt.getNamedChild("status");
205//    if (st == null) {
206//      st = new org.hl7.fhir.r5.elementmodel.Element("status", txt.getProperty().getChild(null, "status"));
207//      txt.getChildren().add(0, st);
208//    }
209//    st.setValue(status.toCode());
210//    org.hl7.fhir.r5.elementmodel.Element div = txt.getNamedChild("div");
211//    if (div == null) {
212//      div = new org.hl7.fhir.r5.elementmodel.Element("div", txt.getProperty().getChild(null, "div"));
213//      txt.getChildren().add(div);
214//      div.setValue(new XhtmlComposer(XhtmlComposer.XML, pretty).compose(x));
215//    }
216//    div.setValue(x.toString());
217//    div.setXhtml(x);
218//  }
219//
220
221  
222  public void generateResourceSummary(XhtmlNode x, ResourceWrapper res, boolean textAlready, boolean showCodeDetails, boolean canLink) throws FHIRException, UnsupportedEncodingException, IOException {
223    if (!textAlready) {
224      XhtmlNode div = res.getNarrative();
225      if (div != null) {
226        if (div.allChildrenAreText())
227          x.getChildNodes().addAll(div.getChildNodes());
228        if (div.getChildNodes().size() == 1 && div.getChildNodes().get(0).allChildrenAreText())
229          x.getChildNodes().addAll(div.getChildNodes().get(0).getChildNodes());
230      }
231      x.tx("Generated Summary: ");
232    }
233    String path = res.fhirType();
234    StructureDefinition profile = getContext().getWorker().fetchResource(StructureDefinition.class, path);
235    if (profile == null)
236      x.tx("unknown resource " +path);
237    else {
238      boolean firstElement = true;
239      boolean last = false;
240      for (PropertyWrapper p : res.children()) {
241        if (!ignoreProperty(p) && !p.getElementDefinition().getBase().getPath().startsWith("Resource.")) {
242          ElementDefinition child = getElementDefinition(profile.getSnapshot().getElement(), path+"."+p.getName(), p);
243          if (p.getValues().size() > 0 && p.getValues().get(0) != null && child != null && isPrimitive(child) && includeInSummary(child, p.getValues())) {
244            if (firstElement)
245              firstElement = false;
246            else if (last)
247              x.tx("; ");
248            boolean first = true;
249            last = false;
250            for (BaseWrapper v : p.getValues()) {
251              if (first)
252                first = false;
253              else if (last)
254                x.tx(", ");
255              last = displayLeaf(res, v, child, x, p.getName(), showCodeDetails, canLink) || last;
256            }
257          }
258        }
259      }
260    }
261  }
262
263
264  private boolean ignoreProperty(PropertyWrapper p) {
265    return Utilities.existsInList(p.getName(), "contained");
266  }
267
268  private boolean includeInSummary(ElementDefinition child, List<BaseWrapper> list) throws UnsupportedEncodingException, FHIRException, IOException {
269    if (child.getName().endsWith("active") && list != null && list.size() > 0 && "true".equals(list.get(0).getBase().primitiveValue())) {
270      return false;
271    }
272    if (child.getIsModifier())
273      return true;
274    if (child.getMustSupport())
275      return true;
276    if (child.getType().size() == 1) {
277      String t = child.getType().get(0).getWorkingCode();
278      if (t.equals("Address") || t.equals("Contact") || t.equals("Reference") || t.equals("Uri") || t.equals("Url") || t.equals("Canonical"))
279        return false;
280    }
281    return true;
282  }
283  
284  private ElementDefinition getElementDefinition(List<ElementDefinition> elements, String path, PropertyWrapper p) {
285    for (ElementDefinition element : elements)
286      if (element.getPath().equals(path))
287        return element;
288    if (path.endsWith("\"]") && p.getStructure() != null)
289      return p.getStructure().getSnapshot().getElement().get(0);
290    return null;
291  }
292
293  private void renderLeaf(ResourceWrapper res, BaseWrapper ew, ElementDefinition defn, XhtmlNode parent, XhtmlNode x, boolean title, boolean showCodeDetails, Map<String, String> displayHints, String path, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome {
294    if (ew == null)
295      return;
296
297    Base e = ew.getBase();
298
299    if (e instanceof StringType)
300      x.addText(((StringType) e).getValue());
301    else if (e instanceof CodeType)
302      x.addText(((CodeType) e).getValue());
303    else if (e instanceof IdType)
304      x.addText(((IdType) e).getValue());
305    else if (e instanceof Extension)
306      return;
307    else if (e instanceof InstantType)
308      x.addText(((InstantType) e).toHumanDisplay());
309    else if (e instanceof DateTimeType) {
310      renderDateTime(x, e);
311    } else if (e instanceof Base64BinaryType)
312      x.addText(new Base64().encodeAsString(((Base64BinaryType) e).getValue()));
313    else if (e instanceof org.hl7.fhir.r5.model.DateType) {
314      org.hl7.fhir.r5.model.DateType dt = ((org.hl7.fhir.r5.model.DateType) e);
315      renderDate(x, dt);
316    } else if (e instanceof Enumeration) {
317      Object ev = ((Enumeration<?>) e).getValue();
318      x.addText(ev == null ? "" : ev.toString()); // todo: look up a display name if there is one
319    } else if (e instanceof BooleanType) {
320      x.addText(((BooleanType) e).getValue().toString());
321    } else if (e instanceof CodeableConcept) {
322      renderCodeableConcept(x, (CodeableConcept) e, showCodeDetails);
323    } else if (e instanceof Coding) {
324      renderCoding(x, (Coding) e, showCodeDetails);
325    } else if (e instanceof CodeableReference) {
326      renderCodeableReference(x, (CodeableReference) e, showCodeDetails);
327    } else if (e instanceof Annotation) {
328      renderAnnotation(x, (Annotation) e);
329    } else if (e instanceof Identifier) {
330      renderIdentifier(x, (Identifier) e);
331    } else if (e instanceof org.hl7.fhir.r5.model.IntegerType) {
332      if (((org.hl7.fhir.r5.model.IntegerType) e).hasValue()) {
333        x.addText(Integer.toString(((org.hl7.fhir.r5.model.IntegerType) e).getValue()));
334      } else {
335        x.addText("??");
336      }
337    } else if (e instanceof org.hl7.fhir.r5.model.Integer64Type) {
338      if (((org.hl7.fhir.r5.model.Integer64Type) e).hasValue()) {
339        x.addText(Long.toString(((org.hl7.fhir.r5.model.Integer64Type) e).getValue()));
340      } else {
341        x.addText("??");
342      }
343    } else if (e instanceof org.hl7.fhir.r5.model.DecimalType) {
344      x.addText(((org.hl7.fhir.r5.model.DecimalType) e).getValue().toString());
345    } else if (e instanceof HumanName) {
346      renderHumanName(x, (HumanName) e);
347    } else if (e instanceof SampledData) {
348      renderSampledData(x, (SampledData) e);
349    } else if (e instanceof Address) {
350      renderAddress(x, (Address) e);
351    } else if (e instanceof ContactPoint) {
352      renderContactPoint(x, (ContactPoint) e);
353    } else if (e instanceof Expression) {
354      renderExpression(x, (Expression) e);
355    } else if (e instanceof Money) {
356      renderMoney(x, (Money) e);
357    } else if (e instanceof ContactDetail) {
358      ContactDetail cd = (ContactDetail) e;
359      if (cd.hasName()) {
360        x.tx(cd.getName()+": ");
361      }
362      boolean first = true;
363      for (ContactPoint c : cd.getTelecom()) {
364        if (first) first = false; else x.tx(",");
365        renderContactPoint(x, c);
366      }
367    } else if (e instanceof UriType) {
368      renderUri(x, (UriType) e, defn.getPath(), rcontext != null && rcontext.getResourceResource() != null ? rcontext.getResourceResource().getId() : null);
369    } else if (e instanceof Timing) {
370      renderTiming(x, (Timing) e);
371    } else if (e instanceof Range) {
372      renderRange(x, (Range) e);
373    } else if (e instanceof Quantity) {
374      renderQuantity(x, (Quantity) e, showCodeDetails);
375    } else if (e instanceof Ratio) {
376      renderQuantity(x, ((Ratio) e).getNumerator(), showCodeDetails);
377      x.tx("/");
378      renderQuantity(x, ((Ratio) e).getDenominator(), showCodeDetails);
379    } else if (e instanceof Period) {
380      Period p = (Period) e;
381      renderPeriod(x, p);
382    } else if (e instanceof Reference) {
383      Reference r = (Reference) e;
384      if (r.getReference() != null && r.getReference().contains("#")) {
385        if (containedIds.contains(r.getReference().substring(1))) {
386          x.ah(r.getReference()).tx("See "+r.getReference());
387        } else {
388          // in this case, we render the resource in line
389          ResourceWrapper rw = null;
390          for (ResourceWrapper t : res.getContained()) {
391            if (r.getReference().substring(1).equals(t.getId())) {
392              rw = t;
393            }
394          }
395          if (rw == null) {
396            renderReference(res, x, r);
397          } else {
398            x.an(rw.getId());
399            ResourceRenderer rr = RendererFactory.factory(rw, context.copy().setAddGeneratedNarrativeHeader(false));
400            rr.render(parent.blockquote(), rw);
401          }
402        }
403      } else {
404        renderReference(res, x, r);
405      }
406    } else if (e instanceof Resource) {
407      return;
408    } else if (e instanceof DataRequirement) {
409      DataRequirement p = (DataRequirement) e;
410      renderDataRequirement(x, p);
411    } else if (e instanceof PrimitiveType) {
412      x.tx(((PrimitiveType) e).primitiveValue());
413    } else if (e instanceof ElementDefinition) {
414      x.tx("todo-bundle");
415    } else if (e != null && !(e instanceof Attachment) && !(e instanceof Narrative) && !(e instanceof Meta)) {
416      throw new NotImplementedException("type "+e.getClass().getName()+" not handled - should not be here");
417    }
418  }
419
420  private boolean displayLeaf(ResourceWrapper res, BaseWrapper ew, ElementDefinition defn, XhtmlNode x, String name, boolean showCodeDetails) throws FHIRException, UnsupportedEncodingException, IOException {
421    return displayLeaf(res, ew, defn, x, name, showCodeDetails, true);
422  }
423  
424  private boolean displayLeaf(ResourceWrapper res, BaseWrapper ew, ElementDefinition defn, XhtmlNode x, String name, boolean showCodeDetails, boolean allowLinks) throws FHIRException, UnsupportedEncodingException, IOException {
425    if (ew == null)
426      return false;
427    Base e = ew.getBase();
428    if (e == null)
429      return false;
430
431    Map<String, String> displayHints = readDisplayHints(defn);
432
433    if (name.endsWith("[x]"))
434      name = name.substring(0, name.length() - 3);
435
436    if (!showCodeDetails && e instanceof PrimitiveType && isDefault(displayHints, ((PrimitiveType) e)))
437      return false;
438
439    if (e instanceof StringType) {
440      x.addText(name+": "+((StringType) e).getValue());
441      return true;
442    } else if (e instanceof CodeType) {
443      x.addText(name+": "+((CodeType) e).getValue());
444      return true;
445    } else if (e instanceof IdType) {
446      x.addText(name+": "+((IdType) e).getValue());
447      return true;
448    } else if (e instanceof UriType) {
449      if (Utilities.isAbsoluteUrlLinkable(((UriType) e).getValue()) && allowLinks) {
450        x.tx(name+": ");
451        x.ah(((UriType) e).getValue()).addText(((UriType) e).getValue());
452      } else {
453        x.addText(name+": "+((UriType) e).getValue());
454      }
455      return true;
456    } else if (e instanceof DateTimeType) {
457      x.addText(name+": "+((DateTimeType) e).toHumanDisplay());
458      return true;
459    } else if (e instanceof InstantType) {
460      x.addText(name+": "+((InstantType) e).toHumanDisplay());
461      return true;
462    } else if (e instanceof Extension) {
463      //      x.tx("Extensions: todo");
464      return false;
465    } else if (e instanceof org.hl7.fhir.r5.model.DateType) {
466      x.addText(name+": "+((org.hl7.fhir.r5.model.DateType) e).toHumanDisplay());
467      return true;
468    } else if (e instanceof Enumeration) {
469      x.addText(((Enumeration<?>) e).getValue().toString()); // todo: look up a display name if there is one
470      return true;
471    } else if (e instanceof BooleanType) {
472      if (((BooleanType) e).hasValue()) {
473        x.addText(name);
474        x.addText(": ");
475        x.addText(((BooleanType) e).getValueAsString());
476        return true;
477      }
478    } else if (e instanceof CodeableReference) {
479      if (((CodeableReference) e).hasReference()) { 
480        Reference r = ((CodeableReference) e).getReference();
481        renderReference(res, x, r, allowLinks);
482      } else {
483        renderCodeableConcept(x, ((CodeableReference) e).getConcept(), showCodeDetails);
484      }
485      return true;
486    } else if (e instanceof CodeableConcept) {
487      renderCodeableConcept(x, (CodeableConcept) e, showCodeDetails);
488      return true;
489    } else if (e instanceof Coding) {
490      renderCoding(x, (Coding) e, showCodeDetails);
491      return true;
492    } else if (e instanceof Annotation) {
493      renderAnnotation(x, (Annotation) e, showCodeDetails);
494      return true;
495    } else if (e instanceof org.hl7.fhir.r5.model.IntegerType) {
496      x.addText(Integer.toString(((org.hl7.fhir.r5.model.IntegerType) e).getValue()));
497      return true;
498    } else if (e instanceof org.hl7.fhir.r5.model.DecimalType) {
499      x.addText(((org.hl7.fhir.r5.model.DecimalType) e).getValue().toString());
500      return true;
501    } else if (e instanceof Identifier) {
502      renderIdentifier(x, (Identifier) e);
503      return true;
504    } else if (e instanceof HumanName) {
505      renderHumanName(x, (HumanName) e);
506      return true;
507    } else if (e instanceof SampledData) {
508      renderSampledData(x, (SampledData) e);
509      return true;
510    } else if (e instanceof Address) {
511      renderAddress(x, (Address) e);
512      return true;
513    } else if (e instanceof ContactPoint) {
514      if (allowLinks) {
515        renderContactPoint(x, (ContactPoint) e);
516      } else {
517        displayContactPoint(x, (ContactPoint) e);
518      }
519      return true;
520    } else if (e instanceof Timing) {
521      renderTiming(x, (Timing) e);
522      return true;
523    } else if (e instanceof Quantity) {
524      renderQuantity(x, (Quantity) e, showCodeDetails);
525      return true;
526    } else if (e instanceof Ratio) {
527      renderQuantity(x, ((Ratio) e).getNumerator(), showCodeDetails);
528      x.tx("/");
529      renderQuantity(x, ((Ratio) e).getDenominator(), showCodeDetails);
530      return true;
531    } else if (e instanceof Period) {
532      Period p = (Period) e;
533      x.addText(name+": ");
534      x.addText(!p.hasStart() ? "?ngen-2?" : p.getStartElement().toHumanDisplay());
535      x.tx(" --> ");
536      x.addText(!p.hasEnd() ? "(ongoing)" : p.getEndElement().toHumanDisplay());
537      return true;
538    } else if (e instanceof Reference) {
539      Reference r = (Reference) e;
540      if (r.hasDisplayElement())
541        x.addText(r.getDisplay());
542      else if (r.hasReferenceElement()) {
543        ResourceWithReference tr = resolveReference(res, r.getReference());
544        x.addText(tr == null ? r.getReference() : "?ngen-3"); // getDisplayForReference(tr.getReference()));
545      } else
546        x.tx("?ngen-4?");
547      return true;
548    } else if (e instanceof Narrative) {
549      return false;
550    } else if (e instanceof Resource) {
551      return false;
552    } else if (e instanceof ContactDetail) {
553      ContactDetail cd = (ContactDetail) e;
554      if (cd.hasName()) {
555        x.tx(cd.getName()+": ");
556      }
557      boolean first = true;
558      for (ContactPoint c : cd.getTelecom()) {
559        if (first) first = false; else x.tx(",");
560        if (allowLinks) {      
561          renderContactPoint(x, c);
562        } else {
563          displayContactPoint(x, c);
564        }
565      }
566      return true;
567    } else if (e instanceof Range) {
568      return false;
569    } else if (e instanceof Meta) {
570      return false;
571    } else if (e instanceof Dosage) {
572      return false;
573    } else if (e instanceof Signature) {
574      return false;
575    } else if (e instanceof UsageContext) {
576      return false;
577    } else if (e instanceof RelatedArtifact) {
578      return false;
579    } else if (e instanceof ElementDefinition) {
580      return false;
581    } else if (e instanceof Base64BinaryType) {
582      return false;
583    } else if (!(e instanceof Attachment))
584      throw new NotImplementedException("type "+e.getClass().getName()+" not handled yet");
585    return false;
586  }
587
588
589
590  private boolean isPrimitive(ElementDefinition e) {
591    //we can tell if e is a primitive because it has types
592    if (e.getType().isEmpty()) {
593      return false;
594    }
595    if (e.getType().size() == 1 && isBase(e.getType().get(0).getWorkingCode())) {
596      return false;
597    }
598    if (e.getType().size() > 1) {
599      return true;
600    }
601    StructureDefinition sd = context.getWorker().fetchTypeDefinition(e.getTypeFirstRep().getCode());
602    if (sd != null) {
603      if (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) {
604        return true;
605      }
606      if (sd.getKind() == StructureDefinitionKind.COMPLEXTYPE) {
607        if (Utilities.existsInList(e.getTypeFirstRep().getCode(), "Extension", "CodeableConcept", "Coding", "Annotation", "Identifier", "HumanName", "SampledData", 
608            "Address", "ContactPoint", "ContactDetail", "Timing", "Range", "Quantity", "Ratio", "Period", "Reference")) {
609          return true;
610        }        
611      }
612    }
613    return false;
614  }
615
616  private boolean isBase(String code) {
617    return code.equals("Element") || code.equals("BackboneElement");
618  }
619  
620  private List<ElementDefinition> getChildrenForPath(List<ElementDefinition> elements, String path) throws DefinitionException {
621    // do we need to do a name reference substitution?
622    for (ElementDefinition e : elements) {
623      if (e.getPath().equals(path) && e.hasContentReference()) {
624        String ref = e.getContentReference();
625        ElementDefinition t = null;
626        // now, resolve the name
627        for (ElementDefinition e1 : elements) {
628          if (ref.equals("#"+e1.getId()))
629            t = e1;
630        }
631        if (t == null)
632          throw new DefinitionException("Unable to resolve content reference "+ref+" trying to resolve "+path);
633        path = t.getPath();
634        break;
635      }
636    }
637
638    List<ElementDefinition> results = new ArrayList<ElementDefinition>();
639    for (ElementDefinition e : elements) {
640      if (e.getPath().startsWith(path+".") && !e.getPath().substring(path.length()+1).contains("."))
641        results.add(e);
642    }
643    return results;
644  }
645
646
647  private boolean generateByProfile(StructureDefinition profile, boolean showCodeDetails) {
648    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
649    if(context.isAddGeneratedNarrativeHeader()) {
650      x.para().b().tx("Generated Narrative"+(showCodeDetails ? " with Details" : ""));
651    }
652    try {
653      generateByProfile(rcontext.getResourceResource(), profile, rcontext.getResourceResource(), profile.getSnapshot().getElement(), profile.getSnapshot().getElement().get(0), getChildrenForPath(profile.getSnapshot().getElement(), rcontext.getResourceResource().getResourceType().toString()), x, rcontext.getResourceResource().getResourceType().toString(), showCodeDetails);
654    } catch (Exception e) {
655      e.printStackTrace();
656      x.para().b().style("color: maroon").tx("Exception generating Narrative: "+e.getMessage());
657    }
658    inject(rcontext.getResourceResource(), x,  NarrativeStatus.GENERATED);
659    return true;
660  }
661
662  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, EOperationOutcome {
663    generateByProfile(new ResourceWrapperDirect(this.context, res), profile, new BaseWrapperDirect(this.context, e), allElements, defn, children, x, path, showCodeDetails, 0);
664  }
665
666  private void generateByProfile(ResourceWrapper res, StructureDefinition profile, BaseWrapper e, List<ElementDefinition> allElements, ElementDefinition defn, List<ElementDefinition> children,  XhtmlNode x, String path, boolean showCodeDetails, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome {
667    if (children.isEmpty()) {
668      renderLeaf(res, e, defn, x, x, false, showCodeDetails, readDisplayHints(defn), path, indent);
669    } else {
670      for (PropertyWrapper p : splitExtensions(profile, e.children())) {
671        if (p.hasValues()) {
672          ElementDefinition child = getElementDefinition(children, path+"."+p.getName(), p);
673          if (child == null) {
674            child = p.getElementDefinition();
675          }
676          if (child != null) {
677            if (!child.getBase().hasPath() || !child.getBase().getPath().startsWith("Resource.")) {
678              generateElementByProfile(res, profile, allElements, x, path, showCodeDetails, indent, p, child);
679            }
680          }
681        }
682      }
683    }
684  }
685
686  public void generateElementByProfile(ResourceWrapper res, StructureDefinition profile, List<ElementDefinition> allElements, XhtmlNode x, String path,
687      boolean showCodeDetails, int indent, PropertyWrapper p, ElementDefinition child) throws UnsupportedEncodingException, IOException, EOperationOutcome {
688    Map<String, String> displayHints = readDisplayHints(child);
689    if ("DomainResource.contained".equals(child.getBase().getPath())) {
690//              if (p.getValues().size() > 0 && child != null) {
691//                for (BaseWrapper v : p.getValues()) {
692//                  x.an(v.get("id").primitiveValue());
693//                }
694//              }
695    } else if (!exemptFromRendering(child)) {
696      if (isExtension(p)) {
697        hasExtensions = true;
698      }
699      List<ElementDefinition> grandChildren = getChildrenForPath(allElements, path+"."+p.getName());
700      filterGrandChildren(grandChildren, path+"."+p.getName(), p);
701      if (p.getValues().size() > 0) {
702         if (isPrimitive(child)) {
703           XhtmlNode para = x.isPara() ? para = x : x.para();
704           String name = p.getName();
705           if (name.endsWith("[x]"))
706             name = name.substring(0, name.length() - 3);
707           if (showCodeDetails || !isDefaultValue(displayHints, p.getValues())) {
708             para.b().addText(name);
709             para.tx(": ");
710             if (renderAsList(child) && p.getValues().size() > 1) {
711               XhtmlNode list = x.ul();
712               for (BaseWrapper v : p.getValues())
713                 renderLeaf(res, v, child, x, list.li(), false, showCodeDetails, displayHints, path, indent);
714             } else {
715               boolean first = true;
716               for (BaseWrapper v : p.getValues()) {
717                 if (first) {
718                   first = false;
719                 } else {
720                   para.tx(", ");
721                 }
722                 renderLeaf(res, v, child, x, para, false, showCodeDetails, displayHints, path, indent);
723               }
724             }
725           }
726        } else if (canDoTable(path, p, grandChildren, x)) {
727          XhtmlNode xn = new XhtmlNode(NodeType.Element, getHeader());
728          xn.addText(Utilities.capitalize(Utilities.camelCase(Utilities.pluralizeMe(p.getName()))));
729          XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table"); 
730          tbl.setAttribute("class", "grid");
731          XhtmlNode tr = tbl.tr();
732          tr.td().tx("-"); // work around problem with empty table rows
733          boolean add = addColumnHeadings(tr, grandChildren);          
734          for (BaseWrapper v : p.getValues()) {
735            if (v != null) {
736              tr = tbl.tr();
737              tr.td().tx("*"); // work around problem with empty table rows
738              add = addColumnValues(res, tr, grandChildren, v, showCodeDetails, displayHints, path, indent) || add;
739            }
740          }
741          if (add) {
742            x.add(xn);
743            x.add(tbl);
744          }
745        } else if (isExtension(p)) {
746          for (BaseWrapper v : p.getValues()) {
747            if (v != null) {
748              PropertyWrapper vp = v.getChildByName("value");
749              PropertyWrapper ev = v.getChildByName("extension");
750              if (vp.hasValues()) {
751                BaseWrapper vv = vp.value();
752                XhtmlNode para = x.para();
753                para.b().addText(p.getStructure().present());
754                para.tx(": ");
755                renderLeaf(res, vv, child, x, para, false, showCodeDetails, displayHints, path, indent);
756              } else if (ev.hasValues()) {
757                XhtmlNode bq = x.addTag("blockquote");                
758                bq.para().b().addText(isExtension(p) ? p.getStructure().present() : p.getName());
759                for (BaseWrapper vv : ev.getValues()) {
760                  StructureDefinition ex = context.getWorker().fetchTypeDefinition("Extension");
761                  List<ElementDefinition> children = getChildrenForPath(ex.getSnapshot().getElement(), "Extension");
762                  generateByProfile(res, ex, vv, allElements, child, children, bq, "Extension", showCodeDetails, indent+1);
763                }
764              }
765            }
766          }          
767        } else {
768          for (BaseWrapper v : p.getValues()) {
769            if (v != null) {
770              XhtmlNode bq = x.addTag("blockquote");
771              bq.para().b().addText(isExtension(p) ? p.getStructure().present() : p.getName());
772              generateByProfile(res, profile, v, allElements, child, grandChildren, bq, path+"."+p.getName(), showCodeDetails, indent+1);
773            }
774          }
775        }
776      }
777    }
778  }
779
780
781  private String getHeader() {
782    int i = 3;
783    while (i <= context.getHeaderLevelContext())
784      i++;
785    if (i > 6)
786      i = 6;
787    return "h"+Integer.toString(i);
788  }
789
790  private List<PropertyWrapper> getValues(String path, PropertyWrapper p, ElementDefinition e) {
791    List<PropertyWrapper> res = new ArrayList<PropertyWrapper>();
792    for (BaseWrapper v : p.getValues()) {
793      for (PropertyWrapper g : v.children()) {
794        if ((path+"."+p.getName()+"."+g.getName()).equals(e.getPath()))
795          res.add(p);
796      }
797    }
798    return res;
799  }
800  
801  private boolean canDoTable(String path, PropertyWrapper p, List<ElementDefinition> grandChildren, XhtmlNode x) {
802    if (isExtension(p)) {
803      return false;
804    }
805    if (x.getName().equals("p")) {
806      return false;
807    }
808
809    for (ElementDefinition e : grandChildren) {
810      List<PropertyWrapper> values = getValues(path, p, e);
811      if (values.size() > 1 || !isPrimitive(e) || !canCollapse(e))
812        return false;
813    }
814    return true;
815  }
816
817  public boolean isExtension(PropertyWrapper p) {
818    return p.getName().contains("extension[");
819  }
820
821
822  private boolean canCollapse(ElementDefinition e) {
823    // we can collapse any data type
824    return !e.getType().isEmpty();
825  }
826  private boolean exemptFromRendering(ElementDefinition child) {
827    if (child == null)
828      return false;
829    if ("Composition.subject".equals(child.getPath()))
830      return true;
831    if ("Composition.section".equals(child.getPath()))
832      return true;
833    return false;
834  }
835
836  private boolean renderAsList(ElementDefinition child) {
837    if (child.getType().size() == 1) {
838      String t = child.getType().get(0).getWorkingCode();
839      if (t.equals("Address") || t.equals("Reference"))
840        return true;
841    }
842    return false;
843  }
844
845  private boolean addColumnHeadings(XhtmlNode tr, List<ElementDefinition> grandChildren) {
846    boolean b = false;
847    for (ElementDefinition e : grandChildren) {
848      b = true;
849      tr.td().b().addText(Utilities.capitalize(tail(e.getPath())));
850    }
851    return b;
852  }
853
854  private boolean addColumnValues(ResourceWrapper res, XhtmlNode tr, List<ElementDefinition> grandChildren, BaseWrapper v, boolean showCodeDetails, Map<String, String> displayHints, String path, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome {
855    boolean b = false;
856    for (ElementDefinition e : grandChildren) {
857      PropertyWrapper p = v.getChildByName(e.getPath().substring(e.getPath().lastIndexOf(".")+1));
858      XhtmlNode td = tr.td();
859      if (p == null || p.getValues().size() == 0 || p.getValues().get(0) == null) {
860        b = true;
861        td.tx(" ");
862      } else {
863        for (BaseWrapper vv : p.getValues()) {
864          b = true;
865          td.sep(", ");
866          renderLeaf(res, vv, e, td, td, false, showCodeDetails, displayHints, path, indent);
867        }
868      }
869    }
870    return b;
871  }
872
873  private void filterGrandChildren(List<ElementDefinition> grandChildren,  String string, PropertyWrapper prop) {
874    List<ElementDefinition> toRemove = new ArrayList<ElementDefinition>();
875    toRemove.addAll(grandChildren);
876    for (BaseWrapper b : prop.getValues()) {
877      List<ElementDefinition> list = new ArrayList<ElementDefinition>();
878      for (ElementDefinition ed : toRemove) {
879        PropertyWrapper p = b.getChildByName(tail(ed.getPath()));
880        if (p != null && p.hasValues())
881          list.add(ed);
882      }
883      toRemove.removeAll(list);
884    }
885    grandChildren.removeAll(toRemove);
886  }
887
888  private List<PropertyWrapper> splitExtensions(StructureDefinition profile, List<PropertyWrapper> children) throws UnsupportedEncodingException, IOException, FHIRException {
889    List<PropertyWrapper> results = new ArrayList<PropertyWrapper>();
890    Map<String, PropertyWrapper> map = new HashMap<String, PropertyWrapper>();
891    for (PropertyWrapper p : children)
892      if (p.getName().equals("extension") || p.getName().equals("modifierExtension")) {
893        // we're going to split these up, and create a property for each url
894        if (p.hasValues()) {
895          for (BaseWrapper v : p.getValues()) {
896            Extension ex  = (Extension) v.getBase();
897            String url = ex.getUrl();
898            StructureDefinition ed = getContext().getWorker().fetchResource(StructureDefinition.class, url);
899            if (ed == null) {
900              if (xverManager == null) {
901                xverManager = new XVerExtensionManager(context.getWorker());
902              }
903              if (xverManager.matchingUrl(url) && xverManager.status(url) == XVerExtensionStatus.Valid) {
904                ed = xverManager.makeDefinition(url);
905                getContext().getWorker().generateSnapshot(ed);
906                getContext().getWorker().cacheResource(ed);
907              }
908            }
909            if (p.getName().equals("modifierExtension") && ed == null) {
910              throw new DefinitionException("Unknown modifier extension "+url);
911            }
912            PropertyWrapper pe = map.get(p.getName()+"["+url+"]");
913            if (pe == null) {
914              if (ed == null) {
915                if (url.startsWith("http://hl7.org/fhir") && !url.startsWith("http://hl7.org/fhir/us")) {
916                  throw new DefinitionException("unknown extension "+url);
917                }
918                // System.out.println("unknown extension "+url);
919                pe = new PropertyWrapperDirect(this.context, new Property(p.getName()+"["+url+"]", p.getTypeCode(), p.getDefinition(), p.getMinCardinality(), p.getMaxCardinality(), ex), null);
920              } else {
921                ElementDefinition def = ed.getSnapshot().getElement().get(0);
922                pe = new PropertyWrapperDirect(this.context, new Property(p.getName()+"["+url+"]", "Extension", def.getDefinition(), def.getMin(), def.getMax().equals("*") ? Integer.MAX_VALUE : Integer.parseInt(def.getMax()), ex), ed.getSnapshot().getElementFirstRep());
923                ((PropertyWrapperDirect) pe).getWrapped().setStructure(ed);
924              }
925              results.add(pe);
926            } else
927              pe.getValues().add(v);
928          }
929        }
930      } else
931        results.add(p);
932    return results;
933  }
934
935
936  private Map<String, String> readDisplayHints(ElementDefinition defn) throws DefinitionException {
937    Map<String, String> hints = new HashMap<String, String>();
938    if (defn != null) {
939      String displayHint = ToolingExtensions.getDisplayHint(defn);
940      if (!Utilities.noString(displayHint)) {
941        String[] list = displayHint.split(";");
942        for (String item : list) {
943          String[] parts = item.split(":");
944          if (parts.length == 1) {
945            hints.put("value", parts[0].trim());            
946          } else {
947            if (parts.length != 2) {
948              throw new DefinitionException("error reading display hint: '"+displayHint+"'");
949            }
950            hints.put(parts[0].trim(), parts[1].trim());
951          }
952        }
953      }
954    }
955    return hints;
956  }
957
958  @SuppressWarnings("rawtypes")
959  private boolean isDefaultValue(Map<String, String> displayHints, List<BaseWrapper> list) throws UnsupportedEncodingException, IOException, FHIRException {
960    if (list.size() != 1)
961      return false;
962    if (list.get(0).getBase() instanceof PrimitiveType)
963      return isDefault(displayHints, (PrimitiveType) list.get(0).getBase());
964    else
965      return false;
966  }
967
968  private boolean isDefault(Map<String, String> displayHints, PrimitiveType primitiveType) {
969    String v = primitiveType.asStringValue();
970    if (!Utilities.noString(v) && displayHints.containsKey("default") && v.equals(displayHints.get("default")))
971      return true;
972    return false;
973  }
974
975
976  protected String tail(String path) {
977    return path.substring(path.lastIndexOf(".")+1);
978  }
979
980  public boolean canRender(Resource resource) {
981    return context.getWorker().getResourceNames().contains(resource.fhirType());
982  }
983
984}