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