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