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