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