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