001package org.hl7.fhir.r5.renderers;
002
003import java.io.File;
004import java.io.IOException;
005import java.io.UnsupportedEncodingException;
006import java.util.ArrayList;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Map;
011import java.util.Set;
012
013import org.apache.poi.ss.formula.eval.ExternalNameEval;
014import org.hl7.fhir.exceptions.DefinitionException;
015import org.hl7.fhir.exceptions.FHIRException;
016import org.hl7.fhir.exceptions.FHIRFormatError;
017import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
018import org.hl7.fhir.r5.extensions.ExtensionUtilities;
019import org.hl7.fhir.r5.model.Base;
020import org.hl7.fhir.r5.model.CanonicalResource;
021import org.hl7.fhir.r5.model.CanonicalType;
022import org.hl7.fhir.r5.model.CodeSystem;
023import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
024import org.hl7.fhir.r5.model.Constants;
025import org.hl7.fhir.r5.model.ContactDetail;
026import org.hl7.fhir.r5.model.ContactPoint;
027import org.hl7.fhir.r5.model.ContactPoint.ContactPointSystem;
028import org.hl7.fhir.r5.model.DateTimeType;
029import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
030import org.hl7.fhir.r5.model.Extension;
031import org.hl7.fhir.r5.model.Narrative.NarrativeStatus;
032import org.hl7.fhir.r5.model.Reference;
033import org.hl7.fhir.r5.model.Resource;
034import org.hl7.fhir.r5.model.StructureDefinition;
035import org.hl7.fhir.r5.model.UriType;
036import org.hl7.fhir.r5.renderers.utils.RenderingContext;
037import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceReferenceKind;
038import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceWithReference;
039import org.hl7.fhir.r5.renderers.utils.ResourceWrapper;
040import org.hl7.fhir.r5.renderers.utils.ResourceWrapper.ElementKind;
041import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
042import org.hl7.fhir.r5.utils.EOperationOutcome;
043
044import org.hl7.fhir.r5.utils.XVerExtensionManager;
045import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
046import org.hl7.fhir.utilities.Utilities;
047import org.hl7.fhir.utilities.VersionUtilities;
048import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
049import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece;
050import org.hl7.fhir.utilities.xhtml.NodeType;
051import org.hl7.fhir.utilities.xhtml.XhtmlNode;
052
053@MarkedToMoveToAdjunctPackage
054public abstract class ResourceRenderer extends DataRenderer {
055
056  private static final String EXT_NS_URL = "http://hl7.org/fhir/5.0/StructureDefinition/extension-NamingSystem.url";
057
058  public enum RendererType {
059    NATIVE, PROFILE, LIQUID
060
061  }
062
063  protected XVerExtensionManager xverManager;
064  protected boolean multiLangMode;
065  protected boolean inner;
066  
067  
068  public ResourceRenderer(RenderingContext context) {
069    super(context);
070  }
071  
072  public boolean isMultiLangMode() {
073    return multiLangMode;
074  }
075
076  public ResourceRenderer setMultiLangMode(boolean multiLangMode) {
077    this.multiLangMode = multiLangMode;
078    return this;
079  }
080
081  public boolean renderingUsesValidation() {
082    return false;
083  }
084  
085  public boolean isInner() {
086    return inner;
087  }
088
089  public ResourceRenderer setInner(boolean inner) {
090    this.inner = inner;
091    return this;
092  }
093
094  /**
095   * Just build the narrative that would go in the resource (per @renderResource()), but don't put it in the resource
096   * @param dr
097   * @return
098   * @throws FHIRFormatError
099   * @throws DefinitionException
100   * @throws FHIRException
101   * @throws IOException
102   * @throws EOperationOutcome
103   */
104  public XhtmlNode buildNarrative(ResourceWrapper dr) throws FHIRFormatError, DefinitionException, FHIRException, IOException, EOperationOutcome {
105    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
106    buildNarrative(new RenderingStatus(), x, dr);
107    checkForDuplicateIds(x);
108    return x;
109  }
110  
111  private void checkForDuplicateIds(XhtmlNode x) {
112    if (context.isTrackNarrativeSource()) {
113      Set<String> ids = new HashSet<>();
114      checkForDuplicateIds(ids, x);
115    }
116  }
117
118  private void checkForDuplicateIds(Set<String> ids, XhtmlNode x) {
119    if (x.hasAttribute("id")) {
120      String id = x.getAttribute("id");
121      if (ids.contains(id)) {
122        throw new Error("Duplicate id '"+id+"' on "+x.allText());
123      } else {
124        ids.add(id);
125      }
126    }
127    if (x.hasChildren()) {
128      for (XhtmlNode c : x.getChildNodes()) {
129        checkForDuplicateIds(ids, c);
130      }
131    }
132    
133  }
134
135  /**
136   * given a resource, update it's narrative with the best rendering available. 
137   * 
138   * ResourceWrapper is a facade to either a org.hl7.fhir.r5.model Resource, or
139   * to a org.hl7.fhir.r5.elementModel (which might a resource of any version). 
140   * 
141   * Note that some resource renderers - only canonical ones - only render native
142   * resources, and not element model ones. These may be migrated in the future 
143   * (only reason not to is the sheer size of the task, though performance might 
144   * be a factor)
145   *  
146   * @param r - the domain resource in question
147   * 
148   * @throws IOException
149   * @throws EOperationOutcome 
150   * @throws FHIRException 
151   */
152  public void renderResource(ResourceWrapper r) throws IOException, FHIRException, EOperationOutcome {  
153    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
154    RenderingStatus status = new RenderingStatus();
155    buildNarrative(status, x, r);
156    String an = r.fhirType()+"_"+r.getId();
157    if (context.isAddName()) {
158      if (!hasAnchorName(x, an)) {
159        injectAnchorName(x, an);
160      }
161    }
162    inject(r, x, status.getExtensions() ? NarrativeStatus.EXTENSIONS :  NarrativeStatus.GENERATED);
163  }
164
165  public XhtmlNode checkNarrative(ResourceWrapper r) throws IOException, FHIRException, EOperationOutcome { 
166    XhtmlNode x = r.getNarrative();
167    String an = r.fhirType()+"_"+r.getId();
168    if (context.isAddName()) {
169      if (!hasAnchorName(x, an)) {
170        injectAnchorName(x, an);
171      }
172    }
173    return x;
174  }
175
176  private void injectAnchorName(XhtmlNode x, String an) {
177    XhtmlNode ip = x;
178    while (ip.hasChildren() && "div".equals(ip.getChildNodes().get(0).getName())) {
179      ip = ip.getChildNodes().get(0);
180    }
181    ip.addTag(0, "a").setAttribute("name", an).tx(" ");    
182  }
183
184  protected boolean hasAnchorName(XhtmlNode x, String an) {
185    if ("a".equals(x.getName()) && an.equals(x.getAttribute("name"))) {
186      return true;
187    }
188    if (x.hasChildren()) {
189      for (XhtmlNode c : x.getChildNodes()) {
190        if (hasAnchorName(c, an)) {
191          return true;
192        }
193      }
194    }
195    return false;
196  }
197
198  // these three are what the descendants of this class override
199  public abstract void buildNarrative(RenderingStatus status, XhtmlNode x, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome;
200  public abstract String buildSummary(ResourceWrapper r) throws UnsupportedEncodingException, IOException;
201    
202  public void buildSummary(RenderingStatus status, XhtmlNode x, ResourceWrapper r) throws UnsupportedEncodingException, IOException {
203    x.tx(buildSummary(r));
204  }
205
206  public String canonicalTitle(ResourceWrapper r) {
207    if (r.has("title")) {
208      return r.primitiveValue("title");
209    }
210    if (r.has("name")) {
211      return r.primitiveValue("name");
212    }
213    if (r.has("id")) {
214      return r.primitiveValue("id");
215    }
216    return "?title?";
217  }
218  
219  public void describe(XhtmlNode x, ResourceWrapper r) throws UnsupportedEncodingException, IOException {
220    x.tx(displayDataType(r));
221  }
222  
223  public void inject(ResourceWrapper r, XhtmlNode x, NarrativeStatus status) throws IOException {
224    r.setNarrative(x, status.toCode(), multiLangMode, context.getLocale(), context.isPretty());
225  }
226
227  public void markLanguage(XhtmlNode x) {
228    x.setAttribute("lang", context.getLocale().toLanguageTag());
229    x.setAttribute("xml:lang", context.getLocale().toLanguageTag());
230    x.addTag(0, "hr");
231    x.addTag(0, "p").b().tx(context.getLocale().getDisplayName());
232    x.addTag(0, "hr");
233  }
234  
235  @Override
236  protected void renderCanonical(RenderingStatus status, XhtmlNode x, ResourceWrapper type) throws FHIRFormatError, DefinitionException, IOException {
237    renderCanonical(status, x, Resource.class, type);
238  }
239
240  public <T extends Resource> void renderCanonical(RenderingStatus status, XhtmlNode x, Class<T> class_, ResourceWrapper canonical) throws UnsupportedEncodingException, IOException {
241    if (!renderPrimitiveWithNoValue(status, x, canonical)) {
242      CanonicalResource target = (CanonicalResource) context.getWorker().fetchResource(class_, canonical.primitiveValue(), canonical.getResourceNative());
243      if (target != null && target.hasWebPath()) {
244        if (canonical.primitiveValue().contains("|")) {
245          x.ah(context.prefixLocalHref(target.getWebPath())).tx(target.present()+ context.formatPhrase(RenderingContext.RES_REND_VER) +target.getVersion()+")");
246        } else {
247          x.ah(context.prefixLocalHref(target.getWebPath())).tx(target.present());
248        }
249        return;
250      }
251      // we can't resolve it as a canonical in the context. We'll try to do a local resolution instead
252      ResourceWithReference rr = resolveReference(canonical);
253      if (rr == null) {
254        x.code(canonical.primitiveValue());
255      } else if (rr.getResource() == null) {
256        x.ah(context.prefixLocalHref(rr.getWebPath())).tx(canonical.primitiveValue());        
257      } else {
258        x.ah(context.prefixLocalHref(rr.getWebPath())).tx(RendererFactory.factory(rr.getResource(), context.forContained()).buildSummary(rr.getResource()));        
259      }
260    }
261  }
262  
263
264  protected String displayCanonical(ResourceWrapper canonical) {
265    if (canonical == null || !canonical.hasPrimitiveValue()) {
266      return "";
267    }
268    String url = canonical.primitiveValue();
269    Resource target = context.getWorker().fetchResource(Resource.class, url, canonical.getResourceNative());
270    if (target == null || !(target instanceof CanonicalResource)) {
271      return url;       
272    } else {
273      CanonicalResource cr = (CanonicalResource) target;
274      return "->"+cr.present();
275    }     
276  }
277  
278  protected String displayReference(ResourceWrapper type) {
279    if (type == null) {
280      return "";
281    }
282    ResourceWrapper display = null;
283    ResourceWrapper actual = null;
284    ResourceWrapper id = null;
285    if (type.fhirType().equals("CodeableReference")) {
286      if (type.has("reference")) {
287        type = type.child("reference");
288      } else {
289        return displayCodeableConcept(type.child("concept"));
290      }
291    }
292    if (type.fhirType().equals("Reference")) {
293      display = type.child("display");
294      actual = type.child("reference");
295      id = type.child("identifier");
296    } else {
297      actual = type;
298    }
299    if (actual != null && actual.hasPrimitiveValue()) {
300      if ("#".equals(actual.primitiveValue())) {
301        return "this resource";
302      } else {
303        ResourceWithReference rr = resolveReference(actual);
304        if (rr == null) {
305          String disp = display != null && display.hasPrimitiveValue() ? displayDataType(display) : actual.primitiveValue();
306          return "->"+disp;
307        } else {
308          String disp;
309          try {
310            disp = display != null && display.hasPrimitiveValue() ? displayDataType(display) : RendererFactory.factory(rr.getResource(), context.forContained()).buildSummary(rr.getResource());
311          } catch (IOException e) {
312            disp = e.getMessage();
313          }
314          return "->"+disp;
315        }
316      }
317    } else if (display != null) {
318      return "->"+display;
319    } else if (id != null) {
320      return "id: "+displayIdentifier(id);
321    } else {
322      return "?ref?";
323    }
324  }
325
326  
327  /**
328   * @param <T>
329   * @param status
330   * @param res
331   * @param x
332   * @param class_ - makes resolution faster, but can just be Resource.class
333   * @param canonical
334   * @throws UnsupportedEncodingException
335   * @throws IOException
336   */
337  public <T extends Resource> void renderCanonical(RenderingStatus status, ResourceWrapper res, XhtmlNode x, Class<T> class_, CanonicalType canonical) throws UnsupportedEncodingException, IOException {
338    if (canonical == null || !canonical.hasPrimitiveValue()) {
339      return;
340    }
341    String url = canonical.asStringValue();
342    Resource target = context.getWorker().fetchResource(Resource.class, url, res.getResourceNative());
343    if (target == null || !(target instanceof CanonicalResource)) {
344      x.code().tx(url);       
345    } else {
346      CanonicalResource cr = (CanonicalResource) target;
347      if (!target.hasWebPath()) {
348        if (url.contains("|")) {
349          x.code().tx(cr.getUrl());
350          x.tx(context.formatPhrase(RenderingContext.RES_REND_VER, cr.getVersion()));
351          x.tx(" ("+cr.present()+")");
352        } else {
353          x.code().tx(url);
354          x.tx(" ("+cr.present()+")");
355        }
356      } else {
357        if (url.contains("|")) {
358          x.ah(context.prefixLocalHref(target.getWebPath())).tx(cr.present()+ context.formatPhrase(RenderingContext.RES_REND_VER) +cr.getVersion()+")");
359        } else {
360          x.ah(context.prefixLocalHref(target.getWebPath())).tx(cr.present());
361        }
362      }
363    }     
364  }
365
366  // todo: if (r.hasExtension(ExtensionDefinitions.EXT_TARGET_ID) || r.hasExtension(ExtensionDefinitions.EXT_TARGET_PATH)) {
367  @Override
368  public void renderReference(RenderingStatus status, XhtmlNode x, ResourceWrapper type) throws FHIRFormatError, DefinitionException, IOException {
369    if (type == null) {
370      return;
371    }
372    xlinkNarrative(x, type);
373    ResourceWrapper display = null;
374    ResourceWrapper actual = null;
375    ResourceWrapper id = null;
376    if (type.fhirType().equals("CodeableReference")) {
377      if (type.has("reference")) {
378        type = type.child("reference");
379      } else {
380        renderCodeableConcept(status, x, type.child("concept"));
381        return;
382      }
383    }
384    if (type.fhirType().equals("Reference")) {
385      display = type.child("display");
386      actual = type.child("reference");
387      id = type.child("identifier");
388    } else {
389      actual = type;
390    }
391    if (actual != null && actual.hasPrimitiveValue()) {
392      ResourceWithReference rr = resolveReference(actual);
393      if (rr == null) {
394        String disp = display != null && display.hasPrimitiveValue() ? displayDataType(display) : actual.primitiveValue();
395        if (Utilities.isAbsoluteUrlLinkable(actual.primitiveValue()) || (isLocalReference(actual.primitiveValue()) && !context.isUnknownLocalReferencesNotLinks())) {
396          x.ah(context.prefixLocalHref(actual.primitiveValue())).tx(disp);
397        } else {
398          x.code().tx(disp);
399        }
400      } else if (rr.getResource() == null) {
401        String disp = display != null && display.hasPrimitiveValue() ? displayDataType(display) : rr.getUrlReference();
402        x.ah(context.prefixLocalHref(rr.getWebPath())).tx(disp);
403      } else if (rr.getResource() != null) {
404        String disp = display != null && display.hasPrimitiveValue() ? displayDataType(display) : RendererFactory.factory(rr.getResource(), context.forContained()).buildSummary(rr.getResource());
405        x.ah(context.prefixLocalHref(rr.getWebPath())).tx(disp);
406      } else {
407        String disp = display != null && display.hasPrimitiveValue() ? displayDataType(display) : "?rref2?";
408        x.ah(context.prefixLocalHref(rr.getWebPath())).tx(disp);
409      }
410    } else if (display != null && id != null) {
411      renderDataType(status, x, display);
412      x.tx(" (Identifier: ");
413      renderIdentifier(status, x, id);
414      x.tx(")");
415    } else if (display != null) {
416      renderDataType(status, x, display);
417    } else if (id != null) {
418      x.tx("Identifier: ");
419      renderIdentifier(status, x, id);
420    } else {
421      x.tx("?rref?");
422    }
423    checkRenderExtensions(status, x, type);
424  }
425  
426 
427  private boolean isLocalReference(String url) {
428    if (url == null) {
429      return false;
430    }
431    if (url.contains("/_history")) {
432      url = url.substring(0, url.indexOf("/_hist"));
433    }
434    
435    if (url.matches(Constants.LOCAL_REF_REGEX)) {
436      return true;
437    } else {
438      return false;
439    }
440  }
441
442  public void renderReference(ResourceWrapper res, HierarchicalTableGenerator gen, List<Piece> pieces, Reference r, boolean allowLinks) throws UnsupportedEncodingException, IOException {
443    if (r == null) { 
444      pieces.add(gen.new Piece(null, "null!", null));
445      return;
446    }
447    ResourceWithReference tr = null;
448    String link = null;
449    StringBuilder text = new StringBuilder();
450    if (r.hasReferenceElement() && allowLinks) {
451      tr = resolveReference(res, r.getReference(), true);
452
453      if (!r.getReference().startsWith("#")) {
454        if (tr != null && tr.getWebPath() != null) {
455          link = tr.getWebPath();
456        } else if (r.getReference().contains("?")) {
457          text.append(context.formatPhrase(RenderingContext.RES_REND_COND_REF)+" ");
458        } else {
459          link = r.getReference();
460        }
461      } 
462    }
463    if (tr != null && tr.getWebPath() != null && tr.getWebPath().startsWith("#")) {
464      text.append(context.formatPhrase(RenderingContext.RES_REND_SEE_ON_THIS_PAGE)+" ");
465    }
466    // what to display: if text is provided, then that. if the reference was resolved, then show the name, or the generated narrative
467    String display = r.hasDisplayElement() ? r.getDisplay() : null;
468    String name = tr != null && tr.getResource() != null ? getNameForResource(tr.getResource()) : null; 
469    
470    if (display == null && (tr == null || tr.getResource() == null)) {
471      if (!Utilities.noString(r.getReference())) {
472        text.append(r.getReference());
473      } else if (r.hasIdentifier()) {
474        text.append(displayIdentifier(wrapWC(res, r.getIdentifier())));
475      } else {
476        text.append("?r-ref?");        
477      }
478    } else if (context.isTechnicalMode()) {
479      text.append(r.getReference());
480      if (display != null) {
481        text.append(": "+display);
482      }
483      if ((tr == null || (tr.getWebPath() != null && !tr.getWebPath().startsWith("#"))) && name != null) {
484        text.append(" \""+name+"\"");
485      }
486      if (r.hasExtension(ExtensionDefinitions.EXT_TARGET_ID) || r.hasExtension(ExtensionDefinitions.EXT_TARGET_PATH)) {
487        text.append("(");
488        for (Extension ex : r.getExtensionsByUrl(ExtensionDefinitions.EXT_TARGET_ID)) {
489          if (ex.hasValue()) {
490            text.append(", ");
491            text.append("#"+ex.getValue().primitiveValue());
492          }
493        }
494        for (Extension ex : r.getExtensionsByUrl(ExtensionDefinitions.EXT_TARGET_PATH)) {
495          if (ex.hasValue()) {
496            text.append(", ");
497            text.append("/#"+ex.getValue().primitiveValue());
498          }
499        }
500        text.append(")");
501      }  
502    } else {
503      if (display != null) {
504        text.append(display);
505      } else if (name != null) {
506        text.append(name);
507      } else {
508        text.append(context.formatPhrase(RenderingContext.RES_REND_DESC));
509      }
510    }
511    if (tr != null && tr.getWebPath() != null && tr.getWebPath().startsWith("#")) {
512      text.append(")");
513    }      
514    pieces.add(gen.new Piece(link,text.toString(), null));
515  }
516  
517  public void renderReference(ResourceWrapper res, HierarchicalTableGenerator gen, List<Piece> pieces, ResourceWrapper r, boolean allowLinks) throws UnsupportedEncodingException, IOException {
518    if (r == null) { 
519      pieces.add(gen.new Piece(null, "null!", null));
520      return;
521    }
522    ResourceWithReference trt = null;
523    String link = null;
524    StringBuilder text = new StringBuilder();
525    if (r.has("reference") && allowLinks) {
526      trt = resolveReference(res, r.primitiveValue("reference"), true);
527
528      if (!r.primitiveValue("reference").startsWith("#")) {
529        if (trt != null && trt.getWebPath() != null) {
530          link = trt.getWebPath();
531        } else if (r.primitiveValue("reference").contains("?")) {
532          text.append(context.formatPhrase(RenderingContext.RES_REND_COND_REF)+" ");
533        } else {
534          link = r.primitiveValue("reference");
535        }
536      } 
537    }
538    if (trt != null && trt.getWebPath() != null && trt.getWebPath().startsWith("#")) {
539      text.append(context.formatPhrase(RenderingContext.RES_REND_SEE_ON_THIS_PAGE)+" ");
540    }
541    // what to display: if text is provided, then that. if the reference was resolved, then show the name, or the generated narrative
542    String display = r.has("display") ? r.primitiveValue("display") : null;
543    String name = trt != null && trt.getResource() != null ? getNameForResource(trt.getResource()) : null; 
544    
545    if (display == null && (trt == null || trt.getResource() == null)) {
546      if (!Utilities.noString(r.primitiveValue("reference"))) {
547        text.append(r.primitiveValue("reference"));
548      } else if (r.has("identifier")) {
549        text.append(displayIdentifier(r.child("identifier")));
550      } else {
551        text.append("?r-ref2?");        
552      }
553    } else if (context.isTechnicalMode()) {
554      text.append(r.primitiveValue("reference"));
555      if (display != null) {
556        text.append(": "+display);
557      }
558      if ((trt == null || (trt.getWebPath() != null && !trt.getWebPath().startsWith("#"))) && name != null) {
559        text.append(" \""+name+"\"");
560      }
561      if (r.hasExtension(ExtensionDefinitions.EXT_TARGET_ID) || r.hasExtension(ExtensionDefinitions.EXT_TARGET_PATH)) {
562        text.append("(");
563        for (ResourceWrapper ex : r.extensions(ExtensionDefinitions.EXT_TARGET_ID)) {
564          if (ex.has("value")) {
565            text.append(", ");
566            text.append("#"+ex.primitiveValue("value"));
567          }
568        }
569        for (ResourceWrapper ex : r.extensions(ExtensionDefinitions.EXT_TARGET_PATH)) {
570          if (ex.has("value")) {
571            text.append(", ");
572            text.append("#"+ex.primitiveValue("value"));
573          }
574        }
575        text.append(")");
576      }  
577    } else {
578      if (display != null) {
579        text.append(display);
580      } else if (name != null) {
581        text.append(name);
582      } else {
583        text.append(context.formatPhrase(RenderingContext.RES_REND_DESC));
584      }
585    }
586    if (trt != null && trt.getWebPath() != null && trt.getWebPath().startsWith("#")) {
587      text.append(")");
588    }      
589    pieces.add(gen.new Piece(link,text.toString(), null));
590  }
591  
592  protected String getNameForResource(ResourceWrapper resource) {
593    ResourceWrapper name = resource.firstChild("name");
594    if (name != null && !name.isEmpty()) {
595      if (name.isPrimitive()) {
596        return name.primitiveValue();          
597      } else if (name.fhirType().equals("HumanName")) {
598        String family = name.primitiveValue("family");
599        String given = name.firstPrimitiveValue("given");
600        return (family == null) ? given : given == null ? family : family+" "+given;
601      } else {
602        String n = name.primitiveValueMN("name", "text", "value");
603        if (n != null) {            
604          return n;            
605        }
606      }
607    }
608    String n = resource.primitiveValue("productName");
609    if (n == null) {            
610      throw new Error("What to render for 'name'? Type is "+resource.fhirType());
611    } else {
612      return n;            
613    }
614  }
615
616  protected void renderUri(RenderingStatus status, ResourceWrapper resource, XhtmlNode x, UriType uri) throws FHIRFormatError, DefinitionException, IOException { 
617    if (!renderPrimitiveWithNoValue(status, x, uri)) {
618      String v = uri.primitiveValue();
619
620      if (v.startsWith("mailto:")) { 
621        x.ah(context.prefixLocalHref(v)).addText(v.substring(7)); 
622      } else { 
623        ResourceWithReference rr = resolveReference(resource, uri.primitiveValue(), false);
624        if (rr != null) {
625          x.ah(rr.getWebPath()).addText(RendererFactory.factory(rr.getResource(), context.forContained()).buildSummary(rr.getResource()));           
626        } else {
627          Resource r = context.getContext().fetchResource(Resource.class, v); 
628          if (r != null && r.getWebPath() != null) { 
629              x.ah(context.prefixLocalHref(r.getWebPath())).addText(RendererFactory.factory(r, context.forContained()).buildSummary(wrap(r)));           
630          } else { 
631            String url = context.getResolver() != null ? context.getResolver().resolveUri(context, v) : null; 
632            if (url != null) {           
633              x.ah(context.prefixLocalHref(url)).addText(v); 
634            } else if (Utilities.isAbsoluteUrlLinkable(v) && !uri.fhirType().equals("id")) { 
635              x.ah(context.prefixLocalHref(v)).addText(v); 
636            } else { 
637              x.addText(v); 
638            } 
639          } 
640        }
641      } 
642    }
643  } 
644  
645  @Override
646  protected void renderUri(RenderingStatus status, XhtmlNode x, ResourceWrapper uri) throws FHIRFormatError, DefinitionException, IOException { 
647    if (!renderPrimitiveWithNoValue(status, x, uri)) {
648      String v = uri.primitiveValue();
649      boolean local = false;
650
651      if (context.getContextUtilities().isResource(v)) {
652        v = "http://hl7.org/fhir/StructureDefinition/"+v;
653        local = true;
654      }
655      if (v.startsWith("mailto:")) { 
656        x.ah(v).addText(v.substring(7)); 
657      } else { 
658        String link = getLinkForCode(v, null, null);
659        if (link != null) {  
660          x.ah(context.prefixLocalHref(link)).addText(v);
661        } else {
662        ResourceWithReference rr = local ? resolveReference(uri.resource(), v, true) : resolveReference(uri);
663        if (rr != null) {
664          if (rr.getResource() == null) {
665            x.ah(context.prefixLocalHref(rr.getWebPath())).addText(rr.getUrlReference());
666          } else {
667            x.ah(context.prefixLocalHref(rr.getWebPath())).addText(RendererFactory.factory(rr.getResource(), context.forContained()).buildSummary(rr.getResource()));
668          }
669        } else {
670          Resource r = context.getContext().fetchResource(Resource.class, v); 
671          if (r != null && r.getWebPath() != null) { 
672              x.ah(context.prefixLocalHref(r.getWebPath())).addText(RendererFactory.factory(r, context.forContained()).buildSummary(wrap(r)));           
673          } else if (r != null) { 
674            x.ah(context.prefixLocalHref(v)).addText(RendererFactory.factory(r, context.forContained()).buildSummary(wrap(r)));           
675          } else { 
676            String url = context.getResolver() != null ? context.getResolver().resolveUri(context, v) : null; 
677            if (url != null) {           
678              x.ah(context.prefixLocalHref(url)).addText(v); 
679            } else if (Utilities.isAbsoluteUrlLinkable(v) && !uri.fhirType().equals("id")) { 
680              x.ah(context.prefixLocalHref(v)).addText(v); 
681            } else { 
682              x.addText(v); 
683            } 
684          } 
685        }
686      } 
687      }
688    }
689  } 
690
691  /**
692   * Eventually this will be retired if and when there's no more direct renderers 
693   * 
694   * @param <T>
695   */
696  protected <T extends Resource> T findCanonical(Class<T> class_, UriType canonical, ResourceWrapper sourceOfReference) {
697    return context.getContext().fetchResource(class_, canonical.asStringValue(), sourceOfReference.getResourceNative());
698  }
699
700  protected <T extends Resource> T findCanonical(Class<T> class_, String canonical, ResourceWrapper sourceOfReference) {
701    return context.getContext().fetchResource(class_, canonical, sourceOfReference.getResourceNative());
702  }
703
704
705  private ResourceWithReference resolveContained(ResourceWrapper resource, String url) {
706    ResourceWrapper container = findContainer(resource);
707    if (container == null) {
708      return null;
709    } else if ("#".equals(url)) {
710      return new ResourceWithReference(ResourceReferenceKind.CONTAINER, url, "#hc"+container.getScopedId(), container);
711    } else {
712      String tid = url.substring(1);
713      for (ResourceWrapper c : container.children("contained")) {
714        if (tid.equals(c.getId())) {
715          return new ResourceWithReference(ResourceReferenceKind.CONTAINED, url, "#hc"+c.getScopedId(), c);
716        }
717      }
718    }
719    return null;
720  }
721  
722  private ResourceWrapper findContainer(ResourceWrapper resource) {
723    ResourceWrapper container = resource;
724    while (container != null) {
725      if (container.isResource() && container.kind() != ElementKind.ContainedResource) {
726        break;
727      }
728      container = container.parent();
729    }
730    return container;
731  }
732
733  private ResourceWithReference resolveOutside(ResourceWrapper resource, String url, String version, boolean followLinks) throws IOException {
734    ResourceWrapper container = findContainer(resource);
735    if (container != null) {
736      while (container != null) {
737        if (container.isResource() && container.fhirType().equals("Bundle")) {
738          ResourceWithReference rr = findInBundle(resource, url);
739          if (rr != null) {
740            return null;
741          }
742        }
743        container = container.parent();
744      }
745    }
746    if (followLinks) {
747      // ok, we didn't find it in the current instance, so we go look elsewhere 
748      if (context.getResolver() != null) {
749        ResourceWithReference rr = context.getResolver().resolve(context, url, version);
750        if (rr != null) {
751          return rr;
752        }
753      }
754      Resource r = context.getWorker().fetchResource(Resource.class, url, version);
755      if (r != null) {
756        return new ResourceWithReference(ResourceReferenceKind.EXTERNAL, url, r.getWebPath(), wrap(r));
757      }
758    }
759    return null;
760  }
761  
762  private ResourceWithReference findInBundle(ResourceWrapper resource, String url) {
763    if (url.equals("Bundle/"+resource.getId())) {
764      return new ResourceWithReference(ResourceReferenceKind.BUNDLE, url, "#"+resource.getScopedId(), resource);
765    }
766    for (ResourceWrapper entry : resource.children("entry")) {
767      if (entry.has("resource")) {
768        ResourceWrapper res = entry.child("resource");  
769        if (entry.has("fullUrl")) { 
770          String fu = entry.primitiveValue("fullUrl");
771          if (url.equals(fu)) {
772           return new ResourceWithReference(ResourceReferenceKind.BUNDLE, url, "#"+res.getScopedId(), res);
773          }
774        }
775        if ("Bundle".equals(res.fhirType())) {
776          ResourceWithReference rr = findInBundle(res, url);
777          if (rr != null) {
778            return rr;
779          }
780        }
781      }
782    }
783    return null;
784  }
785
786  protected ResourceWithReference resolveReference(ResourceWrapper resource, String url, boolean followLinks) throws IOException {
787    if (url == null) {
788      return null;
789    }
790
791    if (url.startsWith("#")) {
792      return resolveContained(resource, url);
793    } else {
794      String version = null;
795      if (url.contains("/_history/")) {
796        version = url.substring(url.indexOf("/_history/")+10);
797        url = url.substring(0, url.indexOf("/_history/"));
798      }
799      
800      return resolveOutside(resource, url, version, followLinks);
801    }
802
803  }
804
805  protected ResourceWithReference resolveReference(ResourceWrapper reference) {
806    try {
807      if (reference.fhirType().equals("CodeableReference")) {
808        if (reference.has("reference")) {
809          return resolveReference(reference.child("reference"));
810        } else {
811          return null;
812        }
813      } else if (reference.fhirType().equals("Reference")) {
814        return resolveReference(reference.getResourceWrapper(), reference.primitiveValue("reference"), true);
815      } else {
816        return resolveReference(reference.getResourceWrapper(), reference.primitiveValue(), true);
817      }
818    } catch (IOException e) {
819      if (context.isDebug()) {
820        e.printStackTrace();        
821      }
822      return null;
823    }
824  }
825  
826  
827  protected String makeIdFromBundleEntry(String url) {
828    if (url == null) {
829      return null;
830    }
831    if (url.startsWith("urn:uuid:")) {
832      return url.substring(9).toLowerCase();
833    }
834    return fullUrlToAnchor(url);    
835  }
836
837  private String fullUrlToAnchor(String url) {
838    return url.replace(":", "").replace("/", "_");
839  }
840
841  protected void generateCopyright(XhtmlNode x, ResourceWrapper cs) {
842    XhtmlNode p = x.para();
843    p.b().tx(getContext().formatPhrase(RenderingContext.RESOURCE_COPYRIGHT));
844    p.addTextWithLineBreaks(" " + context.getTranslated(cs.child("copyright")));
845  }
846  
847  protected void generateCopyrightTableRow(XhtmlNode tbl, ResourceWrapper cs) {
848    XhtmlNode tr = tbl.tr();
849    tr.td().b().tx(getContext().formatPhrase(RenderingContext.RESOURCE_COPYRIGHT));
850    tr.td().addTextWithLineBreaks(" " + context.getTranslated(cs.child("copyright")));
851  }
852
853  public String displayReference(Resource res, Reference r) throws UnsupportedEncodingException, IOException {
854    return (context.formatPhrase(RenderingContext.GENERAL_TODO)); 
855   }
856
857   public Base parseType(String string, String type) {
858     return null;
859   }
860
861   protected String describeStatus(PublicationStatus status, boolean experimental) {
862     switch (status) {
863     case ACTIVE: return experimental ? (context.formatPhrase(RenderingContext.GENERAL_EXPER)) : (context.formatPhrase(RenderingContext.RES_REND_ACT)); 
864     case DRAFT: return (context.formatPhrase(RenderingContext.RES_REND_DRAFT));
865     case RETIRED: return (context.formatPhrase(RenderingContext.RES_REND_RET));
866     default: return (context.formatPhrase(RenderingContext.RES_REND_UNKNOWN));
867     }
868   }
869
870   protected void renderCommitteeLink(XhtmlNode x, CanonicalResource cr) {
871     String code = ExtensionUtilities.readStringExtension(cr, ExtensionDefinitions.EXT_WORKGROUP);
872     CodeSystem cs = context.getWorker().fetchCodeSystem("http://terminology.hl7.org/CodeSystem/hl7-work-group");
873     if (cs == null || !cs.hasWebPath())
874       x.tx(code);
875     else {
876       ConceptDefinitionComponent cd = CodeSystemUtilities.findCode(cs.getConcept(), code);
877       if (cd == null) {
878         x.tx(code);
879       } else {
880         x.ah(context.prefixLocalHref(cs.getWebPath()+"#"+cs.getId()+"-"+cd.getCode())).tx(cd.getDisplay());
881       }
882     }
883   }
884
885   public static String makeInternalBundleLink(ResourceWrapper bundle, String fullUrl) {
886     // are we in a bundle in a bundle? Then the link is scoped
887     boolean inBundle = false;
888     ResourceWrapper rw = bundle.parent();
889     while (rw != null) {
890       if (rw.fhirType().equals("Bundle")) {
891         inBundle = true;
892       }
893       rw = rw.parent();
894      }
895     if (inBundle) {
896       return bundle.getScopedId()+"/"+fullUrl.replace(":", "-");
897     } else {
898       return fullUrl.replace(":", "-");
899     }
900   }
901
902  public boolean canRender(Resource resource) {
903    return true;
904  }
905
906  protected XhtmlNode renderResourceTechDetails(ResourceWrapper r, XhtmlNode x) throws UnsupportedEncodingException, FHIRException, IOException {
907    return renderResourceTechDetails(r, x, (context.isContained() && r.getId() != null ? "#"+r.getId() : r.getId()));
908  }
909  
910  protected XhtmlNode renderResourceTechDetails(ResourceWrapper r, XhtmlNode x, String id) throws UnsupportedEncodingException, FHIRException, IOException {
911    XhtmlNode p = x.para().attribute("class", "res-header-id");
912    markGenerated(p);
913    if (!context.isNoHeader()) {
914      String ft = context.getTranslatedCode(r.fhirType(), VersionUtilities.getResourceTypesUrl(context.getContext().getVersion()));
915      if (id == null) { 
916        p.b().tx(context.formatPhrase(context.isTechnicalMode() && !isInner() ? RenderingContext.PROF_DRIV_GEN_NARR_TECH : RenderingContext.PROF_DRIV_GEN_NARR, ft, ""));      
917      } else {
918        p.b().tx(context.formatPhrase(context.isTechnicalMode() && !isInner() ? RenderingContext.PROF_DRIV_GEN_NARR_TECH : RenderingContext.PROF_DRIV_GEN_NARR, ft, id));
919      }
920    }
921
922    // first thing we do is lay down the resource anchors. 
923    if (!Utilities.noString(r.getId())) {
924      String sid = r.getScopedId();
925      if (sid != null && !willRenderId(r)) {
926        if (!context.hasAnchor(sid)) {
927          context.addAnchor(sid);
928          x.an(context.prefixAnchor(sid));
929        }
930        sid = "hc"+sid;
931        if (!context.hasAnchor(sid)) {
932          context.addAnchor(sid);
933          x.an(context.prefixAnchor(sid));
934        }
935      }
936    }
937
938    if (context.isTechnicalMode()) {
939      RenderingStatus status = new RenderingStatus();
940
941      String lang = r.primitiveValue("language"); 
942      String ir = r.primitiveValue("implicitRules"); 
943      ResourceWrapper meta = r.has("meta") && !r.child("meta").isEmpty() ? r.child("meta") : null;
944      ResourceWrapper versionId = meta == null ? null : meta.child("versionId");
945      ResourceWrapper lastUpdated = meta == null ? null : meta.child("lastUpdated");
946      ResourceWrapper source = meta == null ? null : meta.child("source");
947
948      if (lang != null || versionId != null || lastUpdated != null || ir != null || source != null || meta != null) {
949        XhtmlNode div = x.div().style("display: inline-block").style("background-color: #d9e0e7").style("padding: 6px")
950            .style("margin: 4px").style("border: 1px solid #8da1b4")
951            .style("border-radius: 5px").style("line-height: 60%");
952
953        boolean sfirst = true;
954        p = plateStyle(div.para());
955        if (versionId != null) {
956          p.tx(context.formatPhrase(RenderingContext.RES_REND_VER, versionId.primitiveValue()));
957          sfirst = false;
958        }
959        if (lastUpdated != null) {
960          if (!sfirst) {
961            p.tx("; ");
962          }
963          p.tx(context.formatPhrase(RenderingContext.RES_REND_UPDATED, displayDataType(lastUpdated)));
964          sfirst = false;
965        }
966        if (lang != null) {
967          if (!sfirst) {
968            p.tx("; ");
969          }
970          p.tx(context.formatPhrase(RenderingContext.RES_REND_LANGUAGE, lang));
971          sfirst = false;
972        }
973        if (source != null) {
974          if (!sfirst) {
975            p.tx("; ");
976          }
977          XhtmlNode pp = plateStyle(div.para());
978          pp.startScript("source");
979          renderDataType(status, pp.param("source"), source);
980          pp.execScript(context.formatPhrase(RenderingContext.RES_REND_INFO_SOURCE));
981          pp.closeScript();
982          sfirst = false;
983        }
984        if (ir != null) {
985          if (!sfirst) {
986            p.tx("; ");
987          }
988          plateStyle(div.para()).b().tx(context.formatPhrase(RenderingContext.RES_REND_SPEC_RULES, ir));     
989          sfirst = false;
990        }
991        if (meta != null) {
992          List<ResourceWrapper> items = meta.children("profile");
993          if (!items.isEmpty()) {
994            p = plateStyle(div.para());
995            p.tx(Utilities.pluralize(context.formatPhrase(RenderingContext.GENERAL_PROF), items.size())+": ");
996            boolean first = true;
997            for (ResourceWrapper bw : items) {
998              if (first) first = false; else p.tx(", ");
999              renderCanonical(status, p, StructureDefinition.class, bw);
1000            }
1001          }
1002          items = meta.children("tag");
1003          if (!items.isEmpty()) {
1004            p = plateStyle(div.para());
1005            p.tx(Utilities.pluralize(context.formatPhrase(RenderingContext.RES_REND_TAG), items.size())+": ");
1006            boolean first = true;
1007            for (ResourceWrapper bw : items) {
1008              if (first) first = false; else p.tx(", ");
1009              renderCoding(status, p, bw);
1010            }        
1011          }
1012          items = meta.children("security");
1013          if (!items.isEmpty()) {
1014            p = plateStyle(div.para());
1015            p.tx(Utilities.pluralize(context.formatPhrase(RenderingContext.GENERAL_SECURITY_LABEL), items.size())+": ");
1016            boolean first = true;
1017            for (ResourceWrapper bw : items) {
1018              if (first) first = false; else p.tx(", ");
1019              renderCoding(status, p, bw);
1020            }        
1021          }
1022        }
1023        return div;
1024      }
1025    }
1026    return null;
1027  }
1028
1029  protected boolean willRenderId(ResourceWrapper r) {
1030    return false;
1031  }
1032
1033  protected XhtmlNode plateStyle(XhtmlNode para) {
1034    return para.style("margin-bottom: 0px");
1035  }
1036
1037//  public void renderOrError(DomainResource dr) {
1038//    try {
1039//      render(dr);
1040//    } catch (Exception e) {
1041//      XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
1042//      x.para().tx(context.formatPhrase(RenderingContext.RES_REND_ERROR, e.getMessage())+" ");
1043//      dr.setText(null);
1044//      inject(dr, x, NarrativeStatus.GENERATED);   
1045//    }
1046//    
1047//  }
1048  
1049  public RendererType getRendererType() {
1050    return RendererType.NATIVE;
1051  }
1052  
1053  public class TableRowData {
1054    private Map<String, List<ResourceWrapper>> cols = new HashMap<>();
1055    private TableData data;
1056    
1057    public void value(String name, ResourceWrapper value) {
1058      if (!cols.containsKey(name)) {
1059        cols.put(name, new ArrayList<>());
1060      }
1061      if (!data.columns.contains(name)) {
1062        data.columns.add(name);
1063      }
1064      cols.get(name).add(value);
1065    }
1066
1067    public boolean hasCol(String name) {
1068      return cols.containsKey(name);
1069    }
1070
1071    public List<ResourceWrapper> get(String name) {
1072      return cols.get(name);
1073    }
1074    
1075  }
1076  public class TableData {
1077    private String title;
1078    private List<String> columns = new ArrayList<>();
1079    private List<TableRowData> rows = new ArrayList<>();
1080    public TableData(String title) {
1081      this.title = title;
1082    }
1083    public String getTitle() {
1084      return title;
1085    }
1086    public List<String> getColumns() {
1087      return columns;
1088    }
1089    public List<TableRowData> getRows() {
1090      return rows;
1091    }
1092    public void addColumn(String name) {
1093      columns.add(name);
1094    }
1095    public TableRowData addRow() {
1096      TableRowData res = new TableRowData();
1097      rows.add(res);
1098      res.data = this;
1099      return res;
1100    }
1101  }
1102
1103
1104  public void renderTable(RenderingStatus status, TableData provider, XhtmlNode x) throws FHIRFormatError, DefinitionException, IOException {
1105    List<String> columns = new ArrayList<>();
1106    for (String name : provider.getColumns()) {
1107      boolean hasData = false;
1108      for (TableRowData row : provider.getRows()) {
1109        if (row.hasCol(name)) {
1110          hasData = true;
1111        }
1112      }
1113      if (hasData) {
1114        columns.add(name);
1115      }
1116    }
1117    if (columns.size() > 0) {
1118      XhtmlNode table = x.table("grid", false);
1119      
1120      if (provider.getTitle() != null) {
1121        table.tr().td().colspan(columns.size()).b().tx(provider.getTitle());
1122      }
1123      XhtmlNode tr = table.tr();
1124      for (String col : columns) {
1125        tr.th().b().tx(col);
1126      }
1127      for (TableRowData row : provider.getRows()) {
1128        tr = table.tr();
1129        for (String col : columns) {
1130          XhtmlNode td = tr.td();
1131          boolean first = true;
1132          List<ResourceWrapper> list = row.get(col);
1133          if (list != null) {
1134            for (ResourceWrapper value : list) {
1135              if (first) first = false; else td.tx(", ");
1136              renderDataType(status, td, value);
1137            }
1138          }
1139        }
1140      }      
1141    }
1142  }
1143  
1144  public void renderOrError(ResourceWrapper dr) throws IOException {
1145    try {
1146      renderResource(dr);
1147    } catch (Exception e) {
1148      XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
1149      x.para().tx("Error rendering: " + e.getMessage());
1150      inject(dr, x, NarrativeStatus.GENERATED);
1151    }
1152  }
1153  
1154
1155  public boolean genSummaryTable(RenderingStatus status, XhtmlNode x, ResourceWrapper cr) throws IOException {
1156    if (context.isShowSummaryTable() && cr != null) {
1157      XhtmlNode tbl = x.table("grid", false);
1158      genSummaryTableContent(status, tbl, cr);
1159      return true;
1160    } else {
1161      return false;
1162    }
1163  }
1164  
1165
1166  protected void genSummaryTableContent(RenderingStatus status, XhtmlNode tbl, ResourceWrapper cr) throws IOException {
1167    XhtmlNode tr;
1168    if (cr.has("url")) {
1169      tr = tbl.tr();
1170      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_DEFINING_URL)+":");
1171      xlinkNarrative(tr.td(), cr.child("url")).code().tx(cr.primitiveValue("url"));
1172    } else if (cr.hasExtension(EXT_NS_URL)) {
1173      status.setExtensions(true);
1174      tr = tbl.tr();
1175      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_DEFINING_URL)+":");
1176      xlinkNarrative(tr.td(), cr.extension(EXT_NS_URL)).code().tx(cr.extensionString(EXT_NS_URL));
1177    } else if (!context.isContained()) {                                          
1178      tr = tbl.tr();
1179      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_DEFINING_URL));
1180      tr.td();      
1181    }
1182    if (cr.has("version")) {
1183      tr = tbl.tr();
1184      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_VER)+":");
1185      renderDataType(status, tr.td(), cr.child("version"));
1186    } else if (cr.hasExtension("http://terminology.hl7.org/StructureDefinition/ext-namingsystem-version")) {
1187      status.setExtensions(true);
1188      tr = tbl.tr();
1189      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_VER)+":");
1190      renderDataType(status, tr.td(), cr.extensionValue("http://hl7.org/fhir/5.0/StructureDefinition/extension-NamingSystem.version"));
1191    }
1192
1193    String name = context.getTranslated(cr.child("name"));
1194    String title = context.getTranslated(cr.child("title"));
1195    
1196    if (name != null) {
1197      tr = tbl.tr();
1198      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_NAME)+":");
1199      xlinkNarrative(tr.td(), cr.child("name")).tx(name);
1200    }
1201    
1202    if (title != null && !title.equalsIgnoreCase(name)) {
1203      tr = tbl.tr();
1204      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_TITLE)+":");
1205      xlinkNarrative(tr.td(), cr.child("title")).tx(title);
1206    }
1207
1208    if (cr.has("status") && !context.isContained()) {
1209      tr = tbl.tr();
1210      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_STATUS)+":");
1211      xlinkNarrative(tr.td(), cr.child("status")).tx(describeStatus(status, cr));
1212    }
1213
1214    if (cr.has("description")) {
1215      tr = tbl.tr();
1216      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_DEFINITION)+":");
1217      xlinkNarrative(tr.td(), cr.child("description")).markdown(context.getTranslated(cr.child("description")), "description");
1218    }
1219
1220    if (cr.has("publisher")) {
1221      tr = tbl.tr();
1222      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.CANON_REND_PUBLISHER)+":");
1223      buildPublisherLinks( tr.td(), cr);
1224    }
1225    
1226    if (cr.hasExtension(ExtensionDefinitions.EXT_WORKGROUP)) {
1227      status.setExtensions(true);
1228      tr = tbl.tr();
1229      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.CANON_REND_COMMITTEE)+":");
1230      renderCommitteeLink(tr.td(), cr);
1231    }
1232
1233    if (cr.has("copyright")) {
1234      tr = tbl.tr();
1235      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_COPYRIGHT)+":");
1236      xlinkNarrative(tr.td(), cr.child("copyright")).markdown(context.getTranslated(cr.child("copyright")), "copyright");
1237    }
1238    
1239    if (cr.hasExtension(ExtensionDefinitions.EXT_FMM_LEVEL)) {
1240      status.setExtensions(true);
1241      // Use hard-coded spec link to point to current spec because DSTU2 had maturity listed on a different page
1242      tr = tbl.tr();
1243      markBoilerplate(tr.td()).ah("http://hl7.org/fhir/versions.html#maturity", "Maturity Level").attribute("class", "fmm").tx(context.formatPhrase(RenderingContext.CANON_REND_COMMITTEE)+":");
1244      renderDataType(status, tr.td(), cr.extensionValue(ExtensionDefinitions.EXT_FMM_LEVEL));
1245    }    
1246  }
1247
1248
1249
1250  public void genSummaryTable(RenderingStatus status, XhtmlNode x, CanonicalResource cr) throws IOException {
1251    if (context.isShowSummaryTable() && cr != null) {
1252      XhtmlNode tbl = x.table("grid", false);
1253      genSummaryTableContent(status, tbl, cr);
1254    }
1255  }
1256
1257  
1258  protected void genSummaryTableContent(RenderingStatus status, XhtmlNode tbl, CanonicalResource cr) throws IOException {
1259    XhtmlNode tr;
1260    if (cr.hasUrl()) {
1261      tr = tbl.tr();
1262      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_DEFINING_URL)+":");
1263      tr.td().code().tx(cr.getUrl());
1264    } else if (cr.hasExtension(EXT_NS_URL)) {
1265      status.setExtensions(true);
1266      tr = tbl.tr();
1267      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_DEFINING_URL));
1268      tr.td().code().tx(ExtensionUtilities.readStringExtension(cr, EXT_NS_URL)+":");
1269    } else if (!context.isContained()) {                                          
1270      tr = tbl.tr();
1271      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_DEFINING_URL));
1272      tr.td();      
1273    }
1274    if (cr.hasVersion()) {
1275      tr = tbl.tr();
1276      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_VER)+":");
1277      tr.td().tx(cr.getVersion());
1278    } else if (cr.hasExtension("http://terminology.hl7.org/StructureDefinition/ext-namingsystem-version")) {
1279      status.setExtensions(true);
1280      tr = tbl.tr();
1281      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_VER)+":");
1282      tr.td().tx(ExtensionUtilities.readStringExtension(cr, "http://hl7.org/fhir/5.0/StructureDefinition/extension-NamingSystem.version"));
1283    }
1284
1285    String name = cr.hasName() ? context.getTranslated(cr.getNameElement()) : null;
1286    String title = cr.hasTitle() ? context.getTranslated(cr.getTitleElement()) : null;
1287    
1288    if (name != null) {
1289      tr = tbl.tr();
1290      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_NAME)+":");
1291      tr.td().tx(name);
1292    }
1293    
1294    if (title != null && !title.equalsIgnoreCase(name)) {
1295      tr = tbl.tr();
1296      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_TITLE)+":");
1297      tr.td().tx(title);
1298    }
1299
1300    if (cr.hasStatus() && !context.isContained()) {
1301      tr = tbl.tr();
1302      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_STATUS)+":");
1303      tr.td().tx(describeStatus(status, cr));
1304    }
1305
1306    if (cr.hasDescription()) {
1307      tr = tbl.tr();
1308      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_DEFINITION)+":");
1309      tr.td().markdown(cr.getDescription(), "description");
1310    }
1311
1312    if (cr.hasPublisher()) {
1313      tr = tbl.tr();
1314      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.CANON_REND_PUBLISHER)+":");
1315      buildPublisherLinks(tr.td(), cr);
1316    }
1317    
1318    if (cr.hasExtension(ExtensionDefinitions.EXT_WORKGROUP)) {
1319      status.setExtensions(true);
1320      tr = tbl.tr();
1321      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.CANON_REND_COMMITTEE)+":");
1322      renderCommitteeLink(tr.td(), cr);
1323    }
1324
1325    if (cr.hasCopyright()) {
1326      tr = tbl.tr();
1327      markBoilerplate(tr.td()).tx(context.formatPhrase(RenderingContext.GENERAL_COPYRIGHT)+":");
1328      tr.td().markdown(cr.getDescription(), "copyright");      
1329    }
1330    
1331    if (ExtensionUtilities.hasExtension(cr, ExtensionDefinitions.EXT_FMM_LEVEL)) {
1332      status.setExtensions(true);
1333      // Use hard-coded spec link to point to current spec because DSTU2 had maturity listed on a different page
1334      tr = tbl.tr();
1335      markBoilerplate(tr.td()).ah("http://hl7.org/fhir/versions.html#maturity", "Maturity Level").attribute("class", "fmm").tx(context.formatPhrase(RenderingContext.CANON_REND_COMMITTEE)+":");
1336      tr.td().tx(ExtensionUtilities.readStringExtension(cr, ExtensionDefinitions.EXT_FMM_LEVEL));
1337    }    
1338  }
1339
1340
1341  protected void renderCommitteeLink(XhtmlNode x, ResourceWrapper cr) {
1342    String code = cr.extensionString(ExtensionDefinitions.EXT_WORKGROUP);
1343    CodeSystem cs = context.getContext().fetchCodeSystem("http://terminology.hl7.org/CodeSystem/hl7-work-group");
1344    if (cs == null || !cs.hasWebPath())
1345      x.tx(code);
1346    else {
1347      ConceptDefinitionComponent cd = CodeSystemUtilities.findCode(cs.getConcept(), code);
1348      if (cd == null) {
1349        x.tx(code);        
1350      } else {
1351        x.ah(cs.getWebPath()+"#"+cs.getId()+"-"+cd.getCode()).tx(cd.getDisplay());
1352      }
1353    }
1354  }
1355  
1356  private void buildPublisherLinks(XhtmlNode x, CanonicalResource cr) {
1357    boolean useName = false;
1358    for (ContactDetail cd : cr.getContact()) {
1359      if (!cd.hasName()) {
1360        useName = true;
1361      }
1362    }
1363    boolean first = true;
1364    if (!useName) {
1365      x.tx(Utilities.escapeXml(cr.getPublisher())); 
1366      first = false;          
1367    }    
1368    for (ContactDetail cd : cr.getContact()) {
1369      String name = cd.hasName() ? cd.getName() : cr.getPublisher();
1370      if (cd.hasTelecom()) {
1371        if (first) first = false; else x.tx(". ");
1372        renderContact(x, name, cd.getTelecom());
1373      }
1374    }
1375  }
1376
1377  private void buildPublisherLinks(XhtmlNode x, ResourceWrapper cr) {
1378    boolean useName = false;
1379    for (ResourceWrapper cd : cr.children("contact")) {
1380      if (!cd.has("name")) {
1381        useName = true;
1382      }
1383    }
1384    boolean first = true;
1385    if (!useName) {
1386      x.tx(Utilities.escapeXml(cr.primitiveValue("publisher")));
1387      first = false;          
1388    }    
1389    for (ResourceWrapper cd : cr.children("contact")) {
1390      String name = cd.has("name") ? cd.primitiveValue("name") : cr.primitiveValue("publisher");
1391      if (cd.has("telecom")) {
1392        if (first) first = false; else x.tx(". ");
1393        renderContactW(spanIfTracking(x, cd), name, cd.children("telecom"));
1394      }
1395    }
1396  }
1397  
1398
1399  private void renderContactW(XhtmlNode x, String name, List<ResourceWrapper> telecom) {
1400    List<String> urls = new ArrayList<>();
1401    for (ResourceWrapper t : telecom) {
1402      if ("url".equals(t.primitiveValue()) && t.has("value")) {
1403        urls.add(t.primitiveValue("value"));
1404      }
1405    }
1406    if (urls.size() == 1) {
1407      x.ah(urls.get(0)).tx(name);
1408    } else { // if (urls.size() == 0) {
1409      x.tx(name);
1410    } 
1411    for (ResourceWrapper t : telecom) {
1412      String system = t.primitiveValue("system");
1413      String value = t.primitiveValue("value"); 
1414      if ("url".equals(system) && value != null && urls.size() != 1) {
1415        x.tx(", ");
1416        x.ah(t.primitiveValue("value")).tx("Link");
1417      }
1418      if ("email".equals(system) && value != null) {
1419        x.tx(", ");
1420        x.ah("mailto:"+t.primitiveValue("value")).tx("Email");
1421      }
1422      if ("phone".equals(system) && value != null) {
1423        x.tx(", ");
1424        x.tx(t.primitiveValue("value"));
1425      }
1426      if ("fax".equals(system) && value != null) {
1427        x.tx(", ");
1428        x.tx("Fax:"+t.primitiveValue("value"));
1429      }
1430    } 
1431  }
1432
1433  private void renderContact(XhtmlNode x, String name, List<ContactPoint> telecom) {
1434    List<String> urls = new ArrayList<>();
1435    for (ContactPoint t : telecom) {
1436      if (t.getSystem() == ContactPointSystem.URL && t.hasValue()) {
1437        urls.add(t.getValue());
1438      }
1439    }
1440    if (urls.size() == 1) {
1441      x.ah(urls.get(0)).tx(name);
1442    } else { // if (urls.size() == 0) {
1443      x.tx(name);
1444    } 
1445    for (ContactPoint t : telecom) {
1446      if (t.getSystem() == ContactPointSystem.URL && t.hasValue() && urls.size() != 1) {
1447        x.tx(", ");
1448        x.ah(t.getValue()).tx("Link");
1449      }
1450      if (t.getSystem() == ContactPointSystem.EMAIL && t.hasValue()) {
1451        x.tx(", ");
1452        x.ah("mailto:"+t.getValue()).tx("Email");
1453      }
1454      if (t.getSystem() == ContactPointSystem.PHONE && t.hasValue()) {
1455        x.tx(", ");
1456        x.tx(t.getValue());
1457      }
1458      if (t.getSystem() == ContactPointSystem.FAX && t.hasValue()) {
1459        x.tx(", ");
1460        x.tx("Fax:"+t.getValue());
1461      }
1462    } 
1463  }
1464
1465  protected String describeStatus(RenderingStatus status, ResourceWrapper cr) {
1466    String s = describeStatus(cr.primitiveValue("status"), cr.primitiveValue("experimental"), cr.child("date"), cr.extensionString("http://hl7.org/fhir/StructureDefinition/valueset-deprecated"));
1467    if (cr.hasExtension(ExtensionDefinitions.EXT_STANDARDS_STATUS)) {
1468      status.setExtensions(true);
1469      s = s + presentStandardsStatus(cr.extensionString(ExtensionDefinitions.EXT_STANDARDS_STATUS));
1470    }
1471    return s;
1472  }
1473
1474  protected String describeStatus(RenderingStatus status, CanonicalResource cr) {
1475    String s = describeStatus(cr.getStatus(), cr.hasExperimental() ? cr.getExperimental() : false, cr.hasDate() ? cr.getDateElement() : null, ExtensionUtilities.readBooleanExtension(cr, "http://hl7.org/fhir/StructureDefinition/valueset-deprecated"));
1476    if (cr.hasExtension(ExtensionDefinitions.EXT_STANDARDS_STATUS)) {
1477      status.setExtensions(true);
1478      s = s + presentStandardsStatus(ExtensionUtilities.readStringExtension(cr, ExtensionDefinitions.EXT_STANDARDS_STATUS));
1479    }
1480    return s;
1481  }
1482
1483  private String presentStandardsStatus(String code) {
1484    String pfx = " (<a href=\"http://hl7.org/fhir/codesystem-standards-status.html#"+code+"\">Standards Status</a>: ";
1485    switch (code) {
1486    case "draft" : return pfx+"Draft)"; 
1487    case "normative" : return pfx+"Normative)"; 
1488    case "trial-use" : return pfx+"Trial Use)"; 
1489    case "informative" : return pfx+"Informative)"; 
1490    case "deprecated" : return pfx+"<span style=\"color: maroon; font-weight: bold\">Deprecated</span>)"; 
1491    case "external" : return pfx+"External)"; 
1492    }
1493    return "";
1494  }
1495
1496  protected String describeStatus(PublicationStatus status, boolean experimental, DateTimeType dt, Boolean deprecated) {
1497    String sfx = dt != null ? " as of "+displayDataType(dt) : "";
1498    if (deprecated != null && deprecated) {
1499      if (status == PublicationStatus.RETIRED) {
1500        return "Deprecated + Retired"+sfx;
1501      } else {
1502        return "Deprecated"+sfx; 
1503      }
1504    } else {
1505      switch (status) {
1506      case ACTIVE: return (experimental ? "Experimental" : "Active")+sfx; 
1507      case DRAFT: return "Draft"+sfx;
1508      case RETIRED: return "Retired"+sfx;
1509      default: return "Unknown"+sfx;
1510      }
1511    }
1512  }
1513
1514  protected String describeStatus(String status, String experimental, ResourceWrapper dt, String deprecated) {
1515    String sfx = dt != null ? " as of "+displayDataType(dt) : "";
1516    if ("true".equals(deprecated)) {
1517      if ("retired".equals(status)) {
1518        return "Deprecated + Retired"+sfx;
1519      } else {
1520        return "Deprecated"+sfx; 
1521      }
1522    } else {
1523      switch (status) {
1524      case "active": return ("true".equals(experimental) ? "Experimental" : "Active")+sfx; 
1525      case "draft": return "Draft"+sfx;
1526      case "retired": return "Retired"+sfx;
1527      default: return "Unknown"+sfx;
1528      }
1529    }
1530  }
1531  protected void addContained(RenderingStatus status, XhtmlNode x, List<ResourceWrapper> list) throws FHIRFormatError, DefinitionException, FHIRException, IOException, EOperationOutcome {
1532    for (ResourceWrapper c : list) {
1533      x.hr();
1534      String id = c.getScopedId();
1535      if (!context.hasAnchor(id)) {
1536        context.addAnchor(id);
1537        x.an(context.prefixAnchor(id));
1538      }
1539      RendererFactory.factory(c, context.forContained()).setInner(true).buildNarrative(status, x, c);
1540    }
1541  }
1542
1543  protected void selfLink(XhtmlNode node, String link) {
1544    XhtmlNode a = node.addTag("a");
1545    if (node.hasAttribute("class"))
1546      node.setAttribute("class", node.getAttribute("class")+" self-link-parent");
1547    else
1548      node.setAttribute("class", "self-link-parent");
1549    a.setAttribute("href", "#"+link);
1550    a.setAttribute("title", "link to here");
1551    a.setAttribute("class", "self-link");
1552    XhtmlNode svg = a.addTag("svg");
1553    XhtmlNode path = svg.addTag("path");
1554    String pathData = "M1520 1216q0-40-28-68l-208-208q-28-28-68-28-42 0-72 32 3 3 19 18.5t21.5 21.5 15 19 13 25.5 3.5 27.5q0 40-28 68t-68 28q-15 0-27.5-3.5t-25.5-13-19-15-21.5-21.5-18.5-19q-33 31-33 73 0 40 28 68l206 207q27 27 68 27 40 0 68-26l147-146q28-28 28-67zm-703-705q0-40-28-68l-206-207q-28-28-68-28-39 0-68 27l-147 146q-28 28-28 67 0 40 28 68l208 208q27 27 68 27 42 0 72-31-3-3-19-18.5t-21.5-21.5-15-19-13-25.5-3.5-27.5q0-40 28-68t68-28q15 0 27.5 3.5t25.5 13 19 15 21.5 21.5 18.5 19q33-31 33-73zm895 705q0 120-85 203l-147 146q-83 83-203 83-121 0-204-85l-206-207q-83-83-83-203 0-123 88-209l-88-88q-86 88-208 88-120 0-204-84l-208-208q-84-84-84-204t85-203l147-146q83-83 203-83 121 0 204 85l206 207q83 83 83 203 0 123-88 209l88 88q86-88 208-88 120 0 204 84l208 208q84 84 84 204z";
1555    svg.attribute("height", "20").attribute("width", "20").attribute("viewBox", "0 0 1792 1792").attribute("class", "self-link");
1556    path.attribute("d", pathData).attribute("fill", "navy");
1557  }
1558};