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