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